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

  Hey! Hope you are having a great day so far!html

  今天想和你們討論的是一道我從這學期cs的期末考試獲得靈感的題:Get 24 Poker Game。說到 Get 24 Poker Game,也就是咱們一般說的湊24點,你們可能都比較熟悉。可是由於這個遊戲有不少變種,咱們仍是來簡單說一下游戲的規則。老規矩,上Wikipedia:算法

The 24 Game is an arithmetical card game in which the objective is to find a way to manipulate four integers so that the end result is 24.express

  簡單來講,這個遊戲須要玩家快速用隨機抽出的四張牌上的整數經過常見的加減乘除計算來湊出24點來取得勝利(必定要用全4張,不能夠棄牌)。不一樣於咱們一般玩的鬥地主,釣魚等等撲克遊戲,湊24點更考驗玩家的數學反應能力(less fun but somewhat interesting:))。在這裏稍稍跑一下題,由於可能會有人好奇,爲何偏偏選了24這個數字。其實答案很直白,由於這個數字最容易被湊出來:24是擁有八個除數的最小整數(1, 2, 3, 4, 6, 8, 12, and 24),使用這個數字不只僅把遊戲變得更加簡單(畢竟可能沒有人想花一個小時在用撲克牌湊數上),還把遊戲做廢(目標數字湊不出來)的概率降到最小。app

  好了,但願你們對這個卡牌遊戲已經有一些基礎的認知了,可是在正式開始編這個遊戲以前,我想先從這學期cs期末考試的一道題入手:less

  It is possible to combine the numbers 1, 5, 6, 7 with arithemtic operations to get 21 as follows: 6/(1-5/7).函數

Part I: oop

  Write a function that takes in a list of three numbers and a target number, and returns a string that contains an expression that uses all the numbers in the list once, and results in the target. Assume that the task is possible without using parentheses.測試

  For example, get_target_noparens([3, 1, 2], 7) can return "2*3+1" or "1+2*3" (either output would be fine).this

 

Part II:lua

  Now, write the function get_target which returns a string that contains an expression that uses all the numbers in the list once, and results in the target. The expression can contain parentheses. Assume that the task is possible.

  For example, get_target([1, 5, 6, 7], 21) can return "6/(1-5/7)" , this will return all permutation of the list of number.

 

  這道題有兩部分,第一部分的大意是寫出一個函數,讓其可以以列表的形式接收三個數字並試圖用加減乘除的方法來湊出目標點數(注意,這裏不包括括號操做)。第二部分相對於前一部分難度更高一點,要求寫出函數,讓其可以接受任意數量的數字,並可經過對其進行加減乘除(這裏包括括號)的操做來湊出目標點數。另外,在作這道題的時候不須要考慮備選數字沒有辦法湊出目標點數的狀況。

  咱們不難看出,第二題其實和咱們想要作出的卡牌遊戲的解法基本上是徹底重合,甚至更加簡單。只需把函數的目標數字設爲24,並且限制前面列表的數字數量爲4,就可以作出這個遊戲的解法。但第一題給考生了一個基礎的思路,也就是說第一題給第二題作了很好的鋪墊,可以讓人更快的找到正確的邏輯,完成解題過程。因此只要解決這兩道題,咱們就能夠很流暢的找出湊24點的最終解法(多作一道賺一道啊!)。

 

  話很少說,咱們開始!(完整代碼在文末)


   

  經過讀題,咱們能夠發現第一題和第二題最主要的差異在如下兩點上:

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

  記住以上兩點:)若是咱們能在作第一小題時找到關於第二小題的啓發,會事半功倍的!

 

  第一小題咱們能夠從運算式子的結構入手。正常寫出的話,一個有三個運算數字的式子會長這個樣子:

$1+2+3$

結構一目瞭然,三個數字,兩個運算符號。若是想生成全部排列組合的可能性的話,咱們能夠用嵌套for循環很容易的用代碼實現以下:

1 def get_all_comb(num_list):
2     '''find all arithmatic combination using the given 3 numbers'''
3 
4     for op_1 in ['+','-','*','/']: # 用第一個for loop來考慮全部第一個位置的運算符號的可能性
5         for op_2 in ['+','-','*','/']: # 用第二個for loop來考慮全部第二個位置的運算符號的可能性
6             comb = str(num_list[0])+op_1+str(num_list[1])+op_2+str(num_list[2]) # 組裝運算式
7             print(comb) # 打印運算式

  這段代碼的輸出結果爲 ‘1+2+3’,‘1+2-3’,‘1+2*3’...等等十六個不重複的運算式。可是咱們還要考慮到全部數字的排列組合的狀況,注意在以上的例子裏,全部運算的數字是沒有變化的,但數字位置的變化在多數狀況下會對運算結果形成影響,也就是說在咱們改變運算符號的同時,咱們也要考慮到全部數字的排列狀況(這裏指permutation)。

  一樣,咱們也能夠用以上類似的嵌套循環邏輯來用代碼實現:

