Hey! 若是你尚未看這篇的上文的話,能夠去稍稍瞅一眼,會幫助加速理解這一篇裏面涉及到的遞歸結構哦!(上一篇點這裏:《python實例:解決經典撲克牌遊戲 -- 四張牌湊24點 (一)》)html
若是你已經看完了第一部分的解析,那咱們能夠來繼續上道題的第二部分。python
根據第一部分的分析,第二部分的難點主要在如下兩點:算法
那麼經過重複利用第一部分的代碼,咱們發現第一個難點已經被攻克了,因而如今咱們能夠直接分析第二個難點(如何找到括號的全部合理位置)來完整第二部分的代碼(完整代碼見文末):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',')','+','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
以上就是第二道題第二部分的所有代碼了,咱們稍做測試:
三個測試都經過,那咱們這道題就這麼結束了!
這部分由於須要寫的遞歸函數太多,我只挑了最難的一個來作樹形圖和結構分析。若是其餘的遞歸函數也須要講解,麻煩請給我留言,我會根據需求作相應的講解:)。完事兒,收工!
等等?我是否是忘了什麼?說好的作湊24點遊戲的解法呢???
:)行吧,這就作,實際上以上的函數已經徹底解決了這個遊戲的核心難題,咱們只須要稍加潤色,即可以作出一個好用的24點的做弊器啦(義正詞嚴✧ (≖ ‿ ≖)✧)!在以上的函數基礎上,爲了讓這個做弊器更加的人性化,我用PyQt5作了簡單的GUI,加入了四張牌的問詢收錄,可修改功能,再來一次功能,以及————和電腦玩24點遊戲的功能(既然要作就作的全一點嘛。)
仍是買個關子,既然這篇已經不短了,那咱們就把24點的遊戲製做放到下一篇吧,連接以下:
還沒更,更了這裏就會有個連接:)再等會。
Good good study, day day up! 下次再見!
參考資料: