一,概述
二十四點是一種益智遊戲,它能在遊戲中鍛鍊人們的心算,它往往要求人們將四個數字進行加減乘除(允許使用括號)求得二十四。然後將四個數字的計算公式表示出來。
二,中綴表達式求解
最直接的方法就是採用窮舉法,遊戲中可用的運算符只有四種,四個數字每個只能使用一次。
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;
- }