by Trucker Leonode
[說明:此文改編自我寫的一篇解題報告,原題是某年國家集訓隊組隊賽題目] 問題描述 80年代全世界流行一種數字遊戲,在中國咱們把這種遊戲稱爲「24點」。如今咱們 把這個有趣的遊戲推廣一下:您做爲遊戲者將獲得6個不一樣的天然數做爲操做數, 以及另一個天然數做爲理想目標數,而您的任務是對這6個操做數進行適當的算 術運算,要求運算結果小於或等於理想目標數,而且咱們但願所得結果是最優的, 即結果要最接近理想目標數。 您可使用的運算只有:+,-,*,/,您還可使用()來改變運算順序。注意: 全部的中間結果必須是整數,因此一些除法運算是不容許的(例如,(2*2)/4是 合法的,2*(2/4)是不合法的) 下面咱們給出一個遊戲的具體例子: 若給出的6個操做數是:1,2,3,4,7和25,理想目標數是573; 則最優結果是573:(((4*25-1)*2)-7)*3。 輸入: 輸入文件名爲game.in。輸入文件僅一行,包含7個整數,前6個整數Mi, 1<=Mi<=100,表示操做數,最後一個整數T, 1<=T<=1000,表示理想目標數。 輸出: 輸出文件名爲game.out。輸出文件有兩行,第一行僅一個整數,表示您的程序計算 獲得的最優結果;第二行是一個表達式,即您獲得的最優結果的運算方案。 輸入輸出示例: 輸入文件 1 2 3 4 7 25 573 輸出文件 573 ((4*25-1)*2)-7)*3 算法分析 首先咱們要對這個問題進行數學抽象。 定義1:對於有理數組成的多重集合S , f(S) 定義以下: 若是 S 是空集或只包含一個元素,則 f(S)=S ;不然 f(S)=∪ f( ( S-{r1, r2}) ∪ {r} ) ,對於每個 r=r1+r2 , r1-r2 , r1×r2 ,r1÷r2(r2≠0),且r1, r2取遍 S 中全部元素的組成的二元組。 定義1說明:要計算集合S中的元素經過四則混合運算所能獲得的全部值,咱們只需 要任取 S 中的兩個元素 r1 , r2 ,分別計算 r1 , r2 的加減乘除運算,而後用 所得的結果與 S 中剩下的其餘數字進行四則混合運算。只要取遍全部的 r1 , r2 ,最後獲得的全部結果的並集就是 S 中的元素經過四則混合運算所能獲得的所 有值的集合。 根據上述定義,在本問題中,集合 S 就是由輸入中給定的6個正整數組成的集合, 題目所求就是找出 f(S) 中小於或等於目標數的最大數。 定義2:給定兩個多重集合 S1 , S2,定義 comb( S1, S2 ) = ∪ { r1+r2 , r1-r2, r1×r2, r1÷r2(r2≠0) } (1.1) 其中 ( r1 , r2 ) ∈ S1 × S2。 定義2實際上定義了兩個集合中的元素兩兩進行加減乘除運算所能獲得的結果集合 。 定理1:對於有理數組成的多重集合 S ,若是 S 至少有兩個元素,則 f(S)=∪ comb( f(S1), f(S - S1) ) (1.2) 其中 S1 取遍 S 的全部非空真子集。 定理1的含義是:要計算 S 中的元素經過四則混合運算所能獲得的全部值,能夠先 將 S 分解爲兩個子集 S1 和 S- S1 ,分別計算 S1 和 S-S1 中的元素進行四則混 合運算所能獲得的結果集合,即 f(S1) 和 f(S-S1) ,而後對這兩個集合中的元素 進行加減乘除運算,即 comb( f(S1), f(S-S1) ) ,最後獲得的全部集合的並集就 是 f(S) 。限於篇幅,定理1的正確性易用數學概括法證實。 定義1和定理1實際上分別給出了計算f(S)的兩種不一樣的方法。根據定義1,能夠遞 歸地計算f(S) ,其算法僞代碼以下: 算法1 function f(S) begin 1. if |S| < 2 2. then return S 3. else begin 4. T ← Φ 5. for each (r1, r2) in S do 6. begin 7. r ← r1 + r2; 8. T ← T + f(S – {r1, r2} + {r}); 9. r ← r1 - r2; 10. T ← T + f(S – {r1, r2} + {r}); 11. r ← r1 * r2; 12. T ← T + f(S – {r1, r2} + {r}); 13. if (r2 <> 0) and (r1 mod r2 = 0) then 14. begin 15. r ← r1 / r2; 16. T ← T + f(S – {r1, r2} + {r}); 17. end 18. end 19. return T; 20. end end 上述僞代碼中使用了+, - 來分別表示集合的並和差運算。算法1每次選擇兩個數字 進行某種運算,而後將結果與剩下的數字遞歸地進行運算,最後求得全部數字進行 四則混合運算的結果。固然,在具體實現該算法的過程當中有不少能夠優化的地方, 好比根據加法交換律, a+b+c=a+c+b ,所以咱們能夠規定:若是上一層遞歸做了 加法運算,這一層僅當知足當前的操做數大於上一層的兩個操做數的時候才進行加 法運算,以確保 a+b+c 這樣的式子中的操做數老是從小到大排列,這樣就能夠避 免重複進行等價的加法計算。相似地咱們能夠對乘法也做此規定。在進行減法的時 候,咱們能夠規定只能計算大數減少數,由於最後所需計算獲得的目標數是一個正 數,若是計算過程當中出現負數,確定有另一個較大的正數與其做加法或者有另外 一個負數與其作乘除法以消除負號。所以咱們總能夠調整運算次序使得四則混合運 算的每一步的中間結果都是正數。在做除法的時候,由於題目規定中間結果只能是 整數,因此也只須要用大數除小數,且僅當能除盡的時候才進行除法。對於本題而 言,初始的集合 S 中一共有6個操做數,每次遞歸均可以合併兩個操做數,因此遞 歸到第5層的時候集合 S 中只剩下一個數,這個數就是原先的6個操做數進行四則 混合運算所能獲得的結果。本題只要求最接近目標值的結果,因此實現上述算法的 時候能夠只記錄當前最優的結果。對於本題也能夠利用遞歸回溯構造出全部的四則 混合運算的語法樹,但本質上與算法1是沒有區別的。 定理1則給出了另外一種計算f(S)的方法。咱們固然也能夠根據(1.2)式直接地遞歸計 算f(S),但那樣的話會有不少冗餘計算。例如對於S={1,2,3,4}, f(S) = comb( f({ 1 }), f({ 2,3,4}) )∪ ... ∪ comb( f({ 1,2 }), f({ 3,4 }) ) ∪ ...; 計算f(S)的時候須要計算 f({ 2,3,4 })和f({ 3,4 }) ,又由於 f({2,3,4}) = comb(f({ 2 }), f({3,4})) ∪ ...; 在計算 f({ 2,3,4}) 的時候又要重複地計算 f({ 3,4 }) ,這就產生了冗餘的計 算。這種狀況下直接地遞歸就不適用。必須按照必定的順序,遞推地進行計算。這 種將遞歸改成遞推,以解決冗餘的算法設計策略,就叫作動態規劃。 下面咱們具體闡述一下該算法的步驟。設初始時集合 S 中的 n 個數字分別爲 x[0], x[1],...,x[n-1] ,咱們能夠用一個二進制數k來表示S 的子集 S[k] , x[i] ∈ S[k] 當且僅當二進制數k的第i位爲1。因而咱們用一個數組 F[0..2^n-1] 就能夠保存函數f對於S的全部子集的函數值(注意,函數f的函數值是一個集合) ,且 F[2^n-1]=f(S) 就是所求。 算法2 1. for i ← 0 to 2^n-1 2. do F[i]←Φ; 3. for i ← 0 to n-1 4. do F[2^i]← {x[i]}; 5. for x ← 1 to 2^n-1 do 6. begin 7. for i ← 1to x-1 do 8. begin 9. if x∧i=i then 10. begin 11. j ← x – i; 12. if i < j 13. then F[x] ← F[x] + comp(F[i],F[j]); 14. end; 15. end; 16. end; 17. return F[ 2 n ?1] ; 上述僞代碼中使用了+表示集合的並運算。算法2的第1~2行將F中全部的集合初始 化爲空;第3~4行中 2^i 即表示只包含元素 x[i]的子集(由於 2^i 只有第 i 位 上是1),根據定義1咱們知道當集合中只有一個元素的時候函數 f 的函數值就是 那惟一的元素組成的集合,因此3~4行計算出了函數 f 對於全部只有一個元素的 子集的函數值;第5~17行按照必定的順序計算函數 f 對於 S 的全部子集的函數 值。對於 S 的兩個子集 S[i] 和 S[x] , S[i]真包含於S[x]的充要條件是 x∧ i=i ,這裏 ∧ 是按位進行與操做,而 x∧i=i 的必要條件是 i<x 。於是第7~15 行的循環將S[x]拆成兩個子集S[i]和S[j],並在第13行根據(1.2)式計算全部的 comp( f(S[i]),f(S[j]) ) 的並。第12行的判斷語句是爲了優化算法的效率,由於 將 S[x]拆成兩個子集 S[i]和 S[j]的過程是對稱的,因此咱們對於 comp( f(S[i]),f(S[j]) ) 和 comp( f(S[j]),f(S[i]) ) 二者只取一個進行計算。下面 是函數comp的僞代碼: 算法3 function comp(S1, S2) 1. T ← Φ ; 2. for each x in S1 do 3. begin 4. for each y in S2 do 5. begin 6. T ← T + {(x + y)}; 7. T ← T + {(x * y)}; 8. if x > y then 9. begin 10. T ← T + {(x – y)}; 11. if (y <> 0) and (x mod y = 0) 12. then T ← T + {(x / y)}; 13. end 14. else begin 15. T ← T + {(y – x)}; 16. if (x <> 0) and (y mod x = 0) 17. then T ← T + {(y / x)}; 18. end; 19. end; 20. end; 21. return T; comp在進行計算的時候不考慮參數集合S1和S2的順序,進行減法的時候始終用大 數減少數,這樣保證運算過程當中不出現負數(這樣作的理由前文已經闡明)。 由於咱們只關心最後的f(S)中最接近目標值的數字,而且題目只要求求出任何一組 最優解,因此算法2中的集合不須要是多重集合,只要是通常的集合便可。換句話 說,集合F[i]中全部的元素互不相同,重複出現元素的咱們只保留其中一個。這樣 能夠大大減小計算中的冗餘。作了這樣的處理後,算法2的效率至少不會比算法1差 ,由於算法1中所能採用的主要剪枝手段是排除等價的表達式,但由於等價的兩個 表達式計算出的結果也必定相同,而算法2排除了全部結果相同的表達式,因此算 法2的效率至少不會比算法1差,算法2中所進行的計算基本上都是獲得最優解所必 需的計算。 在實現算法2的過程當中,集合能夠用一個鏈表加上一個哈希表來實現。鏈表中保存 每一個表達式及其值,哈希表用來記錄該集合中是否存在某個特定值的表達式。當向 集合中插入一個新的表達式的時候,首先檢查哈希表,看看該集合是否已經有和新 表達式值相同的表達式,若是有的話就不插入,不然將新的表達式追加到鏈表末尾 。採用這種數據結構,能夠在常數時間內完成集合的插入和刪除操做。利用鏈表, 集合的並操做也很容易高效地實現。 在實現算法2的過程當中,能夠沒必要保存表達式的字符串,只須要記錄下當前的值是 由哪兩個集合中的元素經過哪一種運算獲得的,最後再根據最優解遞歸地計算出最優 解的表達式。這樣只在最後構造最優解的表達式時才進行字符串操做,程序運行效 率能提升7~8倍左右。另外,在comb函數中進行乘法運算的時候要注意考慮運算結 果超出整數範圍的狀況。 通過以上優化,利用算法2實現的程序對於100個隨機生成的測試數據總共只須要5 秒左右就能夠出解,平均每一個數據只須要50毫秒便可出解(測試用的CPU爲賽揚 1GB)。這樣的效率已經很是使人滿意了。 附錄: 1。根據算法1計算24點的代碼 #include <iostream> #include <string> #include <cmath> using namespace std; const double PRECISION = 1E-6; const int COUNT_OF_NUMBER = 4; const int NUMBER_TO_CAL = 24; double number[COUNT_OF_NUMBER]; string expression[COUNT_OF_NUMBER]; bool Search(int n) { if (n == 1) { if ( fabs(number[0] - NUMBER_TO_CAL) < PRECISION ) { cout << expression[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 = expression[i]; expb = expression[j]; expression[j] = expression[n - 1]; expression[i] = '(' + expa + '+' + expb + ')'; number[i] = a + b; if ( Search(n - 1) ) return true; expression[i] = '(' + expa + '-' + expb + ')'; number[i] = a - b; if ( Search(n - 1) ) return true; expression[i] = '(' + expb + '-' + expa + ')'; number[i] = b - a; if ( Search(n - 1) ) return true; expression[i] = '(' + expa + '*' + expb + ')'; number[i] = a * b; if ( Search(n - 1) ) return true; if (b != 0) { expression[i] = '(' + expa + '/' + expb + ')'; number[i] = a / b; if ( Search(n - 1) ) return true; } if (a != 0) { expression[i] = '(' + expb + '/' + expa + ')'; number[i] = b / a; if ( Search(n - 1) ) return true; } number[i] = a; number[j] = b; expression[i] = expa; expression[j] = expb; } } return false; } void main() { for (int i = 0; i < COUNT_OF_NUMBER; i++) { char buffer[20]; int x; cin >> x; number[i] = x; itoa(x, buffer, 10); expression[i] = buffer; } if ( Search(COUNT_OF_NUMBER) ) { cout << "Success." << endl; } else { cout << "Fail." << endl; } } 2。根據算法2計算解決題目的程序代碼: #include <fstream> #include <algorithm> #include <string> #include <sstream> #include <list> #include <cmath> #include <climits> #include <bitset> using namespace std; const char* INPUT_FILE = "game.in"; const char* OUTPUT_FILE = "game.out"; const int NUMBER_COUNT = 6; const int STATE_COUNT = (1 << NUMBER_COUNT); const int MAX_NUMBER = 100; const int MAX_EXPECTION = 1000; const int MAX_VALUE = MAX_EXPECTION * MAX_NUMBER; struct Node { int value; int left, right; int leftvalue, rightvalue; char opr; }; typedef list<Node> NodeList; struct State { bitset<MAX_VALUE+10> exist; NodeList nodelist; }; int number[NUMBER_COUNT], expection; State state[STATE_COUNT]; void ReadData() { ifstream fin(INPUT_FILE); for (int i = 0; i < NUMBER_COUNT; i++) { fin >> number[i]; } fin >> expection; } void Init() { Node node ; for (int i = 0; i < NUMBER_COUNT; i++) { node.value = number[i]; node.left = node.right = -1; state[(1 << i)].nodelist.push_back(node); state[(1 << i)].exist[node.value] = true; } } void Merge(int a, int b, int x) { Node node; NodeList::const_iterator i, j; for (i = state[a].nodelist.begin(); i != state[a].nodelist.end(); i++) { for (j = state[b].nodelist.begin(); j != state[b].nodelist.en d(); j++) { node.value = (*i).value + (*j).value; node.left = a; node.right = b; node.leftvalue = (*i).value; node.rightvalue = (*j).value; node.opr = '+'; if ( (node.value <= MAX_VALUE) && (!state[x].exist[no de.value]) ) { state[x].nodelist.push_back(node); state[x].exist[node.value] = true; } ///////////////////////////////////////////////////// double tmp = double((*i).value) * double((*j).value); if (tmp < INT_MAX) { node.value = (*i).value * (*j).value; node.left = a; node.right = b; node.leftvalue = (*i).value; node.rightvalue = (*j).value; node.opr = '*'; if ( (node.value <= MAX_VALUE) && (!state[x]. exist[node.value]) ) { state[x].nodelist.push_back(node); state[x].exist[node.value] = true; } } ///////////////////////////////////////////////////// if ((*i).value >= (*j).value) { node.value = (*i).value - (*j).value; node.left = a; node.right = b; node.leftvalue = (*i).value; node.rightvalue = (*j).value; node.opr = '-'; } else { node.value = (*j).value - (*i).value; node.left = b; node.right = a; node.leftvalue = (*j).value; node.rightvalue = (*i).value; node.opr = '-'; } if ( (node.value <= MAX_VALUE) && (!state[x].exist[no de.value]) ) { state[x].nodelist.push_back(node); state[x].exist[node.value] = true; } ///////////////////////////////////////////////////// if ( ((*j).value != 0) && ((*i).value >= (*j).value) & & ((*i).value % (*j).value == 0) ) { node.value = (*i).value / (*j).value; node.left = a; node.right = b; node.leftvalue = (*i).value; node.rightvalue = (*j).value; node.opr = '/'; } else if ( ((*i).value != 0) && ((*j).value >= (*i). value) && ((*j).value % (*i).value == 0) ) { node.value = (*j).value / (*i).value; node.left = b; node.right = a; node.leftvalue = (*j).value; node.rightvalue = (*i).value; node.opr = '/'; } if ( (node.value <= MAX_VALUE) && (!state[x].exist[no de.value]) ) { state[x].nodelist.push_back(node); state[x].exist[node.value] = true; } ///////////////////////////////////////////////////// } } } void Solve() { Init(); for (int x = 2; x < STATE_COUNT; x++) { for (int i = 1; i < x; i++) { if ( (x & i) == i ) { int j = x - i; if (i <= j) { Merge(i, j, x); } } } } } void PrintExpression(ostream& out, Node node) { if (node.left == -1) { out << node.value; } else { NodeList::const_iterator iter; out << "("; for (iter = state[node.left].nodelist.begin(); iter != state[node.left].nodelist.end(); iter++) { if ((*iter).value == node.leftvalue) { PrintExpression(out, *iter); break; } } out << node.opr; for (iter = state[node.right].nodelist.begin(); iter != state[node.right].nodelist.end(); iter++) { if ((*iter).value == node.rightvalue) { PrintExpression(out, *iter); break; } } out << ")"; } } void Output() { ofstream fout(OUTPUT_FILE); int bestValue = -INT_MAX; NodeList::const_iterator iter, bestIter; NodeList& nodelist = state[STATE_COUNT-1].nodelist; for (iter = nodelist.begin(); iter != nodelist.end(); iter++) { if ( ((*iter).value <= expection) && (bestValue < (*iter).val ue) ) { bestValue = (*iter).value; bestIter = iter; } } fout << bestValue << endl; PrintExpression(fout, *bestIter ); fout << endl; } int main() { ReadData(); Solve(); Output(); return 0; }