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; }