Python實現24點遊戲

neuyu發表於2021-09-09

24點遊戲是一款老少咸宜的益智遊戲,遊戲的玩法是給出任意四個數字,透過加減乘除四則運算,計算出24。

網上有很多24點遊戲演算法,找出解法並不難,但是難在如何合適地加括號和去除等價的重複表示式上。

1. 目標和要求

我們的目標是給定任意N個正整數(N > 1),找到能夠將這N個數透過四則運算計算得出24的全部表示式,並且只在必要的時候加上括號以及去除等價的重複表示式。

首先,我們要明確什麼是合適的括號?就是指在不影響計算結果的前提下,能不加括號儘量不加括號,比如:

(15 + 8) + 7 -6 = 24 應寫作 15 + 8 + 7 -6 = 24

其次,什麼是等價的重複表示式呢?就是完全相同的表示式,或者是在加法交換率和乘法交換率的作用下,完全等價的表示式。比如:

10 + 12 + 7 - 5 = 24 等價於 10 - 5 +7 + 12 = 24
15 * 8 / (1 + 4) = 24 等價於 15 / (4 + 1) * 8 = 24
(3 + 1) * (2 + 4) = 24 等價於 (1 + 3) * (4 + 2) = 24


2. 演算法

2.1. 求全部解演算法

我採用的演算法是降低維度的演算法,即把多維問題降低到二維來解決。

比如,給定四個數字[1, 2, 3, 4],這是一個四維問題,我們首先要將其轉換為二維問題。具體的辦法是,先將四個數字其中的兩個數字取出,然後將這兩個數字轉化為所能組成的全部表示式。

我們首先取出[1, 2],考慮到加法交換率和乘法交換率的前提下,共有6種可能的不等價表示式,即1+2, 1-2, 1*2, 1/2, 2-1, 2/1,則四維問題就可以轉化為多組三維問題,即['1+2', 3, 4],['1-2', 3, 4],['1*2', 3, 4], ['1/2', 3, 4], ['2-1', 3, 4], ['2/1', 3, 4]。

然後我們窮盡每一種取出兩個數的組合,使用排列組合公式即C(4, 2),所以將四維問題轉化為三維問題共有C(4, 2) * 6 = 36種組合。

下一步是重複這一過程,將三維問題繼續轉化為二維問題,同理,每一個三維問題都可轉化為等價的二維問題,共有C(3, 2) * 6 = 18種組合。

所以,四維問題可轉化為36 * 18 = 648種二維問題,每個二維問題又有6種組合方式,所以,全部的表示式個數為648 * 6 = 3888個。

2.2. 加括號演算法

在每一次二維組合成新表示式的時候,我們根據原有的兩個表示式的各自的運算子號和兩個表示式之間的運算子號的關係來判斷是否需要新增括號。

比如,a、b兩個表示式要組成新的表示式,總共會有如下幾種情況:

  • 如果是a + b,則完全不需要加括號;

  • 如果是a * b或者a / b,若a、b自身的運算子號是加號或減號,則應加括號,如,a = a1 + a2,b為數字,則a * b = (a1 + a2) * b;

  • 如果是a - b,若b為加號或減號,則b應加括號,如,b = b1 - b2,a = a1 + b2,則 a - b = a1 + a2 - (b1 - b2),但值得注意的是,a1 + a2 - (b1 - b2) 其實等價於 a1 + a2 - b1 + b2,這種情況在其他的組合中其實已經存在。因此,可以無需再考慮括號問題;

  • 如果是a / b,若b的符號是乘號或除號,原本理應也要加括號,但其實這種情況與上一種情況類似,我們出於計算簡便考慮,可以不再考慮括號問題。

2.3. 去除等價表示式

對於一個表示式,a + b - c + d 與如下表示式均是等價的:

  • a + d + b - c

  • b + a + d -c

  • b - c + a + d

我們可以在任何一個表示式前再加一個加號,然後使用正規表示式對錶達式進行切割成如下狀態:['+a', '+b', '-c', '+d']。

然後對其進行排序後再組合成字串得到:

  • a + b + d - c

我們將這樣的表示式稱為標準表示式,凡是透過這樣的處理方法得到的標準表示式是相同的,我們均認為是等價表示式,只保留一個標準表示式即可。

乘法交換率也是同樣的轉換方法。


3. 程式碼

演算法講完了,具體的程式碼實現如下:

