python實例:解決經典撲克牌遊戲 -- 四張牌湊24點 (二)

  Hey! 若是你尚未看這篇的上文的話,能夠去稍稍瞅一眼,會幫助加速理解這一篇裏面涉及到的遞歸結構哦!(上一篇點這裏:《python實例:解決經典撲克牌遊戲 -- 四張牌湊24點 (一)》html

  若是你已經看完了第一部分的解析,那咱們能夠來繼續上道題的第二部分。python

  根據第一部分的分析,第二部分的難點主要在如下兩點:算法

  1. 第一題只須要考慮三個數字,兩個符號的排列組合,也就是說在改變運算符號時,咱們只需考慮4選1,或者4選2的狀況。而第二題卻須要徹底不一樣的思路,要在不肯定總數字量的狀況下,在每兩個數字間考慮到全部排列可能。
  2. 第二題須要考慮到一個,或者多個括號的算法,在這種狀況下,咱們不能直接計算結果,由於沒有辦法肯定括號的個數和位置。

  那麼經過重複利用第一部分的代碼,咱們發現第一個難點已經被攻克了,因而如今咱們能夠直接分析第二個難點(如何找到括號的全部合理位置)來完整第二部分的代碼(完整代碼見文末):express

  由於輸入函數的內容是不肯定的(數字不肯定,括號的位置也不肯定),因此咱們頗有可能會須要用到recursion的邏輯來解決括號的位置問題(在後文會更加深刻的解釋爲何須要用到遞歸)。在開始思考具體解題方法以前,咱們首先要明確,括號不可以只包括一個運算數字(至少兩個或更多),括號也不須要被放到整個式子的左右。同時,咱們也要避免增長沒必要要的括號,例如1*((3*4)/5),這個狀況下(3*4)周圍的括號是沒必要要的。app

  好了,知道了這些條件,咱們能夠從最簡單的表達式試着入手(在須要加入括號的狀況下,表達式最少須要三位數字),好比:函數

$1+2\times 3$oop

  在這個狀況下,咱們能夠怎麼加入括號呢?答案是有兩種,以下:測試

$\left( 1+2 \right) \times 3$this

$1+\left( 2\times 3 \right) $lua

  在以上兩種狀況下,咱們不可以繼續加入括號,由於若是把括號內的內容算做一整個計算單位(這裏的計算單位指的是表達式裏的一個元素,等同於單個數字的概念),咱們這個表達式就只剩下了兩個單位,在這個狀況下,在任何位置加入括號都會違反咱們以前設定的規則(括號不能只包括一個數字,同時不能包括所有數字)。因此在有三個元素的狀況下,這個表達式就有三個排列組合的可能性(算上沒有括號的狀況)。

  這看上去好像過於簡單了,那咱們能夠嘗試一下四位數字的表達式,好比:

$1+2\times 3\div 4$

  咱們能夠怎麼在這個表達式中加入括號呢?比起上面的例子,這個表達式明顯所需步驟更多,因此咱們來逐步分析:

  當只須要加入一對括號時,咱們能夠這麼加:

$\left( 1+2 \right) \times 3\div 4  \qquad 1+\left( 2\times 3 \right) \div 4  \qquad 1+2\times \left( 3\div 4 \right) $

$\left( 1+2\times 3 \right) \div 4 \qquad 1+\left( 2\times 3\div 4 \right) $

  注意以上咱們能夠從兩個角度加入括號:括號包括兩個數字,以及括號包括三個數字。這裏括號以從左到右的順序加入。

  當須要加入兩對括號時,咱們能夠這麼加:

$\left( \left( 1+2 \right) \times 3 \right) \div 4  \qquad  \left( 1+2 \right) \times \left( 3\div 4 \right) $

$\left( 1+\left( 2\times 3 \right) \right) \div 4   \qquad  1+\left( \left( 2\times 3 \right) \div 4 \right) $

$\left( 1+2 \right) \times \left( 3\div 4 \right)   \qquad  1+\left( 2\times \left( 3\div 4 \right) \right)  $

  以上咱們在加入一對括號的基礎上,把已括入的內容算做一個計算單位,再加入第二對括號,括號以從左到右邊的順序加入。注意在這個例子裏有重複的表達式,$\left( 1+2 \right) \times \left( 3\div 4 \right) $ 因此實際上咱們是有 11 種排列方式(算上沒有括號的狀況)。

  你看出來規律了麼?若是沒有的話,咱們能夠試試把以上過程用樹形圖表達出來:

  有沒有稍稍清晰一點點?咱們能夠發現每層之間的過渡實際上是同樣的。從第一層到第二層,咱們把一對括號插入序列中全部可能且合理的位置。爲了清晰結構,咱們如今只把目光放在第二層,這樣即可以發現咱們其實從2(容許的列表最小長度)開始,開始給相鄰的,長度爲2的序列湊對(括號只對相鄰的數字生效,中間不能分裂)。隨後咱們開始給相鄰的,長度爲三的序列湊對。以此類推,若是咱們原本的序列長度爲5,咱們便會從長度2開始湊對,隨後用長度3,而後長度4。

  當把括號裏的內容看做一個單位,但第二層生成的序列的長度仍舊大於2時,咱們即可以就計算單位繼續以不一樣長度來湊對,這也就是第三層的雙重括號的來源:咱們在每個分支裏,用長度爲2的分序列給長度爲3的總序列湊對。在以上的結構中,層與層之間的傳遞十分明顯,幾乎在跟咱們明示,這是一個遞歸結構!!!

  相比較第一部分(見《python實例:解決經典撲克牌遊戲 -- 四張牌湊24點 (一)》)用到的全部遞歸函數,這個結構會更加複雜,因此咱們須要仔細分析代碼實現以上遞歸結構所需的幾個重點:

  1. 首先,咱們會發現不是全部的分支都能發展到最深的一層,只有知足條件(長度大於2)的分支才能繼續發展,不然分支會就此中止。這意味着咱們代碼中的base case和序列的長度有關。
  2. 其次,雖然每層的 ‘湊對’ 過程是同樣的,咱們也須要考慮用不一樣的長度來湊,而層與層之間的序列長度並不相同。因此,由於長度的不肯定性,咱們一樣能夠考慮使用遞歸的手法來實現(這裏並非指只用循環沒法實現,而是指用遞歸會更加天然)
  3. 最後,括號加入的順序也很重要,由於咱們須要從最簡單的狀況,也就是沒有括號的序列開始考慮,而後逐漸考慮一個括號,再到兩個括號,由於只有這樣纔可以實現表達式的最簡化。

  那麼咱們準備好用代碼實現了麼?尚未,由於咱們須要考慮,咱們須要用什麼形式來表示括號纔可以方便咱們以後的估值計算呢(在運算複雜度的最差可能性下,在咱們找到了全部括號可能時,咱們須要對每一個可能表達式進行估值操做,因此括號的表達方式越合理,越能提高運算的整體效率)?明顯,爲了格式的贊成,咱們能夠採用['(','1','+','2',')','+','3']的方式來表達括號,也就是括號要以其餘字符同樣的格式,字符串,插入到原式當中。但咱們能夠發現,這個形式在層與層之間的傳遞上並非很合理,由於字符串不可以直接給出表達式中的層次,致使咱們須要寫額外的代碼來判斷計算單位。並且這在估值操做上也不靈便,由於有多個括號的狀況下咱們容易混淆相鄰的不一樣括號(這裏不是不可行的意思,可是咱們頗有可能須要使用額外的循環操做來定義表達式內部的計算優先等級)。因此,在這裏咱們要利用列表的嵌套特色,來用俄羅斯套娃」 來直接表達括號所在位置。好比,(1+2)+3 能夠表達爲[['1','+','2'],'+','3']。

話很少說,咱們來用代碼實現找全部括號可能位置的功能。第一步,咱們先寫一個可以幫助咱們在每層用不一樣長度 「湊對」 的遞歸函數,來方便咱們遞歸過程當中的調用:

 

 1 def layer_sort(equation, depth=3): # 默認湊對的長度爲3(這裏是由於兩個數字加一個運算符號有三位數)
 2     '''generate all possible brackets combination within a recursive layer'''
 3     if depth == len(equation): # 若是湊對長度達到表達式總長,便返回表達式原式
 4         return []
 5     layer_comb = [] # 初始化一個總列表
 6 
 7     # 咱們要從最左邊開始定義左括號的位置,而後在相應長度的結束定義右括號的位置
 8     # len(equation)-depth+1 爲最右邊的左括號合理位置+1,是循環的結束位置
 9     for i in range(0,len(equation)-depth+1,2): # 間隔爲2,由於咱們要跳過一個數字和一個符號
10         new_equation = equation[:i]+[equation[i:i+depth]]+equation[i+depth:] # 寫出新表達式
11         layer_comb.append(new_equation)
12     for j in layer_sort(equation, depth+2):
13         layer_comb.append(j)
14     return layer_comb

 

而後咱們須要在每層使用這個函數,而後判斷函數返回結果是否符合遞歸條件,若是符合,便遞歸下去。這一步會完整咱們找全部括號表達式可能的函數,代碼以下:

 1 def generate_all_brackets(equation):
 2     '''get all bracket combination from the the given equation in list form'''
 3 
 4     layer_possibility = layer_sort(equation) # 找到本層可用的表達式
 5     all_possibility = layer_possibility[:]
 6     for i in layer_possibility:
 7         if len(i) > 3: # 檢查是否達成遞歸條件,這一步同時也是這個遞歸函數的base case
 8             next_layer_equ = generate_all_brackets(i)
 9             for j in next_layer_equ: # 去重操做
10                 if j not in all_possibility:
11                     all_possibility.append(j)
12 
13     return [equation] + all_possibility # 不要忘了在列表最開始加入原式

  好了,經過以上的函數,咱們可以找到全部表達式的排列組合,但由於加入了括號,咱們的估值函數也須要變更(基礎代碼參考上篇,新的估值函數會加入優先考慮括號內計算內容的算法)。在優先算括號內容的狀況下,會出現括號內又有括號內包括號的狀況,因此咱們須要找到方法去找到最內層的計算內容。由於括號位置的不肯定性以及層數的不肯定性,在這裏咱們也須要把估值函數更新成遞歸的結構,完整代碼以下:

 1 #################################### 定義全部可能出現的數學操做,包括加減乘除 ####################################
 2 
 3 def division(a,b): # 除法比較特殊,在以後的代碼裏會考慮到被除數爲0的狀況
 4     return a/b
 5 def multiply(a,b):
 6     return a*b
 7 def add(a,b):
 8     return a+b
 9 def subtract(a,b):
10     return a-b
11 
12 ############################################ 數學表達式處理函數 ##############################################
13 
14 def modify_op(equation, op):
15     '''this function modify the given equation by only computing the section with the given operators
16     parameters:
17         equation: a list that represents a given mathematical equation which may or may not contain the 
18                 given numerical operators. Ex, ['1','+','2'] represents the equation 1+2
19         op: a string that is the given numerical operators'''
20     
21     # 這裏咱們把表明數學計算的字符串和以上定義的操做函數的名字以字典的方式聯繫並儲存起來
22     operators = {'/':division, '*':multiply, '+':add, '-':subtract}
23     
24     while op in equation: # 用while循環來確保沒有遺漏任何字符
25         i = equation.index(op) # 找到表達式內的第一處須要計算的字符位置
26         if op == '/' and equation[i+1] == '0': # 考慮除法操做的被除數爲0的狀況
27             return ['']
28         # 把表達式須要計算的部分替換成計算結果
29         if equation[i+1] != '' and equation[i-1] != '':
30             equation[i-1:i+2] = [str(operators[op](float(equation[i-1]), float(equation[i+1])))] # 注意這裏調用了前面字典裏儲存的函數名
31         else:
32             return ['']
33     return equation # 返回結果
34 
35 def evaluate(equation):
36     '''updated version of the evaluation function, place the original loop in a recursive structure.'''
37 
38     for i in range(len(equation)):
39         if type(equation[i]) == list: # 若是表達式類型爲list
40             equation[i] = evaluate(equation[i]) # 知足括號條件,開始遞歸
41     for op in ['/','*','+','-']:
42         equation = modify_op(equation, op) # 使用helper function
43     return equation[0] # 最後返回最終計算結果

  這裏估值函數的主要更新在 evaluate() 函數裏,咱們檢查了列表裏的每一個元素,若是知足list of list的條件,便用遞歸傳遞下去計算括號內的內容,並把這些內容用計算結果來替換,最終返回結果(相似於咱們以前找括號位置的函數,只有知足要求的分支纔可以進行到下一層)。終於,咱們這道題快作完了!最後一個難點是,把列表形式的計算式轉換成字符串,這一步看似簡單,但卻由於不可以肯定括號的位置和具體結構而變得複雜。欸?這句話是否是有些似曾相識?對嘍!咱們還得寫最後一個遞歸函數來轉換列表和字符串的格式:)話很少說,編了這麼多遞歸了,咱們直接來看代碼:

