HJ67 24点游戏算法 ●●
679. 24点游戏 ●●●
描述
给定一个长度为4的整数数组 cards 。你有 4 张卡片,每张卡片上都包含一个范围在 [1,9] 的数字。您应该使用运算符 [‘+’, ‘-’, ‘*’, ‘/’] 和括号 ‘(’ 和 ‘)’ 将这些卡片上的数字排列成数学表达式,以获得值24。
你须遵守以下规则:
- 除法运算符 ‘/’ 表示实数除法,而不是整数除法。
例如, 4 /(1 - 2 / 3)= 4 /(1 / 3)= 12 。 - 每个运算都在两个数字之间。特别是,不能使用 “-” 作为一元运算符。
例如,如果 cards =[1,1,1,1] ,则表达式 “-1 -1 -1 -1” 是 不允许 的。 - 你不能把数字串在一起
例如,如果 cards =[1,2,1,2] ,则表达式 “12 + 12” 无效。
如果可以得到这样的表达式,其计算结果为 24 ,则返回 true ,否则返回 false 。
输入描述:
读入4个[1,10]的整数,数字允许重复,测试用例保证无异常数字。
输出描述:
对于每组案例,输出一行表示能否得到24点,能输出true,不能输出false。
示例
输入:
7 2 1 10
输出:
true
题解
1. 回溯
参考LeetCode官方题解:
一共有 4 个数和 3 个运算操作,因此可能性非常有限。一共有多少种可能性呢?
首先从 4 个数字中有序地选出 2 个数字,共有 4×3=12 种选法,并选择加、减、乘、除 4 种运算操作之一,用得到的结果取代选出的 2 个数字,剩下 3 个数字。
然后在剩下的 3 个数字中有序地选出 2 个数字,共有 3×2=6 种选法,并选择 4 种运算操作之一,用得到的结果取代选出的 2 个数字,剩下 2 个数字。
最后剩下 2 个数字,有 2 种不同的顺序,并选择 4 种运算操作之一。
因此,一共有 12 × 4 × 6 × 4 × 2 × 4 = 9216 12 \times 4 \times 6 \times 4 \times 2 \times 4=9216 12×4×6×4×2×4=9216 种不同的可能性。
可以通过回溯的方法遍历所有不同的可能性。具体做法是,使用一个列表存储目前的全部数字,每次从列表中选出 2 个数字,再选择一种运算操作,用计算得到的结果取代选出的 2 个数字,这样列表中的数字就减少了 1 个。重复上述步骤,直到列表中只剩下 1 个数字,这个数字就是一种可能性的结果,如果结果等于 24,则说明可以通过运算得到 24。如果所有的可能性的结果都不等于 24,则说明无法通过运算得到 24。
实现时,有一些细节需要注意。
-
除法运算为实数除法,因此结果为浮点数,列表中存储的数字也都是浮点数。在判断结果是否等于 24 时应考虑精度误差,这道题中,误差小于 1 0 − 6 10^{-6} 10−6 可以认为是相等。
-
进行除法运算时,除数不能为 0,如果遇到除数为 0 的情况,则这种可能性可以直接排除。由于列表中存储的数字是浮点数,因此判断除数是否为 0 时应考虑精度误差,这道题中,当一个数字的绝对值小于 1 0 − 6 10^{-6} 10−6 时,可以认为该数字等于 0。
还有一个可以优化的点。
- 加法和乘法都满足交换律,因此如果选择的运算操作是加法或乘法,则对于选出的 2 个数字不需要考虑不同的顺序,在遇到第二种顺序时可以不进行运算,直接跳过。
- 时间复杂度:O(1)。一共有 9216 种可能性,对于每种可能性,各项操作的时间复杂度都是 O(1),因此总时间复杂度是 O(1)。
- 空间复杂度:O(1)。空间复杂度取决于递归调用层数与存储中间状态的列表,因为一共有 4 个数,所以递归调用的层数最多为 4,存储中间状态的列表最多包含 4 个元素,因此空间复杂度为常数。
#include <iostream>
#include <vector>
using namespace std;
const int TARGET = 24;
const double ERROR = 1e-6;
const int ADD = 0, SUBTRACT = 1, MULTIPLY = 2, DIVIDE = 3;
bool backtrack(vector<double> & nums){
if(nums.size() == 1) return abs(nums[0] - TARGET) < ERROR; // 只剩一个数,为最终结果
int size = nums.size();
for(int i = 0; i < size; ++i){ // 选择不同的两个数作为运算数
for(int j = 0; j < size; ++j){
if(i == j) continue; // 避免两个数相同的情况
vector<double> rest;
for(int h = 0; h < size; ++h){
if(h != i && h != j) rest.emplace_back(nums[h]); // rest用于记录剩下的数,以及两数运算后的结果
}
for(int op = 0; op < 4; ++op){ // 对选取的两个数进行四种运算操作
if((op == ADD || op == MULTIPLY) && i > j) continue; // i ADD j == j ADD i,已在 i < j 时计算过,减少重复计算
if(op == ADD){
rest.emplace_back(nums[i] + nums[j]); // 加法
}else if(op == SUBTRACT){
rest.emplace_back(nums[i] - nums[j]); // 减法
}else if(op == MULTIPLY){
rest.emplace_back(nums[i] * nums[j]); // 乘法
}else{
if(abs(nums[j]) < ERROR) continue; // 避免0为除数
rest.emplace_back(nums[i] / nums[j]); // 除法
}
if(backtrack(rest)) return true; // 递归计算剩下的数字,4->3,3->2,2->1
rest.pop_back(); // 回溯,换一个运算符
}
}
}
return false;
}
int main(){
vector<double> nums(4, 0); // 实数运算,将int转换为double,num < 1e-6视为0
while(cin >> nums[0] >> nums[1] >> nums[2] >> nums[3]){
if(backtrack(nums)){
cout << "true" << endl;
}else{
cout << "false" << endl;
}
}
return 0;
}