Python实现24点游戏(完美解决去重和括号问题)

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-8

from __future__ import division
from itertools import combinations
import re


class 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 = True
if 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)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容