回溯算法深化 II

一、491. 递增子序列

题目描述

给你一个整数数组 nums,找出并返回所有该数组中不同的递增子序列,递增子序列中至少有两个元素。数组中可能包含重复元素,如 [4,6,7,7] 的结果需去重。

解题思路(与 90. 子集 II 的核心区别)
  • 相同点:都需要去重,避免重复结果;
  • 不同点
      1. 子集 II 可以排序去重,但本题不能排序(排序会破坏原数组的递增子序列顺序);
    1. 本题去重需在每一层递归中用哈希集合记录已使用的元素,避免同一层选重复元素;
    2. 核心约束:子序列必须递增(当前元素 ≥ 上一个选中的元素)。
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); // 回溯
        }
    }
}
关键解释
  1. 不能排序的原因 :比如原数组 [4,7,6],排序后会生成 [4,6,7],但原数组中不存在该递增子序列,违反题意;
  2. 层内去重 :每一层用 Set 记录已选元素(如 [4,6,7,7] 中,当遍历到第二个 7 时,因第一层已选过 7,直接跳过);
  3. 递增约束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; // 取消标记
        }
    }
}
关键解释
  1. 无 startIndex :排列需要考虑顺序,比如 [1,2][2,1] 是不同排列,因此每次递归都要从数组第一个元素开始遍历;
  2. used 数组 :核心作用是避免同一排列中重复选取同一个元素(如 [1,1] 不是合法排列)。
复杂度分析
  • 时间复杂度:O(n×n!),n! 是排列总数,每个排列复制时间 O(n);
  • 空间复杂度:O(n),used 数组 + 递归栈 + 路径空间。

三、47. 全排列 II

题目描述

给定一个包含重复数字 的数组 nums,返回其所有不重复的全排列。

解题思路(40. 组合总和 II + 46. 全排列)
  • 核心逻辑
    1. 排序:让重复元素相邻,方便去重;
    2. used 数组双重作用:
      • used[i] = true:标记元素已被选入当前排列;
      • used[i-1] = false:标记同一层的前一个重复元素未被选(去重);
    3. 去重条件: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] 的两种写法)
  1. !used[i-1](推荐):表示同一层的前一个重复元素未被选,跳过当前元素(去重);
  2. used[i-1]:表示同一分支的前一个重复元素已被选,跳过当前元素(也能去重,但剪枝效率稍低);
  3. 核心:排序后,只有当重复元素的前一个被使用时,当前元素才可选(避免同一层选重复元素)。
复杂度分析
  • 时间复杂度:O(n×n!),排序时间 O(nlogn),整体仍为 O(n×n!);
  • 空间复杂度:O(n),同全排列。

四、高难度回溯题(思路解析,一刷了解即可)

1. 332. 重新安排行程
  • 核心思路 :回溯 + 贪心(优先选字典序小的机场),用 Map<String, PriorityQueue<String>> 存储航班映射,避免重复路径;
  • 注意:LeetCode 后台数据修改后,纯回溯可能超时,需结合欧拉路径优化。
2. 51. N 皇后
  • 核心思路
    1. 按行递归(每行放一个皇后),列 / 对角线剪枝;
    2. 剪枝条件:
      • 列冲突:同一列已有皇后;
      • 对角线冲突:行差 == 列差(左上 - 右下)或 行和 == 列和(右上 - 左下);
    3. char[][] 模拟棋盘,回溯放置 / 移除皇后。
3. 37. 解数独
  • 核心思路
    1. 二维递归(遍历每个空格),尝试填入 1-9;
    2. 剪枝条件:同一行 / 列 / 3x3 小方格已有该数字;
    3. 找到一个解后立即返回(数独唯一解),无需收集所有结果。

五、回溯算法核心总结(关键差异对比)

题型 核心特点 去重方式 遍历控制
组合(如 77) 无顺序,不重复选取 无需去重(数组无重复) startIndex
组合总和 II 无顺序,数组有重复 排序 + 层内跳过重复元素 startIndex
子集 收集所有节点,无顺序 同组合 startIndex
子集 II 收集所有节点,数组有重复 排序 + 层内跳过重复元素 startIndex
递增子序列 收集递增节点,数组有重复 层内 Set 去重(不能排序) startIndex
全排列 有顺序,可重复选取(全局) 无需去重(数组无重复) used 数组(无 startIndex)
全排列 II 有顺序,数组有重复 排序 + used 数组层内去重 used 数组
N 皇后 / 解数独 二维递归,多条件剪枝 无去重,剪枝冲突条件 二维遍历
java 复制代码
void backtrack(参数) {
    if (终止条件) {
        收集结果;
        return;
    }
    for (遍历当前层所有可能) {
        剪枝(去重/不满足条件);
        选择(加入路径);
        backtrack(参数); // 递归
        回溯(移除路径);
    }
}

总结

  1. 核心差异 :组合 / 子集用 startIndex 控制顺序,排列用 used 数组标记已选元素,递增子序列用层内 Set 去重(不能排序);
  2. 去重关键 :数组有重复时,排序后层内跳过重复元素(子集 II / 全排列 II),不能排序时用层内 Set(递增子序列);
  3. 高难度题:N 皇后 / 解数独需二维递归 + 多条件剪枝,一刷了解思路即可,二刷重点突破。
相关推荐
Tisfy2 小时前
LeetCode 3453.分割正方形 I:二分查找
算法·leetcode·二分查找·题解·二分
漫随流水2 小时前
leetcode算法(101.对称二叉树)
数据结构·算法·leetcode·二叉树
源代码•宸2 小时前
Golang原理剖析(string面试与分析、slice、slice面试与分析)
后端·算法·面试·golang·扩容·string·slice
派森先生2 小时前
排序算法-冒泡排序
算法·排序算法
静心问道2 小时前
排序算法分类及实现
算法·排序算法
2301_764441332 小时前
python实现罗斯勒吸引子(Rössler Attractor)
开发语言·数据结构·python·算法·信息可视化
码农三叔2 小时前
(7-3)自动驾驶中的动态环境路径重规划:实战案例:探险家的行进路线
人工智能·算法·机器学习·机器人·自动驾驶
飞Link3 小时前
【Water】数据增强中的数据标注、数据重构和协同标注
算法·重构·数据挖掘
漫随流水3 小时前
leetcode算法(559.N叉树的最大深度)
数据结构·算法·leetcode·二叉树