一、491. 递增子序列
题目描述
给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素。数组中可能包含重复元素,如 [4,6,7,7] 的结果需去重。
解题思路(与 90. 子集 II 的核心区别)
- 相同点:都需要去重,避免重复结果;
- 不同点 :
-
- 子集 II 可以排序去重,但本题不能排序(排序会破坏原数组的递增子序列顺序);
- 本题去重需在每一层递归中用哈希集合记录已使用的元素,避免同一层选重复元素;
- 核心约束:子序列必须递增(当前元素 ≥ 上一个选中的元素)。
-
java
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Solution {
private List<List<Integer>> result;
private List<Integer> path;
public List<List<Integer>> findSubsequences(int[] nums) {
result = new ArrayList<>();
path = new ArrayList<>();
backtrack(nums, 0);
return result;
}
private void backtrack(int[] nums, int startIndex) {
// 收集条件:子序列长度≥2
if (path.size() >= 2) {
result.add(new ArrayList<>(path));
// 不终止,继续递归(找更长的递增子序列)
}
// 每层递归用Set去重:记录当前层已选过的元素
Set<Integer> used = new HashSet<>();
for (int i = startIndex; i < nums.length; i++) {
// 剪枝条件:
// 1. 当前元素 < 路径最后一个元素(不满足递增);
// 2. 当前层已选过该元素(去重);
if ((!path.isEmpty() && nums[i] < path.get(path.size() - 1)) || used.contains(nums[i])) {
continue;
}
used.add(nums[i]); // 标记当前层已选该元素
path.add(nums[i]); // 选择
backtrack(nums, i + 1); // 递归:下一个元素从i+1开始
path.remove(path.size() - 1); // 回溯
}
}
}
关键解释
- 不能排序的原因 :比如原数组
[4,7,6],排序后会生成[4,6,7],但原数组中不存在该递增子序列,违反题意; - 层内去重 :每一层用
Set记录已选元素(如[4,6,7,7]中,当遍历到第二个 7 时,因第一层已选过 7,直接跳过); - 递增约束 :
nums[i] >= path.get(path.size()-1)保证子序列递增(路径为空时直接选)。
复杂度分析
- 时间复杂度:O(n×2n),每个元素有选 / 不选两种可能,共 2n 种情况,每个结果复制时间 O(n);
- 空间复杂度:O(n),递归栈深度 + 路径空间。
二、46. 全排列
题目描述
给定一个不含重复数字的数组 nums,返回其所有可能的全排列。
解题思路(与组合 / 子集的核心区别)
- 组合 / 子集 :用
startIndex控制遍历起点,避免重复选取(如选了 1 就不再回头选); - 排列 :无顺序要求,每个元素都可被选(只要未被使用),因此不用 startIndex ,改用
used数组标记已选元素。
java
import java.util.ArrayList;
import java.util.List;
class Solution {
private List<List<Integer>> result;
private List<Integer> path;
private boolean[] used; // 标记元素是否被使用
public List<List<Integer>> permute(int[] nums) {
result = new ArrayList<>();
path = new ArrayList<>();
used = new boolean[nums.length];
backtrack(nums);
return result;
}
private void backtrack(int[] nums) {
// 终止条件:路径长度 == 数组长度(生成一个完整排列)
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
// 单层递归:遍历所有元素(无startIndex,因为排列可回头选)
for (int i = 0; i < nums.length; i++) {
if (used[i]) { // 元素已被使用,跳过
continue;
}
used[i] = true; // 标记已使用
path.add(nums[i]); // 选择
backtrack(nums); // 递归
path.remove(path.size() - 1); // 回溯
used[i] = false; // 取消标记
}
}
}
关键解释
- 无 startIndex :排列需要考虑顺序,比如
[1,2]和[2,1]是不同排列,因此每次递归都要从数组第一个元素开始遍历; - used 数组 :核心作用是避免同一排列中重复选取同一个元素(如
[1,1]不是合法排列)。
复杂度分析
- 时间复杂度:O(n×n!),n! 是排列总数,每个排列复制时间 O(n);
- 空间复杂度:O(n),
used数组 + 递归栈 + 路径空间。
三、47. 全排列 II
题目描述
给定一个包含重复数字 的数组 nums,返回其所有不重复的全排列。
解题思路(40. 组合总和 II + 46. 全排列)
- 核心逻辑 :
- 排序:让重复元素相邻,方便去重;
used数组双重作用:used[i] = true:标记元素已被选入当前排列;used[i-1] = false:标记同一层的前一个重复元素未被选(去重);
- 去重条件:
i > 0 && nums[i] == nums[i-1] && !used[i-1](或used[i-1] == true,需理解两种写法的区别)。
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
class Solution {
private List<List<Integer>> result;
private List<Integer> path;
private boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
result = new ArrayList<>();
path = new ArrayList<>();
used = new boolean[nums.length];
Arrays.sort(nums); // 排序:让重复元素相邻
backtrack(nums);
return result;
}
private void backtrack(int[] nums) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 剪枝条件:
// 1. 元素已被使用;
// 2. 同一层重复元素(去重);
if (used[i] || (i > 0 && nums[i] == nums[i-1] && !used[i-1])) {
continue;
}
used[i] = true;
path.add(nums[i]);
backtrack(nums);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
关键解释(used[i-1] 的两种写法)
!used[i-1](推荐):表示同一层的前一个重复元素未被选,跳过当前元素(去重);used[i-1]:表示同一分支的前一个重复元素已被选,跳过当前元素(也能去重,但剪枝效率稍低);- 核心:排序后,只有当重复元素的前一个被使用时,当前元素才可选(避免同一层选重复元素)。
复杂度分析
- 时间复杂度:O(n×n!),排序时间 O(nlogn),整体仍为 O(n×n!);
- 空间复杂度:O(n),同全排列。
四、高难度回溯题(思路解析,一刷了解即可)
1. 332. 重新安排行程
- 核心思路 :回溯 + 贪心(优先选字典序小的机场),用
Map<String, PriorityQueue<String>>存储航班映射,避免重复路径; - 注意:LeetCode 后台数据修改后,纯回溯可能超时,需结合欧拉路径优化。
2. 51. N 皇后
- 核心思路 :
- 按行递归(每行放一个皇后),列 / 对角线剪枝;
- 剪枝条件:
- 列冲突:同一列已有皇后;
- 对角线冲突:行差 == 列差(左上 - 右下)或 行和 == 列和(右上 - 左下);
- 用
char[][]模拟棋盘,回溯放置 / 移除皇后。
3. 37. 解数独
- 核心思路 :
- 二维递归(遍历每个空格),尝试填入 1-9;
- 剪枝条件:同一行 / 列 / 3x3 小方格已有该数字;
- 找到一个解后立即返回(数独唯一解),无需收集所有结果。
五、回溯算法核心总结(关键差异对比)
| 题型 | 核心特点 | 去重方式 | 遍历控制 |
|---|---|---|---|
| 组合(如 77) | 无顺序,不重复选取 | 无需去重(数组无重复) | startIndex |
| 组合总和 II | 无顺序,数组有重复 | 排序 + 层内跳过重复元素 | startIndex |
| 子集 | 收集所有节点,无顺序 | 同组合 | startIndex |
| 子集 II | 收集所有节点,数组有重复 | 排序 + 层内跳过重复元素 | startIndex |
| 递增子序列 | 收集递增节点,数组有重复 | 层内 Set 去重(不能排序) | startIndex |
| 全排列 | 有顺序,可重复选取(全局) | 无需去重(数组无重复) | used 数组(无 startIndex) |
| 全排列 II | 有顺序,数组有重复 | 排序 + used 数组层内去重 | used 数组 |
| N 皇后 / 解数独 | 二维递归,多条件剪枝 | 无去重,剪枝冲突条件 | 二维遍历 |
java
void backtrack(参数) {
if (终止条件) {
收集结果;
return;
}
for (遍历当前层所有可能) {
剪枝(去重/不满足条件);
选择(加入路径);
backtrack(参数); // 递归
回溯(移除路径);
}
}
总结
- 核心差异 :组合 / 子集用
startIndex控制顺序,排列用used数组标记已选元素,递增子序列用层内Set去重(不能排序); - 去重关键 :数组有重复时,排序后层内跳过重复元素(子集 II / 全排列 II),不能排序时用层内
Set(递增子序列); - 高难度题:N 皇后 / 解数独需二维递归 + 多条件剪枝,一刷了解思路即可,二刷重点突破。