百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

Python实现24点游戏求解(完美解决括号问题+大幅优化运算效率)

bigegpt 2024-08-31 16:44 2 浏览

目标要求

对于任意给定的四张扑克牌,计算是否有赢得24点游戏的方法(即使用加、减、乘、除四则运算凑成24的方法);如果有的话,列出所有可能的方法。

【24点游戏规则】

在大小王以外的52张牌中,任意抽取其中4张牌。如果通过加、减、乘、除四则运算(可加括号)的方法,将抽到的4张牌算成24,则为胜利;每张牌都必须使用,且只能使用一次。

第一种解法

依据游戏规则,我们可以想到如下解决思路:使用枚举的方法,将所有的计算方法都枚举出来,将四张扑克牌的数字代入到所有的计算方法中得出结果,如果结果为24则为解。

由此,我们得到了第一种解法。在具体实现中:将所有可能的四则运算组合和所有可能的括号组合合并在一起,由此生成所有可能的算式组合。计算某一个牌组时,先计算所有该牌组所有可能的组合方式,并将所有的组合方式带入所有可能的算式组合求解。

 import itertools
 ?
 class CardGaming:
     def __init__(self):
         self.formula_list = list()  # 存储所有可能的算式
         for marks in itertools.product(["+", "-", "*", "/"], repeat=3):
             for bracket in ["{0}%s{1}%s{2}%s{3}", "({0}%s{1})%s{2}%s{3}", "({0}%s{1}%s{2})%s{3}",
                             "{0}%s({1}%s{2})%s{3}",
                             "{0}%s({1}%s{2}%s{3})", "({0}%s{1})%s({2}%s{3})", "{0}%s{1}%s({2}%s{3})"]:
                 self.formula_list.append((bracket % marks))
 ?
     def solve(self, card_probability):
         answer = []
         for card_order in set(itertools.permutations(card_probability, 4)):  # 遍历所有可能的不同卡牌顺序(最多24种可能)
             for formula in self.formula_list:  # 遍历所有可能的算式(448种可能)
                 final_formula = formula.format(*card_order)
                 try:
                     if round(eval(final_formula), 3) == 24:
                         answer.append(final_formula)
                 except ZeroDivisionError:
                     continue
         return answer
 ?
 if __name__ == "__main__":
     print(CardGaming().solve((3, 3, 8, 8)))  # 输出: 8/(3-8/3)

当前代码在计算每一个牌组的答案时,都需要遍历?4^3*7=448种算式和最多?A44=24种卡牌顺序,即处理最多448*24=10752?种可能性。使用这个解法计算所有可能的扑克牌组合(共计?13^4=28561种解法),需要1906秒(I7 7700,8GB)。

第二种解法

在第一种解法中,计算每一个牌组的答案时,处理的可能性中有很多重复的情况,例如“A+B-C+D”、“D-C+B+A”、“D+A-C+B“等。这就极大地拖累了我们的运算速度。但是,要在第一种解法的基础上来合并这些不同的情况,需要同时考虑符号、括号和卡牌顺序,十分复杂。

因此,我们可以从另外一个角度来解决这个问题。

通过观察我们可以发现,无论什么算式,本质上都是按着一定的顺序,对4张扑克牌的数值进行三次运算;而每一次运算,都是从尚未用过的扑克牌以及之前的运算结果中选择2个进行运算。所以,我们可以将所有算式归纳为:

从4张牌中任意抽取2个进行任意运算,将未抽取的2张牌和运算结果组合成包含3个数值的新列表;在新列表中任意抽取2个进行任意运算,将未抽取的1张牌和运算结果组成包含2个数值的新列表;对新列表中的2个数值进行任意运算得出结果,如果结果为24则为解。

由此,我们得到了第二种算法。在具体实现中,我们主要注意如下几点:

  • 因为不再枚举算式,所以我们也不再需要使用低效的eval()函数运行算式。
  • 因为如果在运算过程中生成算式,会增加很多运算量,所以我们只在求出解后反向生成解的算式(哪怕这样生成算式会更困难,但是需要生成的次数大大减少)。
 def solve(card_probability):
     card_probability = list(card_probability)  # 生成临时列表
     answer = []
     for combine_1 in set(itertools.combinations(card_probability, 2)):  # 在临时列表的4个数中任意抽取2个数
         for answer_1 in all_maybe_count_answer(combine_1[0], combine_1[1]):
             card_list_1 = copy.deepcopy(card_probability)
             card_list_1.remove(combine_1[0])  # 从临时列表移除抽到的数1
             card_list_1.remove(combine_1[1])  # 从临时列表移除抽到的数2
             card_list_1.append(answer_1)  # 添加计算结果到临时列表
             for combine_2 in set(itertools.combinations(card_list_1, 2)):  # 在临时列表的3个数中任意抽取2个数
                 for answer_2 in all_maybe_count_answer(combine_2[0], combine_2[1]):
                     card_list_2 = copy.deepcopy(card_list_1)
                     card_list_2.remove(combine_2[0])  # 从临时列表移除抽到的数1
                     card_list_2.remove(combine_2[1])  # 从临时列表移除抽到的数2
                     card_list_2.append(answer_2)  # 添加计算结果到临时列表
                     for combine_3 in set(itertools.combinations(card_list_2, 2)):  # 抽取临时列表剩下的2个数
                         for answer_3 in all_maybe_count_answer(combine_3[0], combine_3[1]):
                             if round(answer_3, 3) == 24:
                                 answer.append(total_formula(card_probability, combine_1, answer_1, combine_2,
                                                             answer_2, combine_3, answer_3))  # 生成完整算式
     return answer
 ?
 ?
 if __name__ == "__main__":
     start_time = time.time()
     for cards in list(itertools.product([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13], repeat=4)):
         solve(cards)
     print("计算时间:", time.time() - start_time)

