问题提出:
从一副扑克牌里 任意取 4张牌,如何通过 加减乘除,使结果等于24?
任给4个数字 [1,13],通过 + - * / 运算,使得结果 = 24。
解题思路:
把多元运算转化为两元运算,先从四个数中取出两个数进行运算,然后把运算结果和第三个数进行运算,再把结果与第四个数进行运算。
在求表达式的过程中,最难处理的就是对括号的处理,而这种思路很好的避免了对括号的处理。
基于这种思路的一种算法:因为能使用的4种运算符 – * / 都是2元运算符,所以本文中只考虑2元运算符。2元运算符接收两个参数,输出计算结果,输出的结果参与后续的计算。
由上所述,构造所有可能的表达式的算法如下:
(1) 将4个整数放入数组中;
(2) 在数组中取两个数字的排列,共有 P(4,2) 种排列。对每一个排列,
(2.1) 对 – * / 每一个运算符,
(2.1.1) 根据此排列的两个数字和运算符,计算结果
(2.1.2) 改表数组:将此排列的两个数字从数组中去除掉,将 2.1.1 计算的结果放入数组中
(2.1.3) 对新的数组,重复步骤 2
(2.1.4) 恢复数组:将此排列的两个数字加入数组中,将 2.1.1 计算的结果从数组中去除掉 可见这是一个递归过程。步骤 2 就是递归函数。当数组中只剩下一个数字的时候,这就是表达式的最终结果,此时递归结束。
在程序中,一定要注意递归的现场保护和恢复,也就是递归调用之前与之后,现场状态应该保持一致。在上述算法中,递归现场就是指数组,2.1.2 改变数组以进行下一层递归调用,2.1.3 则恢复数组,以确保当前递归调用获得下一个正确的排列。
括号 () 的作用只是改变运算符的优先级,也就是运算符的计算顺序。所以在以上算法中,无需考虑括号。括号只是在输出时需加以考虑。
C++代码:
/* linolzhang 2009.11
point24
*/
#include <iostream>
#include <stdio.h>
#include <string>
#include <cmath>
using namespace std;
double number[4];
string expression[4];
bool bReady = false;
int count = 0; // solution count
void Search(int n)
{
if(n == 1)
{
if( fabs(number[0] - 24) <= 1E-6 )
{
cout << expression[0] << "\t\t";
bReady = true;
count++;
if((count % 3)==0)
printf("\n");
}
}
for(int i=0; i<n; i++)
{
for(int j=i+1; j<n; j++)
{
double a, b;
string expressiona, expressionb;
a = number[i];
b = number[j];
number[j] = number[n - 1];
expressiona = expression[i];
expressionb = expression[j];
expression[j] = expression[n - 1];
expression[i] = '('+ expressiona + '+' + expressionb + ')';
number[i] = a + b;
Search(n-1);
expression[i] = '('+ expressiona+ '-' + expressionb + ')';
number[i] = a - b;
Search(n-1);
expression[i] = '('+expressionb + '-' + expressiona + ')';
number[i] = b - a;
Search(n-1);
expression[i] = '('+ expressiona +'*'+ expressionb+ ')';
number[i] = a * b;
Search(n-1);
if(b != 0)
{
expression[i] = '('+expressiona+'/' + expressionb + ')';
number[i] = a / b;
Search(n-1);
}
if(a != 0)
{
expression[i] = '('+expressionb + '/'+ expressiona + ')';
number[i] = b / a;
Search(n-1);
}
number[i] = a;
number[j] = b;
expression[i] = expressiona;
expression[j] = expressionb;
}
}
}
int main()
{
printf("Please give 4 input:\n");
scanf("%lf %lf %lf %lf", &number[0],&number[1],&number[2],&number[3]);
char buff[20];
for(int i=0; i<4; i++)
{
itoa( (int)number[i], buff, 10 );
expression[i] = buff;
}
printf("\n");
Search(4);
if(bReady)
printf("\nSuccess.\nThe sum of the ways = %d \n",count);
else
printf("No solution!");
printf("Press Any Key To Continue...\n");
getchar();
return 0;
}
采用逆波兰式 是一个更好的解决思路,逆波兰式不需要考虑括号的问题,这种穷举方式直接包含了运算顺序,来看一段 Python代码(来源自网上,没找到准确出处):
# --rpn.py--
from itertools import permutations # 全排列
from fractions import Fraction # 分数运算
from operator omport add, sub, mul, div # 函数+ - × /
card = map(str, [5, 5, 5, 1]) # 输入4张牌存为字符
dict_op = {'+': add, '-':sub, '*':mul, '/':div} # 将运算符转为调用函数
op = ['+', '-', '*', '/'] * 3 # 四则运算,4张牌需要3次运算
op1 = list(permutations(op, 3)) # op中任取三个做全排列,结果构成列表
op2 = map(lambda x: list(x), op1) # 列表元素从元组转为列表
op3 = map(lambda x: sorted(x), op2) # 排序
ops = set(map(lambda x: tuple(x), op3)) # 列表转为集合,去重
# 逆波兰表达式实现
def rpn(lst):
stack = [] # 初始化栈
for i in lst: # 遍历传入表达式列表
if i.isdigit(): # 若为数字,则直接进栈
stack.append(Fraction(i.strip()))
else:
if i in dict_op: # 若为运算符,则取出两元素作运算
try:
a = stack.pop()
b = stack.pop()
stack.append(dict_op(b, a))
except: break
if len(stack) == 1: # 若最后栈内剩余一个元素,接将结果输出
return stack[0]
# 计算24
for operator in ops: # 遍历每一种运算符组合
space = []
space = card + list(operator) # 组合卡牌和运算符
s = set(permutations(space)) # 排列卡牌运算符所有可能
for j in s:
if rpn(list(j)) == 24:
print j
print 'Game over'