还记得小时候拿一副牌玩得24点游戏吗?
拿一副牌,抽去大小王后(J,Q,K记为11,12,13;用1代替A),剩下1~13这52张牌。任意抽取4张牌(称为牌组),用加、减、乘、除(可加括号,高级玩家也可用其他运算符号)把牌面上的数算成24。每张牌必须用且只能用一次。如抽出的牌是3、8、8、9,那么算式为(9-8)×8×3=24。
下面就介绍一种求解24点问题的递归思路以及给出其Python求解代码。
递归的思路便是把大的问题变小,每一步将原先要求解的问题分解成与原问题相似的规模较小的问题更小的问题,直至最后达到一个满足退出条件的简单问题,这样再一层层向上返回,便解决了原先的大规模的问题。在代码实现中,程序不断调用自身,每次调用都使问题规模变小,最后达到退出条件一层层返回直至解决原先的问题。
构成递归需具备的条件: 1.子问题须与原始问题为同样的事,且更为简单; 2.不能无限制地调用本身,须有个出口,化简为非递归状况处理。
理解它的一个非常好的例子便是阶乘。n的阶乘(记为fact(n))定义为: n ! = ∏ i = 1 n i = n ∗ ( n − 1 ) ∗ ( n − 2 ) . . . ∗ 1 n!=\prod_{i=1}^{n}i=n*(n-1)*(n-2)...*1 n!=∏i=1ni=n∗(n−1)∗(n−2)...∗1 它可以通过递归方式定义为: 若n=1 , 则fact(n)=1; 若n为大于1的整数,则fact(n)=n*fact(n-1) 这相当于给了一个退出条件(n=1的情况),和一个把问题分解为更小的同类型问题的方法(求n的阶乘变为求n乘上n-1的阶乘)。很容易验证,这样的递归定义和其本身的定义是一致的,而递归定义很容易用通过函数调用自身的程序代码实现。
24点问题也是同理 用四张牌求出24点也许没那么容易,但如果只有两个数,那么我们可以很容易地通过不同的运算符(例:加减乘除)得到两个数组合之后的结果。因此两个数的24点问题是一目了然的。 那么三个数呢?对于三个数a,b,c,我们可以从其中任意挑选两个数(三种挑选方式),例如a,b,之后通过加减乘除计算得到a与b组合后的数d(如果只有四则运算,a与b一定时d有a*b,a+b,a-b,b-a,a/b,b/a几种可能,当然除法首先要求分母不为0),那么d确定了之后问题便变成c与d算24点的两数问题,它们是否能得出24是一目了然的。于是对一个个分支(及所有可能的c和d)进行尝试,便能判断三个数能否以及如何得出24点。 四个数当然也可以以同样的方法变为多个三数问题,进而变成两数问题输出结果。 当然如果不止四个数也可以以同样的方法一步步使问题变小,直至缩小成两数问题达到递归的返回条件。
于是我们的算法求解如下: 1.一个将两个数a,b通过运算组合成新的数的函数f(a,b) 此函数输入两个数a,b,输出它们四则运算的各种对应解及其运算符。 例如输入(a,b),输出a+b的结果及加号,a-b的结果及减号… 2.递归主程序 输入牌组,判断牌组长度,若为两数问题则通过f(a,b)判断能否算出24点并返回;若牌组长度大于2则任选两个数a,b;调用函数f(a,b),将其输出的数存储在一个新的牌组中(此数所对应的运算符即为当前的解决方案分支),新的牌组长度比原先的长度减一,解这个新的牌组的24点问题。
函数的Python实现 1.一个将两个数a,b通过运算组合成新的数的函数f(a,b) 在Python中可以以一个列表的形式很容易地将输出存储起来,即输入(a,b)两个数,f(a,b)返回[(a+b,’+’),(a-b,’-’),…],这里方括号代表列表,括号代表元祖的数据结构,它将运算结果和其对应的运算符放在一起。运算结果对应的运算符用字符(即’+’,’-’,…)进行输出。 2.递归主程序 牌组可用列表来保存,并记录当前列表长度用来判断,用一个全局的数组记录求解过程,即在每一个递归分支中,保存使用过的数,组合成新数的运算符等。
将牌组记为列表A,全局的数组(当前求解过程)记为temp,初始问题大小记为N,当前问题大小记为n,则主程序可以用一张图加以说明:
cal24.py中代码如下:
#交换a与b def swap(a,b): temp=a a=b b=temp return a,b # +:0, -:1, *:2, /:3 #in:two number #out:a tuple with result and its operator def f(a,b): #让a>=b if(a<b): a,b=swap(a,b) res=[] res.append((a*b,'*')) res.append((a+b,'+')) res.append((a-b,'-')) if(b!=0): res.append((a/b,'/')) res.append((b/a,'/')) else: res.append((0,'/')) return res #a:list,element is number #temp:list,the solution,len=N+1 #N:dimension of initial problem #n:len(a) def cal(a,n,temp,N): if(n<=2): res=f(a[0],a[1]) for i in res: #例(c,'+') i[0]对应c,i[1]对应'+' temp[N+1-n]=(a[0],i[1],a[1],'=',24) if(abs(i[0]-24)<=1E-7): temp[0]=True #输出求解过程 print(temp[1:]) break return else: for i in range(0,n): #若前面得到了结果则退出分支 #若注释if,则即使得到结果也会计算其他求解24点的方案 if(temp[0]==True): break for j in range(i+1,n): res=f(a[i],a[j]) for k in res: temp[N+1-n]=(a[i],k[1],a[j],k[0]) #下一步要求解的新数组 new_a=[] for m in range(0,n): if(m!=i and m!=j): new_a.append(a[m]) new_a.append(k[0]) #print(new_a,n,temp[0]) #input() cal(new_a,n-1,temp,N)在Python shell中进行测试:
>>> a=[1,5,5,5] >>> N=4 >>> n=4 >>> temp=N*[0] >>> temp[0]=False >>> cal(a,n,temp,N) [(1, '/', 5, 0.2), (5, '-', 0.2, 4.8), (5, '*', 4.8, '=', 24)]输出结果意思是1,5,5,5四张牌,用1除以5得到0.2,用5-0.2得到4.8,最后用4.8乘以5可以得到24
当然将cal函数的else分支的if判断注释掉之后,可以输出一个牌组的多种求解方案: Python shell中:
>>> a=[5,6,9,8] >>> N=4 >>> n=4 >>> temp=N*[0] >>> temp[0]=False >>> cal(a,n,temp,N) [(5, '-', 9, 4), (8, '-', 4, 4), (6, '*', 4, '=', 24)] [(5, '+', 8, 13), (9, '-', 13, 4), (6, '*', 4, '=', 24)] [(5, '/', 8, 1.6), (6, '+', 9, 15), (1.6, '*', 15, '=', 24)] [(5, '/', 8, 0.625), (6, '+', 9, 15), (0.625, '/', 15, '=', 24)] [(6, '+', 9, 15), (5, '/', 8, 1.6), (15, '*', 1.6, '=', 24)] [(6, '+', 9, 15), (5, '/', 8, 0.625), (15, '/', 0.625, '=', 24)] [(6, '+', 9, 15), (5, '/', 15, 3.0), (8, '*', 3.0, '=', 24)] [(6, '+', 9, 15), (5, '/', 15, 0.3333333333333333), (8, '/', 0.3333333333333333, '=', 24)] [(6, '+', 9, 15), (8, '*', 15, 120), (5, '/', 120, '=', 24)] [(9, '-', 8, 1), (5, '-', 1, 4), (6, '*', 4, '=', 24)]即牌组[5,6,9,8]有多种解决方案。对于其他牌组,大家可以自行验证。
最后讨论一下24点游戏在四则运算下无解的牌组
在cal24.py中定义以下函数:
#使用此函数时将 输出无解的组合 def judge(a): temp=(n+1)*[0] temp[0]=False cal(a,n,temp,N) if(temp[0]==True): return True else: return False def groupOfNoAnswer(): s=0 NoAnsGroup=[] for i in range(1,14): for j in range(i,14): for k in range(j,14): for l in range(k,14): a=[i,j,k,l] if(not judge(a)): NoAnsGroup.append(tuple(a)) s+=1 return s,NoAnsGroup这没什么东西,就是判断是否有解并遍历整个牌组的所有组合。 Python shell中我们输入
>>> s,NoAns=groupOfNoAnswer() >>> s 458 >>> NoAns[1:10] [(1, 1, 1, 2), (1, 1, 1, 3), (1, 1, 1, 4), (1, 1, 1, 5), (1, 1, 1, 6), (1, 1, 1, 7), (1, 1, 1, 9), (1, 1, 1, 10), (1, 1, 2, 2)]即在四则运算下共有458种牌组得不到24点的解(当然引入别的运算符如乘方等也许可以得出解,这只需改动f(a,b)函数添加新的运算符即可)
附B站视频链接: https://www.bilibili.com/video/BV1Xz4y1Z7yg/