1. 题目来源
链接:24 点游戏
来源:LeetCode
2. 题目说明
你有 4 张写有 1 到 9 数字的牌。你需要判断是否能通过 *,/,+,-,(,) 的运算得到 24。
示例1:
输入: [4, 1, 8, 7]
输出: True
解释: (8-4) * (7-1) = 24
示例2:
输入: [1, 2, 1, 2]
输出: False
注意:
- 除法运算符
/
表示实数除法,而不是整数除法。例如4 / (1 - 2/3) = 12
。 - 每个运算符对两个数进行运算。特别是我们不能用 - 作为一元运算符。例如,
[1, 1, 1, 1]
作为输入时,表达式-1 - 1 - 1 - 1
是不允许的。 - 你不能将数字连接在一起。例如,输入为 [1, 2, 1, 2] 时,不能写成 12 + 12 。
3. 题目解析
方法一:针对数字递归遍历
遍历所有的情况,那么既然是要遍历所有的情况,就递归吧:
- 两个数字加减乘除中加法和乘法对于两个数字的前后顺序没有影响,但是减法和除法是有影响的,而且做除法的时候还要另外保证除数不能为零。
- 要遍历任意两个数字,然后对于这两个数字,尝试各种加减乘除后得到一个新数字,将这个新数字加到原数组中,记得原来的两个数要移除掉
- 调用递归函数进行计算,可以发现每次调用递归函数后,数组都减少一个数字,那么当减少到只剩一个数字了,就是最后的计算结果
- 在递归函数开始时判断,如果数组只有一个数字,且为 24,说明可以算出 24,结果
res
赋值为true
返回。这里的结果res
是一个全局的变量,如果已经为true
了,就没必要再进行运算了,所以第一行应该是判断结果res
,为true
就直接返回了 - 遍历任意两个数字,分别用
p
和q
来取出,然后进行两者的各种加减乘除的运算,将结果保存进数组临时数组t
,记得要判断除数不为零。然后将原数组nums
中的p
和q
移除,遍历临时数组t
中的数字,将其加入数组nums
,并调用递归函数 - 记得完成后要移除数字,恢复状态,这是递归解法很重要的一点。最后还要把
p
和q
再加回原数组nums
,这也是还原状态
注意点:
- 不要将
vector<int>
直接传给vector<double>
,会发生自动类型转换导致出错 - 判定浮点的规则一定需要牢记
在牛客网题库中,也有这个题,但是牛客的测试用例很不严谨,例如浮点数相等判定,除数为0判定,都没有相应的测试用例进行报错,相对这个题而言 LeetCode 测试严格了很多。牛客Link:[编程题]24点游戏算法
参见代码如下:
// 执行用时 :24 ms, 在所有 C++ 提交中击败了46.72%的用户
// 内存消耗 :11 MB, 在所有 C++ 提交中击败了37.59%的用户
class Solution {
public:
bool judgePoint24(vector<int>& nums) {
bool res = false;
double eps = 0.001;
vector<double> arr(nums.begin(), nums.end());
helper(arr, eps, res);
return res;
}
void helper(vector<double>& nums, double eps, bool& res) {
if (res)
return;
if (nums.size() == 1) {
if (abs(nums[0] - 24) < eps)
res = true;
return;
}
for (int i = 0; i < nums.size(); ++i) {
for (int j = 0; j < i; ++j) {
double p = nums[i], q = nums[j];
vector<double> t{p + q, p - q, q - p, p * q};
if (p > eps)
t.push_back(q / p);
if (q > eps)
t.push_back(p / q);
nums.erase(nums.begin() + i);
nums.erase(nums.begin() + j);
for (double e : t) {
nums.push_back(e);
helper(nums, eps, res);
nums.pop_back();
}
nums.insert(nums.begin() + j, q);
nums.insert(nums.begin() + i, p);
}
}
}
};
方法二:针对符号递归遍历
来自大佬的的一种很不同的递归写法,这里将加减乘除操作符放到了一个数组 ops
中。并且没有用全局变量res
,而是让递归函数带有bool
型返回值。在递归函数中,还是要先看nums
数组的长度,如果为 1 了,说明已经计算完成,直接看结果是否为 0 就行了。然后遍历任意两个数字,注意这里的i
和j
都分别从 0 到了数组长度,而上面解法的j
是从 0 到i
,这是因为上面解法将p - q, q - p, q / q, q / p
都分别列出来了,而这里仅仅是nums[i] - nums[j], nums[i] / nums[j],
所以i
和j
要交换位置,但是为了避免加法和乘法的重复计算,可以做个判断,还有别忘记了除数不为零的判断,i
和j
不能相同的判断。建立一个临时数组t
,将非i
和j
位置的数字都加入t
,然后遍历操作符数组ops
,每次取出一个操作符,然后将nums[i]
和nums[j]
的计算结果加入t
,调用递归函数,如果递归函数返回true
了,那么就直接返回true
。否则移除刚加入的结果,还原t
的状态
参见代码如下:
// 执行用时 :28 ms, 在所有 C++ 提交中击败了41.39%的用户
// 内存消耗 :10 MB, 在所有 C++ 提交中击败了51.88%的用户
class Solution {
public:
bool judgePoint24(vector<int>& nums) {
double eps = 0.001;
vector<char> ops{'+', '-', '*', '/'};
vector<double> arr(nums.begin(), nums.end());
return helper(arr, ops, eps);
}
bool helper(vector<double>& nums, vector<char>& ops, double eps) {
if (nums.size() == 1)
return abs(nums[0] - 24) < eps;
for (int i = 0; i < nums.size(); ++i) {
for (int j = 0; j < nums.size(); ++j) {
if (i == j)
continue;
vector<double> t;
for (int k = 0; k < nums.size(); ++k) {
if (k != i && k != j) t.push_back(nums[k]);
}
for (char op : ops) {
if ((op == '+' || op == '*') && i > j)
continue;
if (op == '/' && nums[j] < eps)
continue;
switch(op) {
case '+': t.push_back(nums[i] + nums[j]); break;
case '-': t.push_back(nums[i] - nums[j]); break;
case '*': t.push_back(nums[i] * nums[j]); break;
case '/': t.push_back(nums[i] / nums[j]); break;
}
if (helper(t, ops, eps))
return true;
t.pop_back();
}
}
}
return false;
}
};