1 def get_num_comb(num_list):
2     '''find all combination possibilities of the given 3 numbers'''
3 
4     all_comb = [] # 準備收集全部排列組合
5     for i in range(3): # 三個嵌套for循環分別對應在num_list裏的序數
6         for j in range(3):
7             for k in range(3):
8                 if i != j and i != k and j != k: # 肯定沒有重複元素
9                     print([num_list[i], num_list[j], num_list[k]]) #打印最終結果

  可是咱們能夠經過以上兩段代碼發現,在這裏用for loop來分別實現符號和數字的排列組合雖然是可行的(同理咱們也能夠用相似的for loop結構來),但卻沒法延伸到這道題的侷限外,也就是說,這個思路僅限於這道題的Part 1,若是要作第二部分的話,咱們須要從新寫這部分的函數(這也是這兩道題的第一個主要差異:數字數量的不肯定性)。

  爲了使第一部分的解法能夠延伸到第二題,咱們須要換個思路。很天然的,爲了解決數字數量的不肯定問題,咱們不可以再使用for loop這種須要定量條件的方法,而是使用遞歸(recursion)。

  以上咱們討論到的兩個問題,運算符號以及運算數字的排列組合,能夠被分別寫做兩個遞歸函數。比起普通循環,遞歸函數的結構更加複雜。爲了減小碼代碼時出現沒必要要的概念不清的錯誤,咱們能夠針對每一個遞歸畫出樹形圖來做結構分析。

  咱們先來看運算符號的遞歸規律,若是咱們有三個須要考慮的運算位置的話,樹形圖便以下圖:

  經過觀察,咱們能夠看到第一層有4個分支,第二層有16個,第三層有64個。不難看出,這個遞歸規律的複雜度是隨着遞歸的深度而以二次增加,因此能夠用Big-Oh Notation表達成 O(4^n),n爲運算符號的個數。(關於運算複雜度和常見algorithm會有後續文章跟進,在這裏不作過多解釋)

根據以上基礎結構,咱們能夠用代碼來寫出生成運算符號的遞歸函數,以下:

 1 def generate_comb_op(n):
 2     '''find all combination of Arithmetic operators with n possible spaces for operators'''
 3     # 創建base case
 4     if n==0:
 5         return [] # 當n爲0時不返回任何操做符號
 6     elif n ==1:
 7         return [['+'],['-'],['*'],['/']] # 當n爲1時返回基礎的四個符號,注意這裏須要用到list of list
 8     op_list = generate_comb_op(n-1) # 由於以後要加,因此咱們這裏用到遞歸的邏輯,來找到最基本的operator_list
 9     all_op_list = [] # 新建一個list來準備更新咱們加了運算符號後的sublist
10     # 最後咱們仍是要用循環的邏輯來給咱們原來list裏的元素加新的符號
11     for i in op_list:
12         for j in ['+','-','*','/']:
13             all_op_list.append(i+[j]) # 這裏用了新的list,來確保每一個sublist的長度是相等的
14 
15     return all_op_list # 最後返回最終結果

  若是再次檢查運算複雜度,咱們不難看出這個函數的複雜度符合咱們的預測,爲O(4^n)。

  好了,咱們再來看數字的排列方法。若是想要找到固定數量的數字的全部排列方式,咱們須要用到permutation的邏輯:找到全部排列(長度爲n)的第一個元素,而後根據每一個元素找到剩餘數字的第一個元素(剩餘數字排列長度爲n-1),以此類推,直到最後只剩餘一個數字。咱們來看一下這個遞歸思路的樹狀圖(此樹狀圖用了長度爲三的list爲例):

  遞歸的第一層有三個元素,第二層有3*2=6個元素,第三層有3*2*1=6個元素,咱們能夠看出這個邏輯的複雜度爲 O(n!), n爲須要排列組合數字的個數。

Permutation的邏輯比運算符號的排列稍稍複雜,可是咱們能夠用相似的遞歸結構來解決不一樣的問題,代碼以下:

 1 def generate_permutated_list(num_list):
 2     '''find permuted lists of n given numbers'''
 3     # 創建base case
 4     if len(num_list) == 0:
 5         return [] # 當n爲0時不返回任何數字
 6     if len(num_list) == 1:
 7         return [num_list] # 當n爲1時返回全部式子,做爲以後首數字的基礎
 8     list_of_comb = [] # 新建列表來存更新的排列
 9     for i in range(len(num_list)):
