问题:
N个1到13之间的自然数,能否通过加减乘除计算(每个数有且只能用一次)得到24?
计算24点常用的算法有:① 任取两个数,计算后,将结果放回去,再从剩下的数中任取两个,如此反复直到只剩下一个数;② 先构建前缀/后缀表达式,再计算该表达式;③ 用集合保存中间结果,集合间两两进行合并计算得到新集合(或者对给定的一个集合,对其所有的子集合进行合并计算)。
本文采用第一种方法。定义六种操作符:ADD、SUB、MUL、DIV、RSUB、RDIV,分别对应加、减、乘、除、反减和反除(反减/除:先交换两个数,再减/除)。
显然,取两个数计算时,六种计算结果可能有重复,可以对这6个结果进行去重(实际上,只要分别对加减(ADD、SUB、RSUB)和乘除(MUL、DIV、RDIV)的3个计算结果进行去重判断就可以了,效率和对6个结果去重相差不大)。
另外一种剪枝方法:保存每个数上次计算时所用的操作符(初始值为空)。所取的两个数:
若某个数的上次操作符为减(SUB、RSUB),那么不进行加减(ADD、SUB、RSUB)计算。
若某个数的上次操作符为除(DIV、RDIV),那么不进行乘除(MUL、DIV、RDIV)计算。
比如:取的两个数为 a-b 和 c(c的上次操作符任意),如果进行加减计算的话,
a-b+c 和 c+a-b重复,
c-(a-b)和 c+b-a重复
a-b-c 和 c+b RSUB a重复
也就是说,上次操作符为减的,进行加减计算时,总可以转为某个上次操作符为加的表达式,因而可以不计算。同样,上次操作符为除的,不进行乘除计算。
当然,还可以考虑记录位置进行剪枝,这样避免a+b+c和a+c+b都进行计算。但要注意的是:在给定的组合无解时,越多的剪枝方法,极有可能提高搜索效率,但在给定的组合有解时,很可能反而降低搜索效率。
另外,对有解时输出的表达式的处理对程序的性能影响很大。如果每次计算都保存对应的表达式,会进行大量的字符串操作,严重影响性能。实际上,每次计算只要保存取出的两个数的位置和所进行计算的操作符就够了,最终需要输出表达式时,只要模拟一下递归函数调用过程,进行相应的字符串操作。
下面是程序(gcc 4.5,优化参数-O2)在T4200/2G单核下运行的结果,计算10个数,646646个组合只用了不到13分钟。
N个1到13之间的数的所有组合,计算24点
N | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
时间(ms) | 78 | 610 | 4157 | 19593 | 160922 | 173766 | 764328 |
有解组合数 | 1362 | 6104 | 18554 | 50386 | 125969 | 293930 | 646646 |
总组合 | 1820 | 6188 | 18564 | 50388 | 125970 | 293930 | 646646 |
总表达式 | 1124776 | 15656645 | 105278906 | 492587616 | 3760744504 | 4535030813 | 19912345238 |
表达式A | 457549 | 11864184 | 88679768 | 409129896 | 1173803224 | 4535030813 | 19912345238 |
表达式B | 667227 | 3792461 | 16599138 | 83457720 | 2586941280 | 0 | 0 |
总函数调用 | 1482939 | 20950792 | 141892513 | 669790534 | 5258218577 | 6112529945 | 26948662625 |
函数调用A | 603206 | 15849306 | 119160441 | 551913059 | 1576965280 | 6112529945 | 26948662625 |
函数调用B | 879733 | 5101486 | 22732072 | 117877475 | 3681253297 | 0 | 0 |
其中:表达式A/B、函数调用A/B为:给定的n个数有/无解时,所处理的表达式总数和函数调用总次数。
N=8 与N=9,两个所用时间只相差13秒,这是由于N为8时,有一个组合(8个1)无解,判断这个组合无解需110多秒,而计算剩下的125969个组合还不要50秒。
#include < iostream >
#include < fstream >
#include < sstream >
#include < vector >
#include < ctime >
#include < cmath >
using std::cout;
using std:: string ;
using std::vector;
using std::ofstream;
using std::stringstream;
class Calc {
public :
Calc(){};
void print_result() const ;
bool run( const int src[], size_t sz, double n = 24.0 ,
bool expr_calc = true , bool show = true );
void calc_range( int first, int last,size_t N = 4 ,
double M = 24 , string filename = " 24.out " );
const string & get_expr() const { return expr;}
size_t get_count_expr() const { return count_expr;}
size_t get_count_func() const { return count_func;}
private :
Calc( const Calc & );
Calc & operator = ( const Calc & );
bool init( const int src[], size_t sz, double n);
bool calc(size_t step);
inline bool calc2(size_t step, size_t pos2, double na, double nb, int op);
void calc_expr();
void add_parentheses( string & str) {
string tmp; tmp.reserve(str.size() + 2 );
tmp += ' ( ' ; tmp += str; tmp += ' ) ' ;
str.swap(tmp);
}
char get_op_char( int op) { return char (op >> RSHIFT); }
int get_opv( int op) { return op & OPV_MASK; }
// 0-2位表示操作符的优先级 加减: 1 乘除2 初始值4
// +3位,即RFLAG标志,表示对减除法,交换两个操作数后再计算
// 4-7位表示操作符,8-15位表示该操作符的ascii值
enum {
OP_NULL = 4 , RFLAG = 8 , RSHIFT = 8 , OPV_MASK = 7 ,
FLAG_ADD = 0x10 , FLAG_SUB = 0x20 , FLAG_MUL = 0x40 , FLAG_DIV = 0x80 ,
ADD = ' + ' << RSHIFT | FLAG_ADD | 1 , SUB = ' - ' << RSHIFT | FLAG_SUB | 1 ,
MUL = ' * ' << RSHIFT | FLAG_MUL | 2 , DIV = ' / ' << RSHIFT | FLAG_DIV | 2 ,
RSUB = SUB | RFLAG, RDIV = DIV | RFLAG,
};
struct Info_step { // 记录每一步取两个数所进行的计算
size_t first; // 第一个操作数位置
size_t second; // 第二个操作数位置
int op; // 操作符
};
size_t size;
string expr; // 得到的表达式
double result; // 要得到的结果值
size_t count_expr; // 处理的表达式总数
size_t count_func; // 函数被调用次数
vector < int > old_number; // 要计算的数
vector < double > number; // 中间计算结果
vector < int > ops; // 上一次计算所用的操作符,初始值要设为OP_NULL
vector < Info_step > info_step;
};
bool Calc::init( const int src[], size_t sz, double n)
{
if (sz == 0 || src == NULL) return false ;
size = sz;
expr.clear();
result = n;
count_expr = 0 ;
count_func = 0 ;
old_number.assign(src, src + sz);
number.assign(src, src + sz);
ops.assign(sz, OP_NULL);
info_step.clear();
info_step.resize(sz);
return true ;
}
bool Calc::run( const int src[], size_t sz, double n, bool expr_calc, bool show)
{
if ( ! init(src, sz, n)) return false ;
bool ret = calc(sz);
if (ret && expr_calc) calc_expr();
if (show) print_result();
return ret;
}
void Calc::calc_expr()
{
const size_t sz = size;
static vector < string > str;
static vector < int > op_prev;
static stringstream ss;
str.resize(sz);
op_prev.assign(sz,OP_NULL); // 初始值
for (size_t i = 0 ; i < sz; ++ i) {
ss << old_number[i];
getline(ss, str[i]);
ss.clear();
}
for (size_t k = sz; k -- > 1 ; ) {
size_t i = info_step[k].first;
size_t j = info_step[k].second;
int op = info_step[k].op;
int opv = get_opv(op);
int op1v, op2v;
if (op & RFLAG) {
str[i].swap(str[j]);
op1v = get_opv(op_prev[j]);
op2v = get_opv(op_prev[i]);
} else {
op1v = get_opv(op_prev[i]);
op2v = get_opv(op_prev[j]);
}
if (opv > op1v) add_parentheses(str[i]);
if (opv > op2v || (opv == op2v && (op & (FLAG_SUB | FLAG_DIV))))
add_parentheses(str[j]);
op_prev[i] = op;
op_prev[j] = op_prev[k];
str[i].reserve(str[i].size() + str[j].size() + 1 );
str[i] += get_op_char(op);
str[i] += str[j];
str[j].swap(str[k]);
}
expr.swap(str[ 0 ]);
}
bool Calc::calc(size_t step)
{
++ count_func;
if (step <= 1 ) {
++ count_expr;
const double zero = 1e - 9 ;
if (fabs(result - number[ 0 ]) < zero) return true ;
return false ;
}
-- step;
for (size_t i = 0 ; i < step; i ++ ){
info_step[step].first = i;
for (size_t j = i + 1 ; j <= step; j ++ ) {
info_step[step].second = j;
double na = number[i];
double nb = number[j];
int op1 = ops[i];
int op2 = ops[j];
number[j] = number[step];
ops[j] = ops[step];
int tt = op1 | op2;
bool ba = true , bb = true ;
if (tt & FLAG_SUB) ba = false ;
if (tt & FLAG_DIV) bb = false ;
if (ba) {
if (calc2(step, i, na, nb, ADD)) return true ;
if (nb != 0 && calc2(step, i, na, nb, SUB)) return true ;
if (na != nb && na != 0 && calc2(step, i, na, nb, RSUB)) return true ;
}
if (bb) {
double nmul = na * nb;
if (calc2(step, i, na, nb, MUL)) return true ;
if (na != 0 && nb != 0 ) {
double ndiv = na / nb;
if (nmul != ndiv && calc2(step,i,na, nb, DIV)) return true ;
double nrdiv = nb / na;
if (nrdiv != ndiv && nrdiv != nmul && calc2(step,i,na, nb, RDIV))
return true ;
}
}
number[i] = na;
number[j] = nb;
ops[i] = op1;
ops[j] = op2;
}
}
return false ;
}
inline bool Calc::calc2(size_t step, size_t pos2, double na, double nb, int op)
{
info_step[step].op = op;
ops[pos2] = op;
switch (op) {
case ADD: number[pos2] = na + nb; break ;
case SUB: number[pos2] = na - nb; break ;
case MUL: number[pos2] = na * nb; break ;
case DIV: number[pos2] = na / nb; break ;
case RSUB: number[pos2] = nb - na; break ;
case RDIV: number[pos2] = nb / na; break ;
default : break ;
}
return calc(step);
}
void Calc::print_result() const
{
if (old_number.empty()) return ;
cout << " number: " ;
for (size_t i = 0 ; i < old_number.size(); ++ i)
cout << old_number[i] << " " ;
cout << " \n " ;
if ( ! expr.empty()) std::cout << " result: " << expr << " = " << result << " \n " ;
cout << " expr/func: " << count_expr << " / " << count_func << " \n\n " ;
}
void Calc::calc_range( int first, int last,size_t N, double M, string filename)
{
if (N == 0 || first >= last || filename.empty()) return ;
clock_t ta = clock();
vector < int > vv(N, first);
int * end = & vv[N - 1 ], * p = end, * arr = & vv[ 0 ];
ofstream ofs(filename.c_str());
size_t count = 0 ;
size_t count_b = 0 ;
typedef unsigned long long size_tt;
size_tt count_expr_a = 0 ;
size_tt count_expr_b = 0 ;
size_tt count_func_a = 0 ;
size_tt count_func_b = 0 ;
while ( true ){
++ count_b;
if (run(arr,N,M, true , false )) {
ofs.width( 4 );
ofs << ++ count << " " ;
for (size_t i = 0 ; i < N; ++ i) {
ofs.width( 2 );
ofs << arr[i] << " " ;
}
ofs << " " << get_expr() << " \n " ;
count_expr_a += count_expr;
count_func_a += count_func;
} else {
count_expr_b += count_expr;
count_func_b += count_func;
}
while (p >= arr && * p >= last) -- p;
if (p < arr) break ;
int tmp = ++* p;
while (p < end) *++ p = tmp;
}
ofs.close();
const char sep[] = " / " ;
cout << " N: " << N << " M: " << M
<< " range: " << first << " - " << last << " \n "
<< " expr: " << count_expr_a + count_expr_b << sep << count_expr_a
<< sep << count_expr_b << " \n "
<< " func: " << count_func_a + count_func_b << sep << count_func_a
<< sep << count_func_b << " \n "
<< " total: " << count << sep << count_b << " \n "
<< " time: " << clock() - ta << " ms\n\n " << std::flush;
}
int main()
{
Calc calc;
int ra[ 4 ] = { 3 , 3 , 8 , 8 };
int rb[ 4 ] = { 5 , 7 , 7 , 11 };
int rc[ 4 ] = { 4 , 7 , 11 , 13 };
calc.run(ra, 4 );
calc.run(rb, 4 );
calc.run(rc, 4 );
// calc.calc_range(1,13,4,24,"nul");
// calc.calc_range(1,50,4,24,"nul");
// calc.calc_range(1,100,4,24,"nul");
calc.calc_range( 1 , 13 , 4 , 24 , " a24-04t.txt " );
calc.calc_range( 1 , 13 , 5 , 24 , " a24-05t.txt " );
calc.calc_range( 1 , 13 , 6 , 24 , " a24-06t.txt " );
calc.calc_range( 1 , 13 , 7 , 24 , " a24-07t.txt " );
calc.calc_range( 1 , 13 , 8 , 24 , " a24-08t.txt " );
calc.calc_range( 1 , 13 , 9 , 24 , " a24-09t.txt " );
calc.calc_range( 1 , 13 , 10 , 24 , " a24-10t.txt " );
}