1 def convert_to_str(equation_list):
2     equation = '' # 初始化字符串表達式
3     for i in range(len(equation_list)): 
4         if type(equation_list[i]) == list: # 這裏是遞歸條件,若是數據類型爲list,咱們要把括號內的表達式傳到下一層
5             equation += '(' + convert_to_str(equation_list[i]) + ')' # 加入括號
6         else:
7             equation += equation_list[i] # base case, 若是數據類型不是list,那麼就普通返回字符串
8     return equation # 返回字符串形式的表達式

  最後,咱們把以上的全部代碼整合,寫出這道題第二部分的解答,代碼以下:

  1 #  Write the function get_target which returns a string that contains an expression that uses all the numbers
  2 #  in the list once, and results in the target. The expression can contain parentheses. Assume that the task
  3 #  is possible.
  4 #  For example, get_target([1, 5, 6, 7], 21) can return "6/(1-5/7)". This will return all permutation of the
  5 #  list of number.
  6 
  7 ############################################# 數學表達式生成函數 #############################################
  8 
  9 def generate_comb_op(n):
 10     '''find all combination of Arithmetic operators with n possible spaces for operators'''
 11     # 創建base case
 12     if n==0:
 13         return [] # 當n爲0時不返回任何操做符號
 14     elif n ==1:
 15         return [['+'],['-'],['*'],['/']] # 當n爲1時返回基礎的四個符號,注意這裏須要用到list of list
 16     op_list = generate_comb_op(n-1) # 由於以後要加,因此咱們這裏用到遞歸的邏輯,來找到最基本的operator_list
 17     all_op_list = [] # 新建一個list來準備更新咱們加了運算符號後的sublist
 18     # 最後咱們仍是要用循環的邏輯來給咱們原來list裏的元素加新的符號
 19     for i in op_list:
 20         for j in ['+','-','*','/']:
 21             all_op_list.append(i+[j]) # 這裏用了新的list,來確保每一個sublist的長度是相等的
 22 
 23     return all_op_list # 最後返回最終結果
 24 
 25 
 26 def generate_permutated_list(num_list):
 27     '''find permuted lists of n given numbers'''
 28     # 創建base case
 29     if len(num_list) == 0:
 30         return [] # 當n爲0時不返回任何數字
 31     if len(num_list) == 1:
 32         return [num_list] # 當n爲1時返回全部式子,做爲以後首數字的基礎
 33     list_of_comb = [] # 新建列表來存更新的排列
 34     for i in range(len(num_list)):
 35         first_num = num_list[i] # 生成首字母
 36         for j in generate_permutated_list(num_list[:i] + num_list[i+1:]): # 去除首字母,繼續遞歸
 37             list_of_comb.append([first_num] + j) #加入新的list
 38 
 39     return list_of_comb # 最後返回最終結果
 40 
 41 
 42 #################################### 定義全部可能出現的數學操做,包括加減乘除 ####################################
 43 
 44 def division(a,b): # 除法比較特殊,用了try except來考慮到被除數爲0的狀況
 45     try:
 46         return a/b
 47     except:
 48         return ''
 49 
 50 def multiply(a,b):
 51     return a*b
 52 def add(a,b):
 53     return a+b
 54 def subtract(a,b):
 55     return a-b
 56 
 57 ############################################ 數學表達式處理函數 ##############################################
 58 
 59 def modify_op(equation, op):
 60     '''this function modify the given equation by only computing the section with the given operators
 61     parameters:
 62         equation: a list that represents a given mathematical equation which may or may not contain the 
 63                 given numerical operators. Ex, ['1','+','2'] represents the equation 1+2
 64         op: a string that is the given numerical operators'''
 65     
 66     # 這裏咱們把表明數學計算的字符串和以上定義的操做函數的名字以字典的方式聯繫並儲存起來
 67     operators = {'/':division, '*':multiply, '+':add, '-':subtract}
 68     
 69     while op in equation: # 用while循環來確保沒有遺漏任何字符
 70         i = equation.index(op) # 找到表達式內的第一處須要計算的字符位置
 71         # 把表達式須要計算的部分替換成計算結果
 72         if equation[i+1] != '' and equation[i-1] != '':
 73             equation[i-1:i+2] = [str(operators[op](float(equation[i-1]), float(equation[i+1])))] # 注意這裏調用了前面字典裏儲存的函數名
 74         else:
 75             return ['']
 76     return equation # 返回結果
 77 
 78 def evaluate(equation):
 79     '''updated version of the evaluation function, place the original loop in a recursive structure.'''
 80 
 81     for i in range(len(equation)):
 82         if type(equation[i]) == list: # 若是表達式類型爲list
 83             equation[i] = evaluate(equation[i]) # 知足括號條件,開始遞歸
 84     for op in ['/','*','+','-']:
 85         equation = modify_op(equation, op) # 使用helper function
 86 
 87     return equation[0] # 最後返回最終計算結果
 88 ############################################# 括號位置生成函數 #############################################
 89 
 90 def layer_sort(equation, depth=3): # 默認湊對的長度爲3(這裏是由於兩個數字加一個運算符號有三位數)
 91     '''generate all possible brackets combination within a recursive layer'''
 92     if depth == len(equation): # 若是湊對長度達到表達式總長,便返回表達式原式
 93         return []
 94     layer_comb = [] # 初始化一個總列表
 95 
 96     # 咱們要從最左邊開始定義左括號的位置,而後在相應長度的結束定義右括號的位置
 97     # len(equation)-depth+1 爲最右邊的左括號合理位置+1,是循環的結束位置
 98     for i in range(0,len(equation)-depth+1,2): # 間隔爲2,由於咱們要跳過一個數字和一個符號
 99         new_equation = equation[:i]+[equation[i:i+depth]]+equation[i+depth:] # 寫出新表達式
