C++ 实现计算24点

原理

暴力枚举所有的情况,运算符号4个,加减乘除 + - * / ,整数数字4个(易扩展为5个数或者更多)。所需要枚举的次数:

  1. 数字顺序:4个数的全排列,4! = 24
  2. 运算符号:4个数需要3个符号,每个可选4种,43 = 64
  3. 加括号方式:((AB)C)D(A(BC))D(AB)(CD)A((BC)D)A(B(CD)),共5种。
  4. 枚举次数 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)->110010A((BC)D)->110100A(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;
}

测试

  • 18
    点赞
  • 56
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值