10         first_num = num_list[i] # 生成首字母
11         for j in generate_permutated_list(num_list[:i] + num_list[i+1:]): # 去除首字母,繼續遞歸
12             list_of_comb.append([first_num] + j) #加入新的list
13             
14     return list_of_comb # 最後返回最終結果

  分別生成數學操做符號以及全部數字的排列組合後,咱們要把兩個組合整合起來,以今生成全部的排列可能性。由於這裏咱們不用考慮排列組合數列的不肯定性的問題(每一個排列的長度,以及每組數學操做符號的長度維持不變),咱們能夠用循環的思惟來生成全部數學表達式(全部數字和數學操做符號的組合)。可是生成全部數學表達式還不能完整的解決這個問題,由於咱們不只僅要生成全部的數學表達式,還要把表達式估值並和最終的目標數字進行比較。因此在組合最終的函數以前,咱們須要先寫一個估值函數來方便以後的使用。

  估值函數的難點在於數學操做符號的處理,由於在數學表達式裏這些運算符號都是以字符串的形式表達,例如 ‘+’,‘-’,因此沒法看成正常運算符號放到代碼中來操做。因此在這個狀況,咱們要從新賦予這些字符串它們象徵的含義,代碼以下:

 1 def modify_op(equation, op):
 2     '''this function modify the given equation by only computing the section with the given operators
 3     parameters:
 4         equation: a list that represents a given mathematical equation which may or may not contain the 
 5                 given numerical operators. Ex, ['1','+','2'] represents the equation 1+2
 6         op: a string that is the given numerical operators'''
 7     
 8     # 這裏咱們把表明數學計算的字符串和以上定義的操做函數的名字以字典的方式聯繫並儲存起來
 9     operators = {'/':division, '*':multiply, '+':add, '-':subtract}
