N个数计算24点
问题:
N个1到13之间的自然数,找出所有能通过加减乘除计算(每个数有且只能用一次)得到24的组合?
计算24点常用的算法有:① 任取两个数,计算后,将结果放回去,再从剩下的数中任取两个,如此反复直到只剩下一个数;② 先构建前缀/后缀表达式,再计算该表达式;③ 用集合保存中间结果,集合间两两进行合并计算得到新集合(或者对给定的一个集合,对其所有的子集合进行合并计算)。
本文采用第一种方法。定义六种操作符:ADD、SUB、MUL、DIV、RSUB、RDIV,分别对应加、减、乘、除、反减和反除(反减/除:先交换两个数,再减/除)。
显然,取两个数计算时,六种计算结果可能有重复,可以对这6个结果进行去重(实际上,只要分别对加减(ADD、SUB、RSUB)和乘除(MUL、DIV、RDIV)的3个计算结果进行去重判断就可以了,效率和对6个结果去重相差不大)。
另外一种剪枝方法:保存每个数上次计算时所用的操作符(初始值为空)。所取的两个数:
若某个数的上次操作符为减(SUB、RSUB),那么不进行加减(ADD、SUB、RSUB)计算。
若某个数的上次操作符为除(DIV、RDIV),那么不进行乘除(MUL、DIV、RDIV)计算。
比如:取的两个数为 a-b 和 c(c的上次操作符任意),如果进行加减计算的话,
a-b+c 和 c+a-b重复,
c-(a-b)和 c+b-a重复
a-b-c 和 c+b RSUB a重复
也就是说,上次操作符为减的,进行加减计算时,总可以转为某个上次操作符为加的表达式,因而可以不计算。同样,上次操作符为除的,不进行乘除计算。
当然,还可以考虑记录位置进行剪枝,这样避免a+b+c和a+c+b都进行计算。但要注意的是:在给定的组合无解时,越多的剪枝方法,极有可能提高搜索效率,但在给定的组合有解时,很可能反而降低搜索效率。
另外,对有解时输出的表达式的处理对程序的性能影响很大。如果每次计算都保存对应的表达式,会进行大量的字符串操作,严重影响性能。实际上,每次计算只要保存取出的两个数的位置和所进行计算的操作符就够了,最终需要输出表达式时,只要模拟一下递归函数调用过程,进行相应的字符串操作。
下面是程序(gcc 4.5,优化参数-O2)在T4200/2G单核下运行的结果,计算10个数,646646个组合只用了不到13分钟。
N个1到13之间的数的所有组合,计算24点
N | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
时间(ms) | 78 | 610 | 4157 | 19593 | 160922 | 173766 | 764328 |
有解组合数 | 1362 | 6104 | 18554 | 50386 | 125969 | 293930 | 646646 |
总组合 | 1820 | 6188 | 18564 | 50388 | 125970 | 293930 | 646646 |
总表达式 | 1124776 | 15656645 | 105278906 | 492587616 | 3760744504 | 4535030813 | 19912345238 |
表达式A | 457549 | 11864184 | 88679768 | 409129896 | 1173803224 | 4535030813 | 19912345238 |
表达式B | 667227 | 3792461 | 16599138 | 83457720 | 2586941280 | 0 | 0 |
总函数调用 | 1482939 | 20950792 | 141892513 | 669790534 | 5258218577 | 6112529945 | 26948662625 |
函数调用A | 603206 | 15849306 | 119160441 | 551913059 | 1576965280 | 6112529945 | 26948662625 |
函数调用B | 879733 | 5101486 | 22732072 | 117877475 | 3681253297 | 0 | 0 |
其中:表达式A/B、函数调用A/B为:给定的n个数有/无解时,所处理的表达式总数和函数调用总次数。
N=8与N=9,两个所用时间只相差13秒,这是由于N为8时,有一个组合(8个1)无解,判断这个组合无解需110多秒,而计算剩下的125969个组合还不要50秒。
程序代码: