【LeetCode 每日一题】679. 24 点游戏——回溯

Problem: 679. 24 点游戏

整体思路

这段代码旨在解决经典的 “24点游戏” (24 Game) 问题。问题要求判断给定的四个整数(代表四张牌)是否能通过加、减、乘、除四种运算以及括号的组合,得到结果24。

该算法采用的是一种 回溯 (Backtracking)深度优先搜索 (DFS) 的方法。其核心思想是,通过递归地、穷举地尝试所有可能的运算组合,来探索是否存在一个解。算法的策略是不断地减少牌的数量,直到只剩一张牌,然后判断这张牌是否为24。

算法的逻辑步骤如下:

  1. 初始化

    • 主函数 judgePoint24 接收一个整数数组 cards。为了处理除法运算可能产生的浮点数,它首先将这四个整数转换为一个 List<Double>
    • 然后,它启动核心的 dfs 递归函数。
  2. 递归函数 dfs 的核心逻辑

    • 基础情况 (Base Case):递归的终点是当列表 cards 中只剩下一张牌时 (n == 1)。此时,检查这张牌的值是否在误差范围 EPS 内等于 24。如果是,说明找到了一条通往24的运算路径,返回 true
    • 递归步骤 (Recursive Step):如果列表中有多于一张牌,算法需要选择任意两张牌进行运算,然后用运算结果替换掉这两张牌,进入下一层递归。
      a. 选择两张牌:通过两层嵌套的 for 循环,不重复地(j = i + 1)从当前列表中选出任意两张牌 xy
      b. 穷举所有运算:对选出的 xy,计算所有可能的运算结果:x+y, x-y, y-x, x*y, x/y, y/x。注意,减法和除法不满足交换律,所以 x-yy-x 都要计算。除法操作前要检查除数是否为0(通过 Math.abs(num) > EPS 判断)。
      c. 构造新的牌组并递归:对于每一个运算出的结果 candidate,算法会:
      • 创建一个当前牌组的副本 newCards
      • 从副本中移除 xy,并加入 candidate,形成一个数量减一的新牌组。
      • 对这个新牌组调用 dfs 函数。
      • 代码中的实现技巧:为了高效地构造新牌组,代码先移除索引较大的牌 y (newCards.remove(j)),然后用 candidate 替换掉索引较小的牌 x (newCards.set(i, candidate))。这样操作后,newCards 的大小也减一了。
        d. 结果传播:如果在任何一次递归调用中,dfs 返回了 true,意味着已经找到了一条解法。此时,无需再尝试其他组合,应立即将 true 向上层返回,实现“短路”。
  3. 最终结果

    • 如果所有可能的组合都尝试完毕,仍未找到解(即没有任何 dfs 调用返回 true),则说明这四张牌无法凑出24,函数返回 false

完整代码

import java.util.ArrayList;
import java.util.List;

class Solution {
    // EPS (Epsilon) 是一个极小的正数,用于处理浮点数比较时的精度误差。
    // 如果两个浮点数的差的绝对值小于 EPS,我们认为它们相等。
    private final static double EPS = 1e-9;

    /**
     * 判断给定的四张牌是否能通过运算得到 24。
     * @param cards 包含四个整数的数组
     * @return 如果可以得到 24 则返回 true, 否则返回 false
     */
    public boolean judgePoint24(int[] cards) {
        // 将整数牌转换为 double 类型的列表,以支持除法运算。
        List<Double> temp = new ArrayList<>();
        for (int card : cards) {
            temp.add((double) card);
        }
        // 启动回溯/DFS过程。
        return dfs(temp);
    }

    /**
     * 核心的回溯/DFS函数。
     * @param cards 当前剩余的牌(数字)的列表
     * @return 如果这组牌能凑出 24 则返回 true
     */
    private boolean dfs(List<Double> cards) {
        int n = cards.size();
        // 基础情况:如果只剩下一张牌
        if (n == 1) {
            // 检查这张牌的值是否约等于 24
            return Math.abs(cards.get(0) - 24) < EPS;
        }
        
        // 递归步骤:从 n 张牌中选出两张进行运算
        // 外层循环,选择第一张牌 x
        for (int i = 0; i < n; i++) {
            double x = cards.get(i);
            // 内层循环,选择第二张牌 y (j > i 保证不重复选择)
            for (int j = i + 1; j < n; j++) {
                double y = cards.get(j);
                
                // 生成 x 和 y 运算后的所有可能结果
                List<Double> candidates = new ArrayList<>();
                candidates.add(x + y);
                candidates.add(x - y);
                candidates.add(y - x); // 减法不满足交换律
                candidates.add(x * y);
                if (Math.abs(x) > EPS) { // 避免除以0
                    candidates.add(y / x);
                }
                if (Math.abs(y) > EPS) { // 避免除以0
                    candidates.add(x / y); // 除法不满足交换律
                }
                
                // 对于每一种运算结果,构造新的牌组并进行下一层递归
                // 关键:创建一个副本,保证回溯时状态不受影响
                List<Double> newCards = new ArrayList<>(cards);
                // 先移除索引较大的 j,防止 i 的索引发生变化
                newCards.remove(j); 
                
                for (double candidate : candidates) {
                    // 用运算结果替换掉 i 位置的牌
                    newCards.set(i, candidate);
                    // 进行递归调用
                    if (dfs(newCards)) {
                        // 如果找到了解,立即返回 true,实现“短路”
                        return true;
                    }
                }
            }
        }
        
        // 如果所有组合都尝试完毕,仍未找到解,则返回 false
        return false;
    }
}

时空复杂度

时间复杂度:O(1)

  1. 计算依据:这个问题的输入规模是固定的,永远都是4张牌。
  2. 分析
    • 第一次递归 (n=4):从4张牌中选2张,有 C(4,2) = 6 种组合。每种组合最多产生6种运算结果。所以最多有 36 次进入下一次递归。
    • 第二次递归 (n=3):从3张牌中选2张,有 C(3,2) = 3 种组合。每种组合最多产生6种运算结果。所以最多有 18 次进入下一次递归。
    • 第三次递归 (n=2):从2张牌中选2张,有 C(2,2) = 1 种组合。最多产生6种运算结果。
    • 总的计算路径数量是一个与输入值无关的常数。虽然这个常数可能比较大,但在 Big O 表示法中,它被认为是 O(1)。

综合分析
由于输入大小固定为4,整个算法的执行步骤数有一个固定的上限。因此,时间复杂度为 O(1)

空间复杂度:O(1)

  1. 递归栈深度:递归的深度是固定的。dfs(4) -> dfs(3) -> dfs(2) -> dfs(1)。最大深度为 3。
  2. 数据存储:在递归的每一层,都会创建一个 List 的副本。
    • 第一层 dfs(4): 列表大小为 4。
    • 第二层 dfs(3): 列表大小为 3。
    • 第三层 dfs(2): 列表大小为 2。
    • 由于递归深度和列表大小都有一个固定的上限,所以占用的总空间也是一个与输入值无关的常数

综合分析
算法所需的额外空间(递归栈 + 列表副本)有一个固定的上限。因此,空间复杂度为 O(1)

参考灵神

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xumistore

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值