10     
11     while op in equation: # 用while循環來確保沒有遺漏任何字符
12         i = equation.index(op) # 找到表達式內的第一處須要計算的字符位置
13         if op == '/' and equation[i+1] == '0': # 考慮除法操做的被除數爲0的狀況
14             return ['']
15         # 把表達式須要計算的部分替換成計算結果
16         equation[i-1:i+2] = [str(operators[op](float(equation[i-1]), float(equation[i+1])))] # 注意這裏調用了前面字典裏儲存的函數名
17     return equation # 返回結果
18 
19 def evaluate(equation):
20     '''return the evaluated result in float for the equation'''
21 
22     for op in ['/','*','+','-']: # 這裏須要注意標點順序,除在最早,由於須要考慮特殊狀況,乘其次,而後纔是加減
23         equation = modify_op(equation, op) # 使用helper function
24     return equation[0] # 最後返回最終計算結果

  這裏咱們須要注意,這個估值函數可以接收表達式的形式爲list,而list裏的每項也必需要用字符串的形式來表達。

  最後,咱們只要按照以前提到的思路,整合表達式,並用以上估值函數來計算表達式的值,就能夠完成這道題。在給出完整代碼以前,咱們再來最後複習一下這道題的解題思路:

  1. 找出全部加減乘除的排列組合
  2. 找出全部數字的排列組合
  3. 整合全部表達式可能
  4. 用估值函數計算表達式
  5. 對比表達式答案和目標數
  6. 返回符合要求的表達式

  好了,那咱們來看一下完整代碼:

  1 #  Write a function that takes in a list of three numbers and a target number, and
  2 #  returns a string that contains an expression that uses all the numbers
  3 #  in the list once, and results in the target. Assume that the task is possible
  4 #  without using parentheses.
  5 #
  6 #  For example, get_target_noparens([3, 1, 2], 7) can return "2*3+1" or "1+2*3"
  7 #  (either output would be fine).
  8 
  9 ############################################# 數學表達式生成函數 #############################################
 10 
 11 def generate_comb_op(n):
 12     '''find all combination of Arithmetic operators with n possible spaces for operators'''
 13     # 創建base case
 14     if n==0:
 15         return [] # 當n爲0時不返回任何操做符號
 16     elif n ==1:
 17         return [['+'],['-'],['*'],['/']] # 當n爲1時返回基礎的四個符號,注意這裏須要用到list of list
 18     op_list = generate_comb_op(n-1) # 由於以後要加,因此咱們這裏用到遞歸的邏輯,來找到最基本的operator_list
 19     all_op_list = [] # 新建一個list來準備更新咱們加了運算符號後的sublist
 20     # 最後咱們仍是要用循環的邏輯來給咱們原來list裏的元素加新的符號
 21     for i in op_list:
 22         for j in ['+','-','*','/']:
 23             all_op_list.append(i+[j]) # 這裏用了新的list,來確保每一個sublist的長度是相等的
 24 
 25     return all_op_list # 最後返回最終結果
 26 
 27 
 28 def generate_permutated_list(num_list):
 29     '''find permuted lists of n given numbers'''
 30     # 創建base case
 31     if len(num_list) == 0:
 32         return [] # 當n爲0時不返回任何數字
 33     if len(num_list) == 1:
 34         return [num_list] # 當n爲1時返回全部式子,做爲以後首數字的基礎
 35     list_of_comb = [] # 新建列表來存更新的排列
 36     for i in range(len(num_list)):
 37         first_num = num_list[i] # 生成首字母
 38         for j in generate_permutated_list(num_list[:i] + num_list[i+1:]): # 去除首字母,繼續遞歸
 39             list_of_comb.append([first_num] + j) #加入新的list
 40 
 41     return list_of_comb # 最後返回最終結果
 42 
 43 
 44 #################################### 定義全部可能出現的數學操做,包括加減乘除 ####################################
 45 
 46 def division(a,b): # 除法比較特殊,在以後的代碼裏會考慮到被除數爲0的狀況
 47     return a/b
 48 def multiply(a,b):
 49     return a*b
 50 def add(a,b):
 51     return a+b
 52 def subtract(a,b):
 53     return a-b
 54 
 55 ############################################ 數學表達式處理函數 ##############################################
 56 
 57 def modify_op(equation, op):
 58     '''this function modify the given equation by only computing the section with the given operators
 59     parameters:
 60         equation: a list that represents a given mathematical equation which may or may not contain the 
 61                 given numerical operators. Ex, ['1','+','2'] represents the equation 1+2
 62         op: a string that is the given numerical operators'''
 63     
 64     # 這裏咱們把表明數學計算的字符串和以上定義的操做函數的名字以字典的方式聯繫並儲存起來
 65     operators = {'/':division, '*':multiply, '+':add, '-':subtract}
 66     
 67     while op in equation: # 用while循環來確保沒有遺漏任何字符
 68         i = equation.index(op) # 找到表達式內的第一處須要計算的字符位置
 69         if op == '/' and equation[i+1] == '0': # 考慮除法操做的被除數爲0的狀況
 70             return ['']
 71         # 把表達式須要計算的部分替換成計算結果
 72         equation[i-1:i+2] = [str(operators[op](float(equation[i-1]), float(equation[i+1])))] # 注意這裏調用了前面字典裏儲存的函數名
 73     return equation # 返回結果
 74 
 75 def evaluate(equation):
 76     '''return the evaluated result in float for the equation'''
 77 
 78     for op in ['/','*','+','-']: # 這裏須要注意標點順序,除在最早,由於須要考慮特殊狀況,乘其次,而後纔是加減
 79         equation = modify_op(equation, op) # 使用helper function
 80     return equation[0] # 最後返回最終計算結果
 81 
 82 ############################################# 最終使用函數 ###############################################
 83 
 84 def get_target_noparens(num_list, target):
 85     op_list = generate_comb_op(len(num_list)-1) # 找出全部加減乘除的排列組合
 86     num_comb = generate_permutated_list(num_list) # 找出全部數字的排列組合
 87     # 用for嵌套循環來整合全部表達式可能
 88     for each_op_list in op_list: 
 89         for each_num_list in num_comb:
 90             equation = [] # 用list初始化表達式
 91             equation_str = '' # 用string表達算式
 92             for i in range(len(each_op_list)):
 93                 equation.extend([str(each_num_list[i]), each_op_list[i]])
 94                 equation_str += str(each_num_list[i]) + each_op_list[i]
 95             equation.append(str(each_num_list[-1]))
 96             equation_str += str(each_num_list[-1])
 97             
 98             result = evaluate(equation) # 表達式估值,這裏要用list的形式
 99             if float(result) == float(target):
100                 return equation_str # 用字符串返回算式

  咱們稍做測試:

  • get_target_noparens([1,2,3], 6)返回 1+2+3
  • get_target_noparens([1,2,3], 4.5)返回 1+3/2
  • get_target_noparens([23,1,3], 68)返回 23*3-1

  三個測試都成功經過,那麼咱們的第一題就解完了。

  咱們這裏對第一題的解法一樣可以應用到第二題的基礎部分,由於篇幅緣由,第二題的解題方式會放到下一篇講解,連接以下:

  https://www.cnblogs.com/Lin-z/p/14219620.html

  

  那咱們今天就到這裏,新年快樂!

 

 

參考資料:

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