Day25| 491.递增子序列、46.全排列、47.全排列II、332.重新安排行程、51.N皇后、37.解数独、回溯算法总结

2025-08-02

491.递增子序列

题目链接:491.递增子序列
文档讲解:代码随想录
状态:没做对,忽略了不能排序

思路

本题的关键也是去重,但是与往常不一样的是它无序,所以不能用之前的while方法去重。

使用set容器去重,此时又有一个选择:set为全局变量、set为局部变量

  1. set为全局变量:一直跟随递归进去,即答案中不会有相同的元素,需要不断pop
  2. set为局部变量:仅在树层中存在,符合去重逻辑,不需要pop

搞清楚set存在的位置,明确set容器基本用法,很简单了。

题解

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& nums,int start)
    {
        unordered_set<int> uset;
        if(path.size()>=2)
        {
            res.push_back(path);
        }
        for(int i = start;i<nums.size();i++)
        {
            if(!path.empty() && path[path.size()-1]>nums[i])continue;
            if(uset.find(nums[i])!=uset.end())continue;
            path.push_back(nums[i]);
            uset.insert(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums, 0);
        return res;
    }
};

46.全排列

题目链接:46.全排列
文档讲解:代码随想录
状态:轻松AC,但是回头看还是有点迷糊

思路

第一次遇到排列问题,很显然的不同点就是终止条件+每次for循环从0开始

例如:nums = [1,2,3]按理来说应当使用used数组,不能选取重复的数值,但是本题有一个前提不含重复数字

我整个活,使用set,问题转变为->不能含有相同的数字,很显然符合set去重的第一种用法(见上题)。

题解

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    unordered_set<int> uset;
    void backtracking(vector<int>& nums)
    {
        if(path.size() == nums.size())
        {
            res.push_back(path);
            return ;
        }
        for(int i = 0;i<nums.size();i++)
        {
            if(uset.find(nums[i])==uset.end())
            {
                path.push_back(nums[i]);
                uset.insert(nums[i]);
            }
            else continue;//注意!!!
            backtracking(nums);
            uset.erase(nums[i]);//注意set容器在全局变量中要pop,不然越来越大没得玩
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        backtracking(nums);
        return res;
    }
};

47.全排列II

题目链接:47.全排列II
文档讲解:代码随想录
状态:去重位置放错了,烦死人

思路

一个大杂烩问题:

  1. 有重复数字:树层去重
  2. 全排列问题:树深去重

本题的两个关键点:

  1. 给定的nums是一个可包含重复数字的序列,所以意味着set容器不可行,要使用used数组(后面多用这个,更广泛)
  2. while(i<nums.size()-1 && nums[i+1] == nums[i])i++这个树层去重逻辑会影响i的值,进而影响used[i] = false。故一定要放在最后!!!这个找了好久好久!!!

题解

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& nums,vector<bool> used)
    {
        if(path.size() == nums.size())
        {
            res.push_back(path);
            return ;
        }
        for(int i = 0;i<nums.size();i++)
        {
            if(used[i])continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums,used);
            path.pop_back();
            used[i] = false;//!!!重点关注!!!
            while(i<nums.size()-1 && nums[i+1] == nums[i])i++;
            
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<bool> used(nums.size(), false);
        backtracking(nums,used);
        return res;
        
    }
};

332.重新安排行程–待做

51.N皇后–待做

37.解数独–待做


回溯算法总结

补了好多天的回溯,题目做的倒是OK,但总是套着模板做题,里面的递归细节傻傻搞不清,究其原因还是回溯的工程量太大,是无数个for循环的集合!

本章节主要使用回溯解决的问题:

回溯算法能解决如下问题:

  • 组合问题N个数里面按一定规则找出k个数的集合
  • 排列问题N个数按一定规则全排列,有几种排列方式
  • 切割问题:一个字符串按一定规则有几种切割方式
  • 子集问题:一个N个数的集合里有多少符合条件的子集
  • 棋盘问题N皇后,解数独等等

对于每类题目,写一下自己的总结感悟:

  1. 组合问题:最简单也是最经典的问题,for循环遍历树层,回溯遍历树深,收割想要的结果;关键点在于每次递归进入的for循环起点要不断后移;对于优化:可以针对sumk等值进行剪枝操作。
  2. 排列问题:相比组合问题:每次进入回溯for循环都是从0开始,并且有了树深去重(灵活使用used数组或set容器),对于多点去重:树深+树层,注意搞清逻辑,不要混乱。
  3. 切割问题:切割问题主要是看收集条件以及把for循环遍历分割位置(字符串切割)搞清即可。
  4. 子集问题:子集说实话是组合的一种,不要和排列搞混。就是收集条件放宽了而已。
  5. 棋盘问题:还在仰望······

回溯复杂度分析——二刷详细看

关于回溯算法的复杂度分析在网上的资料鱼龙混杂,一些所谓的经典面试书籍不讲回溯算法,算法书籍对这块也避而不谈,感觉就像是算法里模糊的边界。

所以这块就说一说Carl的个人理解,对内容持开放态度,集思广益,欢迎大家来讨论!

以下在计算空间复杂度的时候我都把系统栈(不是数据结构里的栈)所占空间算进去。

子集问题分析

  • 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n)
  • 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n)

排列问题分析

时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。 空间复杂度:O(n),和子集问题同理。

组合问题分析

时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。 空间复杂度:O(n),和子集问题同理。

N皇后问题分析

时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!)n!表示n * (n-1) * .... * 1。 空间复杂度:O(n),和子集问题同理。

解数独问题分析

时间复杂度:O(9^m) , m.的数目。 空间复杂度:O(n^2),递归的深度是n^2

一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!

最后放一个总结图片:

chart

图片来源于 知识星球-莫非毛

终于补完了所有落下的,回归正常刷题了。