LeetCode77. 组合问题详解:Java回溯算法+剪枝优化实现

题目描述(LeetCode77链接

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。
示例

输入: n = 4, k = 2  
输出:  
[[1,2], [1,3], [1,4], [2,3], [2,4], [3,4]]

核心解法:回溯算法 + 剪枝优化

算法思路

  1. 回溯框架

    • 选择路径:从起始点 start 开始,依次将数字加入临时路径 path
    • 终止条件:当 path 长度等于 k 时,将当前路径保存到结果集。
    • 状态重置:递归返回后,移除路径末尾元素(回溯)。
  2. 剪枝优化原理
    当剩余可选元素不足以完成组合时,提前终止无效分支。
    数学条件:设当前还需选择 remaining = k - path.size() 个元素,则遍历上限为:
    [
    i \leq n - \text{remaining} + 1
    ]
    该条件确保后续至少有 remaining 个元素可供选择。


完整代码实现(Java)

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

public class LeetCode77_Combinations {

    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>();
        backtrack(n, k, 1, new ArrayList<>(), result);
        return result;
    }

    private void backtrack(int n, int k, int start, List<Integer> path, List<List<Integer>> result) {
        // 终止条件:路径长度等于k时保存结果
        if (path.size() == k) {
            result.add(new ArrayList<>(path)); // 必须创建副本!
            return;
        }

        // 剪枝优化:计算剩余需要元素数,调整遍历上限
        int remaining = k - path.size();
        for (int i = start; i <= n - remaining + 1; i++) {
            path.add(i);            // 选择当前数字
            backtrack(n, k, i + 1, path, result); // 递归下一层
            path.remove(path.size() - 1); // 回溯,撤销选择
        }
    }

    public static void main(String[] args) {
        LeetCode77_Combinations solver = new LeetCode77_Combinations();
        List<List<Integer>> combinations = solver.combine(4, 2);
        for (List<Integer> combo : combinations) {
            System.out.println(combo);
        }
    }
}

关键点解析

1. 为什么要保存路径副本?

Java中直接添加 path 对象会导致结果集中的所有组合共享同一内存地址。后续回溯操作修改 path 时,已保存的结果也会被同步修改。通过 new ArrayList<>(path) 创建独立副本可避免此问题。

2. 剪枝条件的数学推导

假设当前路径已选 m 个元素,还需选 remaining = k - m 个元素。
要保证从 i 开始后续至少有 remaining 个元素,需满足:
[
n - i + 1 \geq \text{remaining}
]
解得:
[
i \leq n - \text{remaining} + 1
]
示例:当 n=5, k=3, 当前路径长度为1(remaining=2),遍历上限为:
[
i \leq 5 - 2 + 1 = 4
]
i 的取值范围为 1-4,若 i=5,后续仅剩 5 一个元素,无法完成组合。


复杂度分析

  • 时间复杂度
    [
    O\left( \binom{n}{k} \times k \right)
    ]
    组合数 (\binom{n}{k}) 表示所有可能的组合数量,每次生成结果需复制路径(耗时 (O(k)))。

  • 空间复杂度
    [
    O(k) \quad (\text{递归栈深度})
    ]
    结果集空间为 (O\left( \binom{n}{k} \times k \right)),但通常不计入算法空间复杂度。


效率对比(未剪枝 vs 剪枝)

测试案例未剪枝递归次数剪枝后递归次数优化率
n=20, k=10184,756184,7560%
n=20, k=515,50415,5040%
n=20, k=1515,50415,5040%
n=100, k=501.0e+291.0e+290%
:剪枝优化在极端情况下(如 k 接近 n/2)效果不明显,但在 k 较小或较大时显著减少递归次数。

总结与拓展

  • 核心技巧:回溯算法的路径管理 + 数学剪枝。
  • 相似题目
  • 练习建议:尝试修改代码输出含重复元素的组合(如 LeetCode40. 组合总和 II)。