(其中all_maybe_count_answer函数计算两个参数进行四则运算的所有可能结果;total_formula函数依据中间变量生成完整计算公式)

运行结果:

 计算时间: 136.27595901489258

这种接法在第一次四则运算时,有种C24=6抽取结果,有6种运算结果(减法和除法因顺序不同有2个结果);在第二次四则运算时,有种C23=3抽取结果,有6种运算结果;在第三次四则运算时,有C22=1种抽取结果,有6种运算结果。

因此,这种算法在求一个扑克牌组的解时,仅需要考虑??C24*6*C23*6*C22*6=3888种可能性。使用这个解法计算所有可能的扑克牌组合,需要136秒(I7 7700,8GB),比第一种解法快了10倍以上。

完整源代码详见扩展链接

相关推荐

Java 泛型大揭秘:类型参数、通配符与最佳实践

引言在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java5引入了一种名为泛型(Generics)的强大功能。本文将详细介绍Java泛型的概念、优势和局限性,以及如何在...

K8s 的标签与选择器:流畅运维的秘诀

在Kubernetes的世界里,**标签(Label)和选择器(Selector)**并不是最炫酷的技术,但却是贯穿整个集群管理与运维流程的核心机制。正是它们让复杂的资源调度、查询、自动化运维变得...

哈希Hash算法:原理、应用(哈希算法 知乎)

原作者:Linux教程,原文地址:「链接」什么是哈希算法?哈希算法(HashAlgorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被...

C#学习:基于LLM的简历评估程序(c# 简历)

前言在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子,感觉还挺好玩的,为了练习一下C#,我最近使用C#重写了一个。准备不同的简历:image-20250528183949844查...

55顺位,砍41+14+3!季后赛也成得分王,难道他也是一名球星?

雷霆队最不可思议的新星:一个55号秀的疯狂逆袭!你是不是也觉得NBA最底层的55号秀,就只能当饮水机管理员?今年的55号秀阿龙·威金斯恐怕要打破你的认知了!常规赛阶段,这位二轮秀就像开了窍的天才,直接...

5分钟读懂C#字典对象(c# 字典获取值)

什么是字典对象在C#中,使用Dictionary类来管理由键值对组成的集合,这类集合被称为字典。字典最大的特点就是能够根据键来快速查找集合中的值,其键的定义不能重复,具有唯一性,相当于数组索引值,字典...

c#窗体传值(c# 跨窗体传递数据)

在WinForm编程中我们经常需要进行俩个窗体间的传值。下面我给出了两种方法,来实现传值一、在输入数据的界面中定义一个属性,供接受数据的窗体使用1、子窗体usingSystem;usingSyst...

C#入门篇章—委托(c#委托的理解)

C#委托1.委托的定义和使用委托的作用:如果要把方法作为函数来进行传递的话,就要用到委托。委托是一个类型,这个类型可以赋值一个方法的引用。C#的委托通过delegate关键字来声明。声明委托的...

C#.NET in、out、ref详解(c#.net framework)

简介在C#中,in、ref和out是用于修改方法参数传递方式的关键字,它们决定了参数是按值传递还是按引用传递,以及参数是否必须在传递前初始化。基本语义对比修饰符传递方式可读写性必须初始化调用...

C#广义表(广义表headtail)

在C#中,广义表(GeneralizedList)是一种特殊的数据结构,它是线性表的推广。广义表可以包含单个元素(称为原子),也可以包含另一个广义表(称为子表)。以下是一个简单的C#广义表示例代...

「C#.NET 拾遗补漏」04:你必须知道的反射

阅读本文大概需要3分钟。通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员Type类的GetMembe...

C#启动外部程序的问题(c#怎么启动)

IT&OT的深度融合是智能制造的基石。本公众号将聚焦于PLC编程与上位机开发。除理论知识外,也会结合我们团队在开发过程中遇到的具体问题介绍一些项目经验。在使用C#开发上位机时,有时会需要启动外部的一些...

全网最狠C#面试拷问:这20道题没答出来,别说你懂.NET!

在竞争激烈的C#开发岗位求职过程中,面试是必经的一道关卡。而一场高质量的面试,不仅能筛选出真正掌握C#和.NET技术精髓的人才,也能让求职者对自身技术水平有更清晰的认知。今天,就为大家精心准备了20道...

C#匿名方法(c#匿名方法与匿名类)

C#中的匿名方法是一种没有名称只有主体的方法,它提供了一种传递代码块作为委托参数的技术。以下是关于C#匿名方法的一些重要特点和用法:特点省略参数列表:使用匿名方法可省略参数列表,这意味着匿名方法...

C# Windows窗体(.Net Framework)知识总结

Windows窗体可大致分为Form窗体和MDI窗体,Form窗体没什么好细说的,知识点总结都在思维导图里面了,下文将围绕MDI窗体来讲述。MDI(MultipleDocumentInterfac...