原理
暴力枚举所有的情况,运算符号4个,加减乘除 + - * / ,整数数字4个(易扩展为5个数或者更多)。所需要枚举的次数:
- 数字顺序:4个数的全排列,4! = 24。
- 运算符号:4个数需要3个符号,每个可选4种,43 = 64。
- 加括号方式:((AB)C)D、(A(BC))D、(AB)(CD)、A((BC)D)、A(B(CD)),共5种。
- 枚举次数 24*64*5
实现细节
- 全排列枚举由库函数
next_permutation
来完成枚举 - 64种运算符号搭配由一个整数状压(0—63)来完成枚举
- 加括号方式由后缀表达式来完成,运算对象用0表示,运算符用1表示,由右向左开始,以
(A-(B+C))*D
为例,转成后缀表达式为ABC+-D*
,改成由右向左阅读的顺序,*D-+CBA
,再转成用01标记的二进制数1011000
,为了在代码中便于对运算符和运算对象同时操作,去掉最后一个0,变为101100
,去掉的那个在初始时提前压栈,这样就正好3个0、3个1了,同理,((AB)C)D
->101010
、(AB)(CD)
->110010
、A((BC)D)
->110100
、A(B(CD))
->111000
- 由于运算过程中含有除法,用double又不是我风格,所以写个小结构体表示有理数。包含分子和分母。
- 验证是否有解和打印解分开,更灵活。
代码
运算过程和模拟计算后缀表达式差不多,一个原理。
#include <bits/stdc++.h>
#define top_and_pop(stack, var) var=stack.top();stack.pop()
using namespace std;
//运算 + - * / , 数量 num_n = 4
const int num_n = 4, max_oper = 1 << (2*num_n - 2);
struct Num {
int a, b;
Num(int ta = 0, int tb = 1) : a(ta), b(tb) {
if(b < 0)
a = -a, b = -b;
int g = __gcd(abs(a), b);
a /= g, b /= g;
}
};
// 候选的后缀表达式所代表的整数值
int methods[5] = {0b101010, 0b101100, 0b110010, 0b110100, 0b111000};
bool hasAnswer(int* arr, int oper_code, int method) {
stack<int> ops;
stack<Num> nums;
int arr_pos = 0;
nums.push(Num(arr[arr_pos++]));
while(method) {
if(method & 1) {
Num x,y;
top_and_pop(nums, y);
top_and_pop(nums, x);
switch(ops.top()) {
case 0: // +
nums.push({x.a * y.b + x.b * y.a, x.b * y.b});
break;
case 1: // -
nums.push({x.a * y.b - x.b * y.a, x.b * y.b});
break;
case 2: // *
nums.push({x.a * y.a, x.b * y.b});
break;
case 3: // /
if(y.a == 0)
return false;
nums.push({x.a * y.b, x.b * y.a});
break;
}
ops.pop();
} else {
nums.push(Num(arr[arr_pos++]));
ops.push(oper_code & 3);
oper_code >>= 2;
}
method >>= 1;
}
return nums.top().a == 24 && nums.top().b == 1;
}
void printAnswer(int* arr, int oper_code, int method) {
const char* operstr = "+-*/";
string zuo = "(", you = ")", s1, s2;
stack<char> ops;
stack<string> str;
int arr_pos = 0;
str.push(to_string(arr[arr_pos++]));
while(method) {
if(method & 1) {
top_and_pop(str, s2);
top_and_pop(str, s1);
str.push(zuo + s1 + ops.top() + s2 + you);
ops.pop();
} else {
str.push(to_string(arr[arr_pos++]));
ops.push(operstr[oper_code & 3]);
oper_code >>= 2;
}
method >>= 1;
}
string res = str.top();
cout << res.substr(1, res.length() - 2) << endl;
}
int arr[num_n]; // 4 个数字
int main() {
int t;
cin >> t;
outer_loop:
while(t--) {
for(int i = 0; i < num_n; i++) {
cin >> arr[i];
}
sort(arr, arr + num_n);
do {
for(int meth : methods) {
for(int oper = 0; oper < max_oper; oper++) {
if(hasAnswer(arr, oper, meth)) {
printAnswer(arr, oper, meth);
goto outer_loop; // break multiloop
}
}
}
} while(next_permutation(arr, arr + num_n));
cout << "No solution" << endl;
}
}
测试
注: 第一个数是测试组数
扩展
上述代码可以很容易的扩展成多个数字,比如对5个数字进行24点计算。把 num_n
改为5,然后再修改一下 methods
数组为
int methods[14] = {0b10101010,0b10101100,0b10110010,0b10110100,0b10111000,0b11001010,0b11001100,0b11010010,0b11010100,0b11011000,0b11100010,0b11100100,0b11101000,0b11110000};
其中14就是5个数字加括号一共的种类数,他是卡特兰数。其实上面那一串二进制数是有规律的,所以可以写代码来生成,而不需要手算。
其实,也可以写成递归版的,就不需要算这些后缀表达式了。改天写一写。
5个数24点测试
(date: 2019.2.17)
five years later…
(date:2024.1.15)
递归版的
原理就是遍历每一组的两两组合,组合成一个新数,这样总数量减一,就能按数量递归下去了。
// 24dian.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//
#include <iostream>
#include <vector>
#include <string>
using Numbers = std::vector<double>;
using Answers = std::vector<std::string>;
//存储步骤解
Answers answers;
//转为字符串 a + b = c
std::string to_concat(double a, char oper, double b, double c) {
char buffer[100];
std::snprintf(buffer, 100, "%g %c %g = %g", a, oper, b, c);
return std::string(buffer);
}
//递归求解
bool my_solve24(const Numbers& nums, Answers* ans = nullptr) {
int n = nums.size();
//只剩一个数时,直接判断是否等于24
if (n == 1) {
return std::abs(nums[0] - 24) < 1e-6;
}
//第一次,清空答案
if (ans == nullptr) {
ans = &answers;
ans->clear();
}
//尝试两两组合
for (int i = 0;i < n;i++) {
for (int j = i + 1;j < n;j++) {
//拷贝一个不含 nums[i] 和 nums[j] 的新数组
Numbers newArr;
for (int k = 0; k < n; k++) {
if (k == i || k == j)
continue;
newArr.push_back(nums[k]);
}
double a = nums[i], b = nums[j];
//a+b
newArr.push_back(a + b);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(a, '+', b, a + b));
return true;
}
newArr.pop_back();
//a-b
newArr.push_back(a - b);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(a, '-', b, a - b));
return true;
}
newArr.pop_back();
//b-a
newArr.push_back(b - a);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(b, '-', a, b - a));
return true;
}
newArr.pop_back();
//a*b
newArr.push_back(a * b);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(a, '*', b, a * b));
return true;
}
newArr.pop_back();
//a/b
if (b != 0) {
newArr.push_back(a / b);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(a, '/', b, a / b));
return true;
}
newArr.pop_back();
}
//b/a
if (a != 0) {
newArr.push_back(b / a);
if (my_solve24(newArr, ans)) {
ans->push_back(to_concat(b, '/', a, b / a));
return true;
}
newArr.pop_back();
}
}
}
return false;
}
int main() {
const int n = 4;
while (true) {
Numbers nums;
std::cout << "请输入" << n << "个整数,用空格分隔:" << std::endl;
for (int i = 0; i < n; i++) {
double num;
std::cin >> num;
nums.push_back(num);
}
if (my_solve24(nums)) {
//输出答案
for (auto it = answers.rbegin(); it != answers.rend(); it++) {
std::cout << *it << std::endl;
}
}
else {
std::cout << "无解。" << std::endl;
}
}
return 0;
}