# coding: utf-8from __future__ import divisionfrom itertools import combinationsimport reclass Solver:

    # 需要達成的目標結果值
    target = 24

    # 四則運算子號定義,其中,a -- b = b - a,a // b = b / a
    ops = ['+', '-', '*', '/', '--', '//']    # precise_mode為精準模式,若開啟,則減號及除號後開啟括號
    def __init__(self, precise_mode=False):
        self.precise_mode = precise_mode    def solution(self, nums):
        result = []
        groups = self.dimensionality_reduction(self.format(nums))        for group in groups:            for op in self.ops:
                exp = self.assemble(group[0], group[1], op)['exp']                if self.check(exp, self.target) and exp not in result:
                    result.append(exp)        return [exp + '=' + str(self.target) for exp in result]    # 對需要處理的數字或表示式組合進行降維,降低到二維
    def dimensionality_reduction(self, nums):
        result = []        # 如果維數大於2,則選出兩個表示式組合成一個,從而降低一個維度,透過遞迴降低到二維
        if len(nums) > 2:            for group in self.group(nums, 2):                for op in self.ops:
                    new_group = [self.assemble(group[0][0], group[0][1], op)] + group[1]
                    result += self.dimensionality_reduction(new_group)        else:
            result = [nums]        return result    # 將兩個表示式組合成一個新表示式
    def assemble(self, exp1, exp2, op):

        # 如果運算子為'--'或者'//',則交換數字順序重新計算
        if op == '--' or op == '//':            return self.assemble(exp2, exp1, op[0])        # 如果是乘法,則根據兩個表示式的情況加括號
        if op in r'*/':
            exp1 = self.add_parenthesis(exp1)
            exp2 = self.add_parenthesis(exp2)        if self.precise_mode:            if op == '-':
                exp2 = self.add_parenthesis(exp2)            elif op == '/':
                exp2 = self.add_parenthesis(exp2, True)

        exp = self.convert(exp1['exp'] + op + exp2['exp'], op)        return {'op': op, 'exp': exp}    # 根據需要為表示式新增相應的括號    @staticmethod
    def add_parenthesis(exp, is_necessary=False):

        # 如果上一計算步驟的運算子號為加號或減號,則需加括號
        if (is_necessary and not exp['exp'].isdigit()) or exp['op'] in r'+-':
            result = {                'exp': '(' + exp['exp'] + ')',                'op': exp['op']
            }        else:
            result = exp        return result    # 檢查表示式是否與結果相等,考慮到中間步驟的除法,因此不採用相等判斷,而是採用計算值和目標值的絕對值是否符合某個精度    @staticmethod
    def check(exp, target, precision=0.0001):
        try:            return abs(eval(exp) - target) < precision        except ZeroDivisionError:            return False

    # 將表示式各項重新排序成為等價標準表示式    @staticmethod
    def convert(exp, op):
        if op in r'+-':
            pattern = r'([+-](((.+)|d+)[*/]((.+)|d+)|d+))'
            exp = '+' + exp        else:
            pattern = r'([*/]((.+?)|d+))'
            exp = '*' + exp
        result = ''.join(sorted([i[0] for i in re.findall(pattern, exp)]))        if len(result) != len(exp):
            result = exp        return result[1:]    # 將輸入的數字格式化為字典,數字的運算子號為空格,注意不是空字元    @staticmethod
    def format(nums):
        return [{'op': ' ', 'exp': str(num)} for num in nums]    # 對錶達式列表進行分組,返回列表,[[[n1, n2], [n3, n4]], [[n1, n3], [n2, n4]], ...]    @staticmethod
    def group(exp_list, counter):

        # 生成以下標號為元素的列表
        index_list = [i for i in range(len(exp_list))]        # 以下標號列表取出不重複的組合
        combination = list(combinations(index_list, counter))        # 使用下標得到原表示式並組成最終的結果陣列
        for group1 in combination:
            group2 = list(set(index_list) - set(group1))            yield [
                [exp_list[g1] for g1 in group1],
                [exp_list[g2] for g2 in group2]
            ]

auto_input = Trueif auto_input:    from numpy import random
    customer_input = random.randint(1, 20, size=4)else:
    customer_input = list()
    customer_input.append(input('請輸入第一個數字:'))
    customer_input.append(input('請輸入第二個數字:'))
    customer_input.append(input('請輸入第三個數字:'))
    customer_input.append(input('請輸入第四個數字:'))

task = Solver()
answer = task.solution(customer_input)if len(answer) == 0:
    print('No solutions')else:    for a in answer:
        print(a)



作者:沒文化的查哥
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4550/viewspace-2816141/,如需轉載,請註明出處,否則將追究法律責任。

相關文章