【Hot100】9.回溯算法

这一类的核心思想是:暴力搜索的优化版。通过“试错”来寻找所有可能的解。

  • 核心模板
    void backtrack(路径, 选择列表) {
        if (满足结束条件) {
            结果集.add(路径);
            return;
        }
        for (选择 : 选择列表) {
            做选择;
            backtrack(路径, 选择列表);
            撤销选择; // ⚠️ 关键:回溯,恢复状态
        }
    }
    
  • 关键要素
    • 路径 (track):已经做出的选择。
    • 选择列表:当前可以做的选择。
    • 结束条件:到达决策树底层,无法再做选择。
    • 剪枝:提前排除不可能的分支,提高效率。
    • 去重:处理数组中有重复元素的情况(排序 + used 数组或索引判断)。

📝 本类题型总结

题目 核心技巧 关键差异点 (vs 子集)
78. 子集 基础回溯 基准:每个节点都收集。start 控制不回头。
46. 全排列 used 数组 used 标记。结束条件是长度满。可回头选(只要没用过)。
39. 组合总和 可复选 + 剪枝 递归传 i (可复选)。sum > target 剪枝。
40. 组合总和 II 不可复选 + 去重 递归传 i+1排序 + i>start && nums[i]==nums[i-1] 去重
131. 分割回文 切割线 + 判断 start 是切割起点。需判断子串是否回文。
51. N 皇后 二维回溯 + 校验 按行递归。放置前需检查 列、对角线 冲突。

🏆 母题:LeetCode 78. 子集 (Subsets)

题目内容:给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
核心逻辑

  1. 决策树:每个元素都有“选”或“不选”两种可能。
  2. 收集时机每一个节点(包括根节点)都是一个合法的子集,所以进入递归函数第一件事就是把当前路径加入结果集。
  3. 控制重复:通过 start 参数控制,只向后选,不向前选,避免重复组合(如 [1,2][2,1] 视为同一个)。
import java.util.*;

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> track = new LinkedList<>();
    
    public List<List<Integer>> subsets(int[] nums) {
        backtrack(nums, 0);
        return res;
    }
    
    void backtrack(int[] nums, int start) {
        // ⚠️ [核心逻辑] 前序位置:每个节点都是一个子集,直接加入结果
        res.add(new ArrayList<>(track));
        
        for (int i = start; i < nums.length; i++) {
            // 做选择
            track.add(nums[i]);
            // 递归:i+1 表示不能重复选当前元素,只能选后面的
            backtrack(nums, i + 1);
            // 撤销选择
            track.removeLast();
        }
    }
}

🔄 变体 1:LeetCode 46. 全排列 (Permutations)

题目内容:给定一个不含重复数字的数组 nums ,返回其所有可能的全排列。
相比母题的变化

  1. 结束条件:当路径长度等于数组长度时,说明所有元素都用过了,收集结果。
  2. 选择列表:不能使用 start 索引,因为可以回头选前面的元素(只要没被用过)。
  3. 标记数组:需要 boolean[] used 数组来标记哪些元素已经在当前路径中。
import java.util.*;

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> track = new LinkedList<>();
    boolean[] used;
    
    public List<List<Integer>> permute(int[] nums) {
        used = new boolean[nums.length];
        backtrack(nums);
        return res;
    }
    
    void backtrack(int[] nums) {
        // ⚠️ [变化点 1] 结束条件:路径长度等于数组长度
        if (track.size() == nums.length) {
            res.add(new ArrayList<>(track));
            return;
        }
        
        for (int i = 0; i < nums.length; i++) {
            // ⚠️ [变化点 2] 剪枝:如果已使用,跳过
            if (used[i]) continue;
            
            // 做选择
            track.add(nums[i]);
            used[i] = true;
            
            backtrack(nums);
            
            // 撤销选择
            used[i] = false;
            track.removeLast();
        }
    }
}

🔄 变体 2:LeetCode 39. 组合总和 (Combination Sum)

题目内容:给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合。
注意candidates 中的数字可以 无限制重复被选取
相比母题的变化

  1. 结束条件sum == target 收集结果;sum > target 剪枝返回。
  2. 递归参数:传递 i 而不是 i+1,因为当前元素可以重复选
  3. 剪枝优化:可以先对数组排序,若 sum + candidates[i] > target,后续更大的数也不用看了,直接 break
import java.util.*;

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> track = new LinkedList<>();
    int sum = 0;
    
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 排序有助于剪枝
        Arrays.sort(candidates);
        backtrack(candidates, target, 0);
        return res;
    }
    
    void backtrack(int[] candidates, int target, int start) {
        // ⚠️ [变化点 1] 结束条件
        if (sum == target) {
            res.add(new ArrayList<>(track));
            return;
        }
        if (sum > target) return; // 剪枝
        
        for (int i = start; i < candidates.length; i++) {
            // ⚠️ [变化点 2] 剪枝:如果当前和已经超过 target,后面的更大,直接 break
            if (sum + candidates[i] > target) break;
            
            // 做选择
            track.add(candidates[i]);
            sum += candidates[i];
            
            // ⚠️ [变化点 3] 递归传 i:允许重复选当前元素
            backtrack(candidates, target, i);
            
            // 撤销选择
            sum -= candidates[i];
            track.removeLast();
        }
    }
}