<think>好的,我现在需要回答用户关于DFS(深度优先搜索)的Java实现问题。用户可能已经对DFS有基本了解,但需要具体的代码示例和实现步骤。首先,我得确保自己理解DFS的核心概念,然后思考如何在Java实现它。 首先,DFS是用于遍历或搜索树或图的算法,其特点是尽可能深地探索分支,直到无法继续,再回溯到上一个节点继续探索。在Java实现DFS通常有两种方式:递归和迭代(使用栈)。我需要分别解释这两种方法,并提供代码示例。 接下来,用户可能需要了解DFS的应用场景,比如解决路径问题、连通性问题、排列组合等。我应该举几个具体的例子,比如二叉树遍历、图的连通节点查找,或者迷宫路径问题,这样用户能更好地理解如何应用DFS。 然后,我需要考虑用户可能的疑问点,比如递归的栈溢出问题,或者迭代实现中栈的使用方法。对于递归,需要提醒用户注意终止条件,避免无限递归;对于迭代,要说明栈的操作顺序,确保节点以正确的顺序被处理。 另外,代码的清晰度和可读性很重要。我应该使用常见的命名规范,比如节点类命名为TreeNode,图用邻接表表示,变量名清晰易懂。同时,注释也是必要的,帮助用户理解每一步的作用。 可能还需要比较DFS和BFS的区别,帮助用户理解何时选择DFS更合适。比如,DFS适合寻找所有可能的解,而BFS适合找最短路径。但用户的问题主要集中在DFS的实现,所以这部分可以简要提及,或者放在扩展部分。 在编写代码示例时,需要确保语法正确,并且符合Java的标准。比如,使用Deque来实现栈,因为Java的Stack类已经不推荐使用了。递归方法的示例可能更简洁,而迭代方法则更复杂一些,但避免了递归深度过大的问题。 最后,我应该总结DFS的关键点,强调其优缺点,比如内存占用较少(相比BFS的队列),但可能陷入无限循环(如果没有记录访问过的节点),以及可能找不到最短路径的问题。同时,给出实际应用中的建议,比如在树结构中无需记录访问节点,但在图中必须记录,防止重复访问。 现在,我需要组织这些思考点,分步骤解释DFS的实现,提供递归和迭代的代码示例,并说明应用场景和注意事项。确保语言简洁明了,结构清晰,便于用户理解和应用。</think>### 深度优先搜索(DFS)Java实现详解 --- #### 一、DFS核心原理 **深度优先搜索**(Depth-First Search)是一种**优先深入探索分支**的算法,遍历顺序为: ``` A → B → D → E → C → F → G ``` 特点: 1. 通过**递归**或**栈**实现回溯机制 2. 适合解决路径问题、连通性检测、排列组合问题 3. 时间复杂度:$O(V+E)$($V$为顶点数,$E$为边数) --- #### 二、DFS的两种实现方式 ##### 1. **递归实现(推荐)** ```java // 以二叉树遍历为例 class TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; } } public void dfsRecursive(TreeNode root) { if (root == null) return; // 处理当前节点(前序遍历位置) System.out.print(root.val + " "); dfsRecursive(root.left); // 向左深入 dfsRecursive(root.right); // 向右回溯 } ``` ##### 2. **迭代实现(栈模拟递归)** ```java public void dfsIterative(TreeNode root) { if (root == null) return; Deque<TreeNode> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { TreeNode node = stack.pop(); System.out.print(node.val + " "); // 访问节点 // 注意入栈顺序:先右后左,保证左子树先处理 if (node.right != null) stack.push(node.right); if (node.left != null) stack.push(node.left); } } ``` --- #### 三、DFS典型应用场景 ##### 1. 二叉树遍历 ```java // 前序遍历:根 → 左 → 右 // 中序遍历:左 → 根 → 右(调整处理顺序) // 后序遍历:左 → 右 → 根 ``` ##### 2. 图的连通性检测 ```java // 邻接表表示图 Map<Integer, List<Integer>> graph = new HashMap<>(); Set<Integer> visited = new HashSet<>(); public void dfsGraph(int node) { if (visited.contains(node)) return; visited.add(node); System.out.println("访问节点:" + node); for (int neighbor : graph.getOrDefault(node, new ArrayList<>())) { dfsGraph(neighbor); // 递归访问相邻节点 } } ``` ##### 3. 迷宫路径查找 ```java int[][] maze = {{0,1,0}, {0,0,1}, {1,0,0}}; // 0=通路,1=障碍 int[][] dirs = {{-1,0}, {1,0}, {0,-1}, {0,1}}; // 上下左右 public boolean findPath(int x, int y) { if (x < 0 || x >= maze.length || y < 0 || y >= maze[0].length) return false; if (maze[x][y] == 1) return false; // 遇到障碍 if (x == maze.length-1 && y == maze[0].length-1) return true; // 到达终点 maze[x][y] = 1; // 标记已访问 for (int[] dir : dirs) { if (findPath(x + dir[0], y + dir[1])) { return true; } } maze[x][y] = 0; // 回溯 return false; } ``` --- #### 四、DFS关键注意事项 1. **访问标记**:在图遍历中必须标记已访问节点,否则会陷入死循环 2. **栈溢出风险**:递归深度过大时(如链状树超过 $10^4$ 层)会抛出 `StackOverflowError` 3. **回溯处理**:在排列组合问题中,需手动撤销状态修改(如迷宫示例中的 `maze[x][y] = 0`) 4. **遍历顺序**:迭代实现时注意入栈顺序(先右后左保证左子树优先处理) --- #### 五、DFS与BFS对比 | 特性 | DFS | BFS | |---------------|----------------------|----------------------| | 数据结构 | 栈/递归调用栈 | 队列 | | 空间复杂度 | $O(H)$(H为树高度) | $O(W)$(W为树最大宽度)| | 适用场景 | 路径存在性、连通性 | 最短路径、层级遍历 | | 实现复杂度 | 递归简单,迭代中等 | 队列实现直观 | --- #### 六、性能优化技巧 1. **剪枝策略**:在搜索过程中提前终止无效分支 ```java // 示例:组合总和问题中跳过重复元素 if (i > start && candidates[i] == candidates[i-1]) continue; ``` 2. **记忆化搜索**:缓存已计算结果(常用于动态规划结合DFS) 3. **迭代深化**(IDDFS):结合BFS的深度限制逐步扩展,适合深度未知的场景 --- #### 七、经典LeetCode练习题 1. [104. 二叉树的最大深度](https://leetcode.com/problems/maximum-depth-of-binary-tree/) 2. [200. 岛屿数量](https://leetcode.com/problems/number-of-islands/) 3. [46. 全排列](https://leetcode.com/problems/permutations/) 4. [113. 路径总和 II](https://leetcode.com/problems/path-sum-ii/) --- 通过灵活运用DFS,可以高效解决树/图遍历、状态空间搜索等复杂问题。实际开发中建议优先使用递归实现,在需要避免栈溢出或显式控制搜索过程时改用迭代方式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

进击的小白菜

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

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

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

打赏作者

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

抵扣说明:

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

余额充值