24点的回溯求解算法
原题目见leetcode:
679. 24 点游戏
leetcode, 24点游戏, 回溯算法
基本思路
- 递归(回溯):每一步从当前数组中选择任意两个数,计算这两个数可能得到的所有结果(+ - * /),把结果替换回数组中,继续递归。
- 终止条件:数组长度为 1 时,用 EPS 判断该值是否等于 24。
关键实现要点
选择两个数(枚举 C(n,2))
代码说明:用双重循环 i, j(i < j)枚举任意一对数。
javascript
for (let i = 0; i < n - 1; i++) {
for (let j = i + 1; j < n; j++) {
// nums[i], nums[j] 是当前的数对
}
}
解释:保证不重复且覆盖所有组合;i 从 0 到 n-2,j 从 i+1 到 n-1。
生成候选运算结果
代码说明:对选中的 a, b 生成所有可能的四则运算结果,注意除法时除数需非 0(用 EPS 判断)。
javascript
const results = [];
results.push(a + b);
results.push(a - b);
results.push(b - a);
results.push(a * b);
if (Math.abs(b) > EPS) results.push(a / b);
if (Math.abs(a) > EPS) results.push(b / a);
解释:减法和除法非交换(a-b 与 b-a 都要尝试),乘法和加法可视为交换对称但为简单起见这里保留全部结果,后续可选去重优化。
构造下一层数组并递归
代码说明:把除去 i、j 的剩余数复制到下一层数组 nextNums,再把某个候选结果 push 进去,然后递归调用。
javascript
const nextNums = [];
for (let k = 0; k < n; k++) {
if (k !== i && k !== j) nextNums.push(nums[k]);
}
for (const res of results) {
nextNums.push(res);
if (solve24(nextNums)) return true; // 只要有一条分支成功就返回 true
nextNums.pop(); // 回溯,恢复状态
}
解释:每个候选结果代表把 a 与 b 用某种运算合并为一个新数,数组长度减一,继续穷举。
终止条件与数值判断
代码说明:当数组只剩一个数时,用 EPS 判断是否为 24。
javascript
if (nums.length === 1) {
return Math.abs(nums[0] - 24) < EPS;
}
解释:浮点计算会有误差,使用 EPS(1e-6)判断“接近等于”。
回溯与剪枝提示
说明:递归返回后需恢复 nextNums(pop),否则状态污染;可选地对 results 或(i,j)层使用 Set 去重,减少重复分支。示例剪枝:当 a === b 时,a+b 与 b+a 相同,可避免重复。
数值与精度注意点
说明:除法前判断除数绝对值大于 EPS;若要完全避免浮点误差,可用分数/有理数表示(实现复杂度上升)。
完整代码示例
下面给出完整、可运行且带注释的 JavaScript 实现:
javascript
// 完整实现:判断是否能用四则运算得到 24
const EPS = 1e-6;
function judgePoint24(cards) {
if (!cards || cards.length === 0) return false;
// 将所有输入转换为浮点数,避免整数除法问题
const nums = cards.map(x => Number(x));
return solve24(nums);
}
function solve24(nums) {
const n = nums.length;
if (n === 0) return false;
if (n === 1) {
return Math.abs(nums[0] - 24) < EPS;
}
// 枚举任意两个数
for (let i = 0; i < n - 1; i++) {
for (let j = i + 1; j < n; j++) {
const a = nums[i];
const b = nums[j];
// 构造下一层数组:把除了 i, j 的数放入 nextNums
const nextNums = [];
for (let k = 0; k < n; k++) {
if (k !== i && k !== j) nextNums.push(nums[k]);
}
// 生成所有可能的运算结果
const results = [];
results.push(a + b);
results.push(a - b);
results.push(b - a);
results.push(a * b);
if (Math.abs(b) > EPS) results.push(a / b);
if (Math.abs(a) > EPS) results.push(b / a);
// 对每个可能的结果,放回数组并递归
for (const res of results) {
nextNums.push(res);
if (solve24(nextNums)) return true;
nextNums.pop();
}
}
}
return false;
}
// 示例
console.log(judgePoint24([3, 3, 8, 8])); // true
console.log(judgePoint24([1, 1, 1, 1])); // false
如何运行
将上面代码保存为 solve24.js
,然后在终端中运行:
bash
node solve24.js
小结与后续优化建议
- 这是一道典型的回溯穷举题,关键在于如何枚举数对并正确处理浮点与除法的边界。可选优化包括:在每层对 results 去重(减少重复分支)、对对称运算(加、乘)做剪枝、或对有理数情况使用分数表示以完全避免浮点误差(复杂度和实现难度增加)。