🔄 变体 3:LeetCode 40. 组合总和 II (Combination Sum II)

题目内容:给定一个候选人编号的集合 candidates 和一个目标数 target ,找出所有唯一组合。candidates 中的每个数字在每个组合中 只能使用一次
注意:解集不能包含重复的组合(输入数组可能有重复数字)。
相比母题的变化

  1. 不可复选:递归传 i + 1
  2. 去重逻辑(核心难点)
    • 排序:让相同的数字相邻。
    • 树层去重:在同一层递归中,如果 candidates[i] == candidates[i-1]i > start,说明前一个相同的数在这一层已经用过了(或者被剪枝了),当前这个再用会产生重复组合,必须 continue
    • 树枝去重:如果是递归深层(i == start),即使相同也可以使用(因为是不同的位置)。
import java.util.*;

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> track = new LinkedList<>();
    int sum = 0;
    
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates); // ⚠️ [变化点 1] 必须排序,以便去重
        backtrack(candidates, target, 0);
        return res;
    }
    
    void backtrack(int[] candidates, int target, int start) {
        if (sum == target) {
            res.add(new ArrayList<>(track));
            return;
        }
        if (sum > target) return;
        
        for (int i = start; i < candidates.length; i++) {
            // ⚠️ [变化点 2] 剪枝
            if (sum + candidates[i] > target) break;
            
            // ⚠️ [变化点 3] 核心去重逻辑
            // i > start 保证是同一层(横向)的比较
            // 如果当前元素和前一个元素相同,且前一个元素在同一层还没被使用(或者说已经回溯了),则跳过
            if (i > start && candidates[i] == candidates[i - 1]) {
                continue;
            }
            
            track.add(candidates[i]);
            sum += candidates[i];
            
            // ⚠️ [变化点 4] 递归传 i+1:不可重复选
            backtrack(candidates, target, i + 1);
            
            sum -= candidates[i];
            track.removeLast();
        }
    }
}

🔄 变体 4:LeetCode 131. 分割回文串 (Palindrome Partitioning)

题目内容:给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
相比母题的变化

  1. 问题转化:切割问题。start 表示切割线的起始位置,i 表示切割线的结束位置。
  2. 判断逻辑:每次切割出的子串 s[start...i] 必须是回文串,才能继续递归。
  3. 结束条件start 到达字符串末尾。
import java.util.*;

class Solution {
    List<List<String>> res = new ArrayList<>();
    LinkedList<String> track = new LinkedList<>();
    
    public List<List<String>> partition(String s) {
        backtrack(s, 0);
        return res;
    }
    
    void backtrack(String s, int start) {
        // ⚠️ [变化点 1] 结束条件:切割线到了最后
        if (start == s.length()) {
            res.add(new ArrayList<>(track));
            return;
        }
        
        for (int i = start; i < s.length(); i++) {
            // ⚠️ [变化点 2] 剪枝:如果不是回文串,直接跳过
            if (!isPalindrome(s, start, i)) continue;
            
            String sub = s.substring(start, i + 1);
            track.add(sub);
            
            // 下一层从 i+1 开始切
            backtrack(s, i + 1);
            
            track.removeLast();
        }
    }
    
    // 辅助函数:判断子串是否回文
    private boolean isPalindrome(String s, int left, int right) {
        while (left < right) {
            if (s.charAt(left) != s.charAt(right)) return false;
            left++;
            right--;
        }
        return true;
    }
}

🔄 变体 5:LeetCode 51. N 皇后 (N-Queens)

题目内容:按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
相比母题的变化

  1. 二维回溯:通常按行遍历(每行放一个皇后),列作为选择列表。
  2. 合法性检查:放置前需检查:
    • 当前列是否有皇后。
    • 主对角线(左上到右下)是否有皇后。
    • 副对角线(右上到左下)是否有皇后。
  3. 结果格式化:将棋盘转换为 List<String>
import java.util.*;

class Solution {
    List<List<String>> res = new ArrayList<>();
    
    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        for (char[] row : board) Arrays.fill(row, '.');
        backtrack(board, 0);
        return res;
    }
    
    void backtrack(char[][] board, int row) {
        int n = board.length;
        // ⚠️ [变化点 1] 结束条件:行数超过最后一行
        if (row == n) {
            res.add(convertBoard(board));
            return;
        }
        
        for (int col = 0; col < n; col++) {
            // ⚠️ [变化点 2] 合法性检查
            if (!isValid(board, row, col)) continue;
            
            board[row][col] = 'Q';
            backtrack(board, row + 1); // 下一行
            board[row][col] = '.'; // 撤销
        }
    }
    
    private boolean isValid(char[][] board, int row, int col) {
        int n = board.length;
        // 检查列
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') return false;
        }
        // 检查主对角线 (左上)
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') return false;
        }
        // 检查副对角线 (右上)
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') return false;
        }
        return true;
    }
    
    private List<String> convertBoard(char[][] board) {
        List<String> list = new ArrayList<>();
        for (char[] row : board) {
            list.add(new String(row));
        }
        return list;
    }
}
posted @ 2026-03-08 21:55  scales123  阅读(45)  评论(0)    收藏  举报