一,概述
二十四点是一种益智游戏,它能在游戏中锻炼人们的心算,它往往要求人们将四个数字进行加减乘除(允许使用括号)求得二十四。然后将四个数字的计算公式表示出来。
二,中缀表达式求解
最直接的方法就是采用穷举法,游戏中可用的运算符只有四种,四个数字每个只能使用一次。
1)不考虑括号情况
4个数全排列:4!=24种
需要三个运算符,且运算符可以重复:4*4*4=64
总计:1536
2)考虑括号(是个难点)
自己想的加括号:四个数有五种加括号方式: (AB)CD 、AB(CD)、A(BC)D 、A((BC)D) 、(AB)(CD)、A(B(CD))
错误点:这里添加括号的时候,需要把四个数都看成相乘。需要加两个括号来列举比较直观
AB(CD) = (AB)(CD)
改正后:((AB)C)D 、(AB)(CD)、 (A(BC))D 、A((BC)D) 、A(B(CD))
四个运算数五种不同加括号方式的由来。这是一个经典的Catalan数问题。
这个经典Catalan数问题在组合数学教材上都能找到。原题目是:n 个数相乘, 不改变它们的位置, 只用括号表示不同的相乘顺序,令g(n)表示这种条件下构成不同乘积的方法数,令C(n)表示第n个Catalan数。则有g(n)=C(n-1)。前几个Catalan数为:C(0)=1,C(1)=1,C(2)=2,C(3)=5,C(4)=14,C(5)=42。所以g(4)=C(3)=5。
根据Catalan数的计算公式,有g(4)=g(1)g(3)+g(2)g(2)+g(3)g(1)。
Catalan数的计算公式也同时提供了构造答案的方法。对于4个数,中间有3个位置,可以在任何一个位置一分为二,被分开的两半各自的加括号方案再拼凑起来就得到一种4个数的加括号方案:
一个数时:(A),一种
两个数:g(2)=g(1)g(1),所以是(A)(B)=(AB),一种
三个数:g(3)=g(1)g(2)+g(2)g(1)=(A)(BC)+(AB)(C),两种
四个数:g(4)=g(1)g(3)+g(2)g(2)+g(3)g(1)
=(A)[(B)(CD)+(BC)(D)]+(AB)(CD)+[(A)(BC)+(AB)(C)](D)
=A(B(CD)) + A((BC)D) + (AB)(CD) + (A(BC))D + ((AB)C)D
共有五种。于是写代码枚举这五种加括号的方式即可。这种方法只是一种能得到正确答案的方法,扩展性和效率都极差。而且生成的表达式中也有冗余括号。
#include <iostream> #include <cmath> using namespace std; const double Threshold = 1E-6; const int CardsNumber = 4; const int ResultValue = 24; double number[CardsNumber]; string result[CardsNumber]; bool PointsGame(int n) { if(n == 1) { // 由于浮点数运算会有精度误差,所以用一个很小的数1E-6来做容差值 // 本书2.6节中讨论了如何将浮点数转化为分数的问题 if(fabs(number[0] – ResultValue) < Threshold)//结果等于24 { cout << result[0] << endl;//输出表达式 return true; } else { return false; } } for(int i = 0; i < n; i++)//第一个数(计算时被两个数结果替换) { for(int j = i + 1; j < n; j++)//第二个数(计算时候被最后一个数替换) { double a, b;//存放计算的数 string expa, expb;//存放表达式中两个数 a = number[i]; b = number[j]; number[j] = number[n – 1];//去除第二个数 expa = result[i]; expb = result[j]; result[j] = result[n – 1];//表达式去除 result[i] = ‘(‘ + expa + ‘+’ + expb + ‘)’; number[i] = a + b;//去除第一个数 if(PointsGame(n – 1)) return true; result[i] = ‘(‘ + expa + ‘-‘ + expb + ‘)’; number[i] = a – b; if(PointsGame(n – 1)) return true; result[i] = ‘(‘ + expb + ‘-‘ + expa + ‘)’; number[i] = b – a; if(PointsGame(n – 1)) return true; result[i] = ‘(‘ + expa + ‘*’ + expb + ‘)’; number[i] = a * b; if(PointsGame(n – 1)) return true; if(b != 0) { result[i] = ‘(‘ + expa + ‘/’ + expb + ‘)’; number[i] = a / b; if(PointsGame(n – 1)) return true; } if(a != 0) { result[i] = ‘(‘ + expb + ‘/’ + expa + ‘)’; number[i] = b / a; if(PointsGame(n – 1)) return true; } number[i] = a;//将本次循环的结果消除,继续测试下一对数 number[j] = b; result[i] = expa; result[j] = expb; } } return false; } int main() { int x; for(int i = 0; i < CardsNumber; i++) { char buffer[20]; cout << “the ” << i << “th number:”; cin >> x; number[i] = x; itoa(x, buffer, 10); result[i] = buffer; } if(PointsGame(CardsNumber)) { cout << “Success.” << endl; } else { cout << “Fail.” << endl; } }
三,分支限界法求解
#include <iostream> #include <set> #include <string> #include <cmath> using namespace std; #define N 4 // 4张牌,可变 #define RES 24 // 运算结果为24,可变 #define EPS 1e-6 struct Elem { Elem(double r, string& i):res(r),info(i){} Elem(double r, char* i):res(r),info(i){} double res; // 运算出的数据 string info; // 运算的过程 bool operator<(const Elem& e) const { return res < e.res; // 在set的红黑树插入操作中需要用到比较操作 } }; int A[N]; // 记录N个数据 // 用二进制数来表示集合和子集的概念,0110表示集合包含第2,3个数 set<Elem> vset[1<<N]; // 包含4个元素的集合共有16个子集0-15 set<Elem>& Fork(int m) { // memo递归 if (vset[m].size()) { return vset[m]; } for (int i=1; i<=m/2; i++) if ((i&m) == i) { set<Elem>& s1 = Fork(i); set<Elem>& s2 = Fork(m-i); set<Elem>::iterator cit1; set<Elem>::iterator cit2; // 得到两个子集合的笛卡尔积,并对结果集合的元素对进行6种运算 for (cit1=s1.begin(); cit1!=s1.end(); cit1++) for (cit2=s2.begin(); cit2!=s2.end(); cit2++) { string str; str = “(“+cit1->info+”+”+cit2->info+”)”; vset[m].insert(Elem(cit1->res+cit2->res,str)); str = “(“+cit1->info+”-“+cit2->info+”)”; vset[m].insert(Elem(cit1->res-cit2->res,str)); str = “(“+cit2->info+”-“+cit1->info+”)”;; vset[m].insert(Elem(cit2->res-cit1->res,str)); str = “(“+cit1->info+”*”+cit2->info+”)”; vset[m].insert(Elem(cit1->res*cit2->res,str)); if (abs(cit2->res)>EPS) { str = “(“+cit1->info+”/”+cit2->info+”)”; vset[m].insert(Elem(cit1->res/cit2->res,str)); } if (abs(cit1->res)>EPS) { str = “(“+cit2->info+”/”+cit1->info+”)”; vset[m].insert(Elem(cit2->res/cit1->res,str)); } } } return vset[m]; } int main() { int i; for (i=0; i<N; i++) cin >> A[i]; // 递归的结束条件 for (i=0; i<N; i++) { char str[10]; sprintf(str,”%d”,A[i]); vset[1<<i].insert(Elem(A[i],str)); } Fork((1<<N)-1);//开始1111 表示四个数 // 显示算出24点的运算过程 set<Elem>::iterator it; for (it=vset[(1<<N)-1].begin(); it!=vset[(1<<N)-1].end(); it++) { if (abs(it->res-RES) < EPS) cout << it->info << endl; } }