100         layer_comb.append(new_equation)
101     for j in layer_sort(equation, depth+2):
102         layer_comb.append(j)
103     return layer_comb
104 
105 def generate_all_brackets(equation):
106     '''get all bracket combination from the the given equation in list form'''
107 
108     layer_possibility = layer_sort(equation) # 找到本層可用的表達式
109     all_possibility = layer_possibility[:]
110     for i in layer_possibility:
111         if len(i) > 3: # 檢查是否達成遞歸條件,這一步同時也是這個遞歸函數的base case
112             next_layer_equ = generate_all_brackets(i)
113             for j in next_layer_equ: # 去重操做
114                 if j not in all_possibility:
115                     all_possibility.append(j)
116 
117     return [equation] + all_possibility # 不要忘了在列表最開始加入原式
118 
119 ########################################### 字符串格式轉換函數 #############################################
120 
121 def convert_to_str(equation_list):
122     equation = '' # 初始化字符串表達式
123     for i in range(len(equation_list)):
124         if type(equation_list[i]) == list: # 這裏是遞歸條件,若是數據類型爲list,咱們要把括號內的表達式傳到下一層
125             equation += '(' + convert_to_str(equation_list[i]) + ')' # 加入括號
126         else:
127             equation += equation_list[i] # base case, 若是數據類型不是list,那麼就普通返回字符串
128     return equation # 返回字符串形式的表達式
129 
130 ############################################# 最終使用函數 ################################################
131 
132 def get_target(num_list, target):
133     op_list = generate_comb_op(len(num_list) - 1)  # 找出全部加減乘除的排列組合
134     num_comb = generate_permutated_list(num_list)  # 找出全部數字的排列組合
135     # 用for嵌套循環來整合全部表達式可能
136     for each_op_list in op_list:
137         for each_num_list in num_comb:
138             equation, equation_list= [], []  # 初始化基礎表達式,以及基礎+括號表達式
139             for i in range(len(each_op_list)):
140                 equation.extend([str(each_num_list[i]), each_op_list[i]])
141             equation.append(str(each_num_list[-1])) # 組裝基礎表達式
142             equation_list.append(equation) # 把基礎表達式加入基礎+括號表達式的列表
143             equation_list.extend(generate_all_brackets(equation)) # 把全部括號表達式加入括號表達式的列表
144             for each_equation in equation_list:
145                 equation_str = convert_to_str(each_equation) # 先把列表轉化成字符串
146                 if evaluate(each_equation) == str(float(target)): # 若是最終結果相等便返回字符串
147                     return equation_str

  以上就是第二道題第二部分的所有代碼了,咱們稍做測試:

  • get_target([1,87,3,10],47) 返回  87-(1+3)*10
  • get_target([1,5,6,7],21) 返回  6/(1-5/7)
  • get_target([1,7,3], 11) 返回  1+7*3

  三個測試都經過,那咱們這道題就這麼結束了!

  這部分由於須要寫的遞歸函數太多,我只挑了最難的一個來作樹形圖和結構分析。若是其餘的遞歸函數也須要講解,麻煩請給我留言,我會根據需求作相應的講解:)。完事兒,收工!

 

  等等?我是否是忘了什麼?說好的作湊24點遊戲的解法呢???

  :)行吧,這就作,實際上以上的函數已經徹底解決了這個遊戲的核心難題,咱們只須要稍加潤色,即可以作出一個好用的24點的做弊器啦(義正詞嚴✧ (≖ ‿ ≖)✧)!在以上的函數基礎上,爲了讓這個做弊器更加的人性化,我用PyQt5作了簡單的GUI,加入了四張牌的問詢收錄,可修改功能,再來一次功能,以及————和電腦玩24點遊戲的功能(既然要作就作的全一點嘛。)

  仍是買個關子,既然這篇已經不短了,那咱們就把24點的遊戲製做放到下一篇吧,連接以下:

  還沒更,更了這裏就會有個連接:)再等會。

  

  Good good study, day day up! 下次再見!

  

  參考資料:

  • https://en.wikipedia.org/wiki/24_Game
  • 例題選自 University of Toronto, ESC180 2020 Final, Question 4 & Question 5
相關文章
相關標籤/搜索