【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 ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
核心逻辑:
- 决策树:每个元素都有“选”或“不选”两种可能。
- 收集时机:每一个节点(包括根节点)都是一个合法的子集,所以进入递归函数第一件事就是把当前路径加入结果集。
- 控制重复:通过
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 ,返回其所有可能的全排列。
相比母题的变化:
- 结束条件:当路径长度等于数组长度时,说明所有元素都用过了,收集结果。
- 选择列表:不能使用
start索引,因为可以回头选前面的元素(只要没被用过)。 - 标记数组:需要
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 中的数字可以 无限制重复被选取。
相比母题的变化:
- 结束条件:
sum == target收集结果;sum > target剪枝返回。 - 递归参数:传递
i而不是i+1,因为当前元素可以重复选。 - 剪枝优化:可以先对数组排序,若
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 中的每个数字在每个组合中 只能使用一次。
注意:解集不能包含重复的组合(输入数组可能有重复数字)。
相比母题的变化:
- 不可复选:递归传
i + 1。 - 去重逻辑(核心难点):
- 排序:让相同的数字相邻。
- 树层去重:在同一层递归中,如果
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 所有可能的分割方案。
相比母题的变化:
- 问题转化:切割问题。
start表示切割线的起始位置,i表示切割线的结束位置。 - 判断逻辑:每次切割出的子串
s[start...i]必须是回文串,才能继续递归。 - 结束条件:
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 的棋盘上,并且使皇后彼此之间不能相互攻击。
相比母题的变化:
- 二维回溯:通常按行遍历(每行放一个皇后),列作为选择列表。
- 合法性检查:放置前需检查:
- 当前列是否有皇后。
- 主对角线(左上到右下)是否有皇后。
- 副对角线(右上到左下)是否有皇后。
- 结果格式化:将棋盘转换为
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;
}
}

浙公网安备 33010602011771号