【代码随想录】二刷-回溯算法

news2024/11/17 0:01:49

回溯算法

  • 《代码随想录》

  • 什么是回溯算法?
    • 回溯算法也可以叫做回溯搜索法,它是一种搜索方式。
    • 回溯是递归的副产品,只要有递归就会有回溯。
  • 回溯法的效率:
    • 回溯法的本质是穷举,穷举所有可能,然后选出我们想要的答案。(n层for循环嵌套)
    • 如果想让回溯法更高效一些,可以加一些剪枝操作,但也无法改变回溯法就是穷举的本质。
  • 回溯法一般可以解决如下几种问题:
    • 组合问题: N个数里面按一定规则找出K个数的集合
    • 切割问题: 一个字符串按一定规则由于几种切割方式
    • 子集问题: 一个N个数的集合里有多少符合条件的子集
    • 排列问题: N个数按一定规则全排列,有几种排列方式。
    • 棋盘问题: N皇后,解数独等
  • 如何理解回溯法?
    • 回溯法解决的都可以抽象为树型结构。
    • 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度构成了树的深度。
    • 递归要有终止条件,所以必然是一棵高度有限的树(N叉树)
  • 回溯模板
    • for循环横向遍历,递归 纵向遍历,回溯不断调整结果集。
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    // for循环-横向遍历
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归-纵向遍历
        回溯,撤销处理结果
    }
}
  • 性能分析
    • 组合问题分析
      • 时间复杂度: O ( n ∗ 2 n ) O(n* 2^n) O(n2n)
        • 组合问题其实就是一种子集问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。
      • 空间复杂度: O ( n ) O(n) O(n)
        • 和子集问题同理。
    • 子集问题分析
      • 时间复杂度: O ( n ∗ 2 n ) O(n * 2^n) O(n2n)
        • 每种元素状态无非选与不选,所以时间复杂度为&O(2^n)$;
        • 构造每一组子集都需要填进数组,又需要 O ( n ) O(n) O(n);
        • 所以最终时间复杂度为: O ( n ∗ 2 n ) O(n*2^n) O(n2n)
      • 空间复杂度: O ( n ) O(n) O(n)
        • 递归深度为n,所以系统栈所用空间为 O ( n ) O(n) O(n)
        • 每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传引用,并不会重新申请内存。
    • 排列问题
      • 时间复杂度: O ( n ! ) O(n!) O(n!)
        • 这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * … 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:result.push_back(path)),该操作的复杂度为 O ( n ) O(n) O(n)
        • 所以,最终时间复杂度为:n * n!,简化为 O ( n ! ) O(n!) O(n!)
      • 空间复杂度: O ( n ) O(n) O(n)
        • 和子集问题同理。

77. 组合

  • 回溯法的经典题目
  • 图解如下图所示:

class Solution {
public:
    vector<int>path;// 选取的组合
    vector<vector<int>>ret;// 最终结果
    // 用startIndex来记录下一层递归搜索的起始位置
    // 防止重现重复的组合
    void backtracking(int n ,int k ,int startIndex){
        if(path.size() == k){
            ret.push_back(path);// 收集结果
            return ;
        }
        for(int i = startIndex;i <= n;i++){// 横向遍历
            path.push_back(i);// 添加
            backtracking(n,k,i+1);// 纵向遍历
            path.pop_back();// 撤销
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return ret;
    }
};
  • 剪枝优化,将for循环控制条件改为
class Solution {
public:
    vector<int>path;// 选取的组合
    vector<vector<int>>ret;// 最终结果
    // 用startIndex来记录下一层递归搜索的起始位置
    // 防止重现重复的组合
    void backtracking(int n ,int k ,int startIndex){
        if(path.siz)e() == k){
            ret.push_back(path);// 收集结果
            return ;
        }
        // 如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){ // i为本次搜索的起始位置
            path.push_back(i);// 添加
            backtracking(n,k,i+1);// 纵向遍历
            path.pop_back();// 撤销
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return ret;
    }
};

216.组合总和III

  • 模板题,在上一题的基础上,在收集结果的时候增加是否等于目标值的判断。
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(int k, int n,int startIndex){
        if(path.size() == k){
            // 满足指定和才收集
            if(accumulate(path.begin(),path.end(),0) == n)ret.push_back(path);
            return ;
        }
        for(int i = startIndex;i <= 9;i++){
            path.push_back(i);
            backtracking(k,n,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return ret;
    }
};
  • 优化剪枝
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(int k, int n,int startIndex){
        if(path.size() == k){
            // 满足指定和才收集
            if(accumulate(path.begin(),path.end(),0) == n)ret.push_back(path);
            return ;
        }
        for(int i = startIndex;i <= 9-(k-path.size())+1;i++){// 剩余元素个数不足了,终止
            path.push_back(i);
            if(accumulate(path.begin(),path.end(),0) > n){// 已经大于目标值了,剪掉
               path.pop_back();
               return;
            }
            backtracking(k,n,i+1); 
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return ret;
    }
};

17. 电话号码的字母组合

  • 注意题目的意思是,根据这几个数字所对应的字母进行组合。几个数字,每个组合就有几个元素。
  • 从每个数字对应的元素中取一个。然后组合。
  • 与上面题的不同,本体每一个数字代表的是不同的组合,也就是求不同集合之间的组合,而上面两道题,都是都同一个集合中的组合。
class Solution {
public:
    string path;
    vector<string>ret;
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    void backtracking(const string& digits,int index){
        if(index == digits.size()){// 每个数字对应的字母都取了一个,收集。
            ret.push_back(path);;
            return;
        }
        int digit = digits[index]-'0';//将对应char转为int
        string letters = letterMap[digit];
        for(int i = 0; i< letters.size();i++){
            path.push_back(letters[i]);
            backtracking(digits,index+1);
            path.pop_back();
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0)return ret;
        backtracking(digits,0);
        return ret;
    }
};
  • 隐藏回溯细节
    • 增加参数s,每次修改仅在调用递归函数传参时修改,递归结束返回回来,原值并未被修改。从而达到回溯效果。
class Solution {
public:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
    vector<string>ret;
    void backtracking(const string& digits,string s,int index){// 增加参数string
        if(index == digits.size()){
            ret.push_back(s);
            return  ;
        }
        int digit = digits[index]-'0';
        string leeters = letterMap[digit];
        for(int i =0; i < leeters.size();i++){
            backtracking(digits,s+leeters[i],index+1);// 回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size() == 0)return ret;
        string s;
        s.clear();
        backtracking(digits,s,0);
        return ret;
    }
};

39. 组合总和

  • 注意题目要求,所有元素不限制选取次数。在我们实际的代码中,要修改模板的控制下标。
>-  方法1: 需要排序,因为按顺序取,需要判断是否超出目标值,超过则终止当前层的选取——剪枝。
class Solution {
public:
    vector<vector<int>>ret;
    vector<int>path;
    void backtracking(vector<int>& candidates,int target,int startIndex){
        if(accumulate(path.begin(),path.end(),0) == target){
            ret.push_back(path);
            return ;
        }
        for(int i = startIndex; i < candidates.size();i++){
            path.push_back(candidates[i]);
            if(accumulate(path.begin(),path.end(),0) > target){// 在这里终止,因为是排序过了,后面的只会更大。所以终止选取。
                path.pop_back();
                return;
            }
            backtracking(candidates,target,i);// 不加1,重复当前值
            path.pop_back();
        }
        // 或者
        // for(int i = startIndex; i < candidates.size() && accumulate(path.begin(),path.end(),0) < target;i++){
        //     path.push_back(candidates[i]);
        //     backtracking(candidates,target,i);
        //     path.pop_back();
        // }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());// 注意排序
        backtracking(candidates,target,0);
        return ret;
    }
};
  • 方法2: 无序排序,递归后,收集结果前判断,是否超过目标,超过目标值终止计算,返回,继续选取下一个值。因为没排序,后面可能还有符合条件的。注意对比与方法1终止条件位置的不同。
class Solution {
public:
    vector<vector<int>>ret;
    vector<int>path;
    void backtracking(vector<int>& candidates,int target,int startIndex){
        if(accumulate(path.begin(),path.end(),0) > target){// 超过目标值则回退,不要再选啦。
            return ;
        }
        if(accumulate(path.begin(),path.end(),0) == target){
            ret.push_back(path);
            return ;
        }
        for(int i = startIndex; i < candidates.size();i++){
            path.push_back(candidates[i]);
            backtracking(candidates,target,i);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates,target,0);
        return ret;
    }
};
  • 错误提示:
    • 参数startIndex,传递的时候不同于上面的两题,而是传入当前收集的值的下标,i,不是i+1,为了继续重复选取当前元素。
    • 开始我每次传的都是0,这样会造成重复的组合。

40.组合总和II

  • 在上一题的基础上增加去重
  • 去重1: 使用startIndex去重
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>& candidates,int target,int stratIndex){
        if(accumulate(path.begin(),path.end(),0) == target){
            ret.push_back(path);
            return  ;
        }
        for(int i = stratIndex;i < candidates.size();i++){
            // 去重
            if(i > stratIndex && candidates[i] == candidates[i-1])continue;
            path.push_back(candidates[i]);
            if(accumulate(path.begin(),path.end(),0) > target){
                path.pop_back();
                return;
            }
            backtracking(candidates,target,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0);
        return ret;
    }
};
  • 使用used数组去重——下面的90题子集II有更详细的去重解释。
    • 同一个树枝上的元素可以重复,同一个树层上的元素不可以重复。去的就是同一个树层的重。
    • 详见代码中的注释。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>& candidates,int target,int stratIndex,vector<bool>&used){
        if(accumulate(path.begin(),path.end(),0) == target){
            ret.push_back(path);
            return  ;
        }
        for(int i = stratIndex;i < candidates.size();i++){
            // 去重,跳过同一层使用过的元素
            // used[i-1] == true,表明同一个树枝candiates[i-1]使用过——联想往下纵向遍历。
            // used[i-1] == false,表明同一个树层candiates[i-1]使用过——联想往右横向遍历。
            // 为什么? used[i-1] == false;
            // 因为,同一树层,used[i-1] == false才能表示,当前取的candidates[i]是从candidates[i-1]回溯而来。
            // used[i-1] == true 说明进入下一层递归,向下纵向遍历,同一个树枝上使用过啦。

            // 与前一个元素与当前元素相等,并且在同一个树枝使用过,去掉。
            if(i > 0 && candidates[i] == candidates[i-1] &&
                used[i-1] == false)continue;

            path.push_back(candidates[i]);
            if(accumulate(path.begin(),path.end(),0) > target){
                path.pop_back();
                return;
            }
            used[i] = true;// 树枝使用了,遍历
            backtracking(candidates,target,i+1,used);
            used[i] = false;
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        vector<bool>used(candidates.size(),false);
        backtracking(candidates,target,0,used);
        return ret;
    }
};

131.分割回文串

  • 可以说是回溯模板题,只是收集结果方式有所不同,判断从某一位置截取到当前位置的字串是否为回文字串, 是则收集。
class Solution {
public:
    // 判断是否为回文子串-双指针
    bool isPalindrome(const string& s,int start,int end){
        for(int i = start,j = end; i < j;i++,j--){
            if(s[i] != s[j])return false;
        }
        return true;
    }
    vector<vector<string>>ret;
    vector<string>path;
    void backtracking(const string& s, int startIndex){
        if(startIndex >= s.size()){// 收集一轮结果
            ret.push_back(path);
            return ;
        }
        for(int i = startIndex;i < s.size();i++){
            // 想象了一下搜索过程,每次递归刚进入先选一个字母,直到最后,然后开始返回,倒数第二次选两个,依次回溯调整选取状态...其中要判断每次选取的子串是否为回文子串。
            if(isPalindrome(s,startIndex,i)){// 是回文字串
                string str  = s.substr(startIndex,i-startIndex+1);
                path.push_back(str);// 收集
            }else continue;//不是回文字串
            backtracking(s,i+1);// 切割的地方不能重复切割,所以传入i+1
            path.pop_back();// 回溯
        }
    }
    vector<vector<string>> partition(string s) {
        backtracking(s,0);
        return ret;
    }
};
  • 优化,提前计算出某一个子串是否是回文字串。dp思想。
  • 具体来说, 给定一个字符串s, 长度为n, 它成为回文字串的充分必要条件是s[0] == s[n-1]且s[1:n-1]是回文字串。
class Solution {
public:
    vector<vector<string>>ret;
    vector<string>path;
    vector<vector<bool>>isPalindrome;
    void backtracking(const string& s, int startIndex){
        if(startIndex >= s.size()){// 收集一轮结果
            ret.push_back(path);
            return ;
        }
        for(int i = startIndex;i < s.size();i++){
            if(isPalindrome[startIndex][i]){// 是回文字串
                string str  = s.substr(startIndex,i-startIndex+1);
                path.push_back(str);// 收集
            }else continue;//不是回文字串
            backtracking(s,i+1);
            path.pop_back();// 回溯
        }
    }
    // 计算每个子串是否为回文字串
    void computePalindrome(const string& s){
           isPalindrome.resize(s.size(),vector<bool>(s.size(),false));
           for(int i = s.size()-1;i >= 0;i--){
               for(int j = i;j < s.size();j++){
                   if(j ==i)isPalindrome[i][j] = true;
                   else if(j - i == 1)isPalindrome[i][j] = (s[i] == s[j]);
                   else isPalindrome[i][j] = (s[i]==s[j] && isPalindrome[i+1][j-1]);
               }
           }
    }
    vector<vector<string>> partition(string s) {
        computePalindrome(s);
        backtracking(s,0);
        return ret;
    }
};

93.复原IP地址

  • 同上切割问题,可以使用回溯搜索法把所有可能性搜出来。
class Solution {
public:
    vector<string>ret;
    bool isValid(const string& s,int start,int end){
        if(start > end)return false;
        if(s[start] == '0' && start != end)return false;// 前导0,不合法
        int num = 0;
        for(int i = start;i <= end;i++){
            if(s[i] > '9' || s[i] < '0'){// 非数字
                return false;
            }
            // 从前往后遍历一个数转成整型
            num = num* 10 + (s[i]-'0');
            if(num >  255)return false;
        }
        return true;
    }
    void backtracking(string& s,int startIndex,int pointCount){
        if(pointCount == 3){
            if(isValid(s,startIndex,s.size()-1)){// 判断最后一段是不是合法的
                ret.push_back(s);// 在原字符串上修改
            }
            return ;
        }
        for(int i = startIndex;i < s.size();i++){
            if(isValid(s,startIndex,i)){// 判断这个区间子串是否合法
                s.insert(s.begin()+i+1,'.');// 在当前这个数字后面插入一个.
                pointCount++;
                backtracking(s,i+2,pointCount);// 注意,新加了一个点,所以要加2,到下一个数字
                pointCount--;
                s.erase(s.begin()+i+1);// 删除插入的点
            }else break;//不合法直接结束
        }
    }
    vector<string> restoreIpAddresses(string s) {
        if(s.size() < 4 || s.size() > 12)return ret;// 剪枝
        backtracking(s,0,0);
        return ret;
    }
};

78.子集

  • 方法1: 使用一个used数组来标记是否使用当前元素,0-暂时不考虑,1-使用,2-不使用。
class Solution {
public:
    vector<int>used;// 记录是否使用过
    vector<vector<int>>ret;
    void backtracking(vector<int>& nums,int startIndex){
        if(startIndex == nums.size()){// 到达边界出,开始收集结果
            vector<int>path;
            for(int i = 0; i <  nums.size();i++){
                if(used[i] == 1)path.push_back(nums[i]);
            }
            ret.push_back(path);
            return;
        }
        // startIndex当前位置
        used[startIndex] = 1;// 选当前位置
        backtracking(nums,startIndex+1);
        used[startIndex] = 0;// 恢复成未考虑状态

        used[startIndex] = 2;// 不选当前位置
        backtracking(nums,startIndex+1);
        used[startIndex] = 0;// 恢复成未考虑状态
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        used.resize(nums.size(),0);
        backtracking(nums,0);
        return ret;
    }
};
  • 方法2: 相对于方法2来说,方法1更直观一些。
    • 可结合下图理解,当前元素,选与不选的这个过程。
    • startIndex既是边界,也是当前要操作的元素。
    • 方法1,是通过使用used数组来标记当前元素(startIndex下标所对应的元素)选还是不选,最后到达边界,遍历原数组,统一收集结果path,在放入最终结果集ret中。
    • 方法2,则是将选的元素直接收集进path数组中,不选了,也就是回溯,再将其pop出来,此时不断移动边界startIndex直至到达边界。到达边界后,再将小数组path放进结果集ret中。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>& nums,int startIndex){
        ret.push_back(path);// 收集结果
        if(startIndex == nums.size())return ;// 此终止条件可以省略
        for(int i = startIndex;i < nums.size(); i++){
            path.push_back(nums[i]);// 选上当前元素
            backtracking(nums,i+1);// 递归
            path.pop_back();// 回溯-不选当前元素啦
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backtracking(nums,0);
        return ret;
    }
};

90. 子集 II

  • 开始没想明白这跟上面那个题有什么区别,我说上面那个没说不让重复不也没重吗,其实不对,因为上面那个给的数据就不是重复的,所以求子集没重复,这题给的数据出现重复的元素了,所以就需要增加一个去重操作。
  • 相当于40题组合总和II中,我们使用的used数组,是一个意思,但是我觉得那样的意思容易让人弄混,容易直译数组名used的含义,所以我们这里将其改为int型数组,1表示当前树枝上使用了,2表示当前树层上使用了,0表示暂未考虑。
    • 上方划线文字作废,就按照true表示当前树枝使用过,false表示当前树层使用过就行。
  • 如下方图解所示,理解起来稍微有一点抽象,可以画图走一遍,下图仅给出第一次选1的分支。

  • 仔细想一下,其实不难。
    • 就是先确定一点,即,这个树枝上第一个元素,我们称为"父亲",然后再往后选剩下的每一个数,即"孩子"
    • 重复的情况就是,当前这个元素上当前这个元素和前一个元素相同,并且前一个元素不是父亲元素(i > 0),——说明,当前父亲和孩子的组合,出现过了,就是重复啦,去掉。
      • 那你可能就问了,那后面也是重复的吗,当然,再往后选孩子的孩子,也是重复的。因为在上一个"树枝"已经选取过了。
  • 如下图所示意:

class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    vector<bool>used;
    void backtracking(vector<int>&nums,int startIndex){
        ret.push_back(path);// 注意本题收集结果的方式,每次递归都要收集结果。
        if(startIndex == nums.size())return;// 这行可以不加
        for(int i = startIndex;i < nums.size();i++){
            // i > 0防止越界
            if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false)continue;// 去重
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums,i+1);
            used[i] = false;
            path.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());// 排序,用于去重
        used.resize(nums.size(),false);
        backtracking(nums,0);
        return ret;
    }
};
  • 方法2: 使用下标去重,看起来更简单些,但思想同使用used数组。此方法更贴切本题中第二张图。
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>&nums,int startIndex){
        ret.push_back(path);
        for(int i = startIndex;i < nums.size();i++){
            if(i > startIndex && nums[i] == nums[i-1])continue;// 去重
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());// 排序,用于去重
        backtracking(nums,0);
        return ret;
    }
};
  • 方法3: 使用set去重,去重判断同理。不做过多解释。
    • 有趣的一点也是重要的一点是,注意,是先判断是出现过,然后在往set里emplace,这步就相当于使用used数组中的。可以对照理解一下。是一个意思。
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false)continue;// 去重
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>&nums,int startIndex){
        ret.push_back(path);
        unordered_set<int>set;
        for(int i = startIndex;i < nums.size();i++){
            if(set.find(nums[i]) != set.end())continue;// 去重
            path.push_back(nums[i]);
            set.emplace(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());// 排序,用于去重
        backtracking(nums,0);
        return ret;
    }
};

491.递增子序列

  • 本题无法像上一题那样简单的使用used数组(bool数组)进行去重。
  • 因为并不是简单的对比前一个元素,因为前一个元素不一定就可以放进去,也就是说如果(可以的话)仍使用上面used数组去重方式,逻辑会更加复杂。
  • 方法1: 使用set去重,同上题使用set的方法,如下图所示,不理解就跟着走一下。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>& nums,int startIndex){
        // 题目要求,递增要求,至少有两个元素
        if(path.size() > 1)ret.push_back(path);
        unordered_set<int>set;
        for(int i = startIndex;i < nums.size();i++){
            if(!path.empty() && nums[i] < path.back() ||
                set.find(nums[i]) != set.end())continue;
            set.emplace(nums[i]);
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            // 为什么这里不用将set中的元素弹出来,因为我们每次递归进来都会创建一个set。是局部变量。详情见上图所示
            // 即每个set只负责本层。
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums,0);
        return  ret;
    }
};
  • 方法2: 使用用数组模拟哈希表来去重
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    void backtracking(vector<int>& nums,int startIndex){
        // 题目要求,递增要求,至少有两个元素
        if(path.size() > 1)ret.push_back(path);
        int used[201] = {0};// 数据范围-100到+100
        for(int i = startIndex;i < nums.size();i++){
            if(!path.empty() && nums[i] < path.back() ||
                used[nums[i]+100] == 1)continue;
            used[nums[i]+100] = 1;
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums,0);
        return  ret;
    }
};

46. 全排列

  • 相对于上面使用used数组去重,本题会更好理解些,我们使用used数组,仅仅来记录当前元素是否被使用过,而且不需要indexStart来记录每层递归开始的位置,而是每次都从头开始,从而收集所有的排列方式。
  • 主要的一点是,不会给重复的数,以此为去重问题,见下题。
  • 给上面的补充,写在这里,每一层,是指对应树枝的下面的每一层,不是整体的每一层。
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    vector<bool>used;// 记录当前元素是否使用过,true使用过,false没使用过
    void backtracking(vector<int>& nums){
        if(path.size() == nums.size()){
            ret.push_back(path);
            return ;
        }
        for(int i = 0; i < nums.size() ;i++){
            if(used[i] == true)continue;// 该元素已经使用过啦
            path.push_back(nums[i]);
            used[i] = true;
            backtracking(nums);
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        used.resize(nums.size(),false);
        backtracking(nums);
        return ret;
    }
};

47.全排列 II

  • 在上一题的基础上,给出重复的元素,需要增加去重。类似于上面求子集问题中的去重。
    • 在去重逻辑中,当前元素与前一位相等,并且前一位对应used为false,去掉。
    • 还是求子集问题中的去重道理。不过可以更简单的理解为,重复的数,以其为选中基准,重新排列组合出来的结果也是相同的(因为顺序固定了)。所以要去掉。——树层去重
    • 先选中的这个数为下面树层的根。

  • 方法1: 树层去重
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    vector<bool>used;// 用来记录是否使用过
    void backtracking(vector<int>& nums){
        if(path.size() == nums.size()){
            ret.push_back(path);
            return ;
        }
        for(int i = 0; i < nums.size();i++){
            if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false)continue;
            if(used[i] == false){// 当前元素没使用,那就选上
                used[i] = true;// 使用
                path.push_back(nums[i]);
                backtracking(nums);// 递归
                used[i] = false;// 回溯
                path.pop_back();
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        used.resize(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums);
        return ret;
    }
};
  • 方法2: 树枝去重
    • 即,对于树的每一个树枝来说,当我们确定了一个树层的根后,下面不可以再选与其相同的子根。

// 只需修改一步,将false改为true即可
// 如果理解了上面的我说的层的概念,这个树枝去重会很好理解。
if(i > 0 && nums[i] == nums[i-1] && used[i-1] == true)continue;
  • 树层去重和树枝去重对比

  • 可以很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位的去重虽然最后可以得到答案,但是做了很多无用搜索。

  • 方法3: 使用set去重
class Solution {
public:
    vector<int>path;
    vector<vector<int>>ret;
    vector<bool>used;// 用来记录是否使用过
    void backtracking(vector<int>& nums){
        if(path.size() == nums.size()){
            ret.push_back(path);
            return ;
        }
        unordered_set<int>set;
        for(int i = 0; i < nums.size();i++){
            if(set.find(nums[i]) != set.end())continue;// 使用过了
            if(used[i] == false){// 当前元素没使用,那就选上
                used[i] = true;
                set.insert(nums[i]);
                path.push_back(nums[i]);
                backtracking(nums);
                used[i] = false;
                path.pop_back();
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        used.resize(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums);
        return ret;
    }
};
  • 注意:
    • 使用set去重的版本相对于used数组的版本效率会低很多。
    • 因为频繁的对unordered_set进行insert操作,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。

332. 重新安排行程

  • 就是看能不能把所有票都用上。走完全程。
class Solution {
public:
    // 某一个机场可以到达的机场都有哪些,航班次数为这样的航班有几个。
    // 出发机场,<到达机场,航班次数>
    unordered_map<string,map<string,int>>targets;
    vector<string>ret;// 收集结果,沿途经过的机场数
    bool backtracking(int ticketNum){
        // ret.size()当前经过的机场数量 == 航班数量+1
        // 就是将所有票都用上了,终止。
        if(ret.size() == ticketNum + 1)return true;// 当前票都使用完毕
    
        //每次遍历的时候拿到上一个降落的机场,遍历这个机场接下来可以往哪飞
        for(pair<const string,int>& target : targets[ret[ret.size()-1]]){// 注意传引用,修改second值,const-因为map中的key不可修改。
            if(target.second > 0){// 
                ret.push_back(target.first);
                target.second--;
                if(backtracking(ticketNum))return true;// 找到了终止,就不pop出来了
                ret.pop_back();// 回溯
                target.second++;
            }
        }
        return false;
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        for(const vector<string>& vec: tickets){// 记录映射关系-当前机场都能到哪个机场,能到达几次
            targets[vec[0]][vec[1]]++;// 计数,使用了当前票--,避免死循环。
        }
        ret.push_back("JFK");// 起始机场
        backtracking(tickets.size());
        return ret;
    }
};

第51题. N皇后

  • 时间复杂度: O ( n ! ) O(n!) O(n!)
  • 空间复杂度: O ( n ) O(n) O(n),和子集问题同理
  • 皇后们的要求
    • 不能同行
    • 不能同列
    • 不能同斜线,45 & 135
class Solution {
public:
    vector<vector<string>>ret;
    vector<string>chess_borad;
    void backtracking(int n,int row){// n为棋盘大小,row为当前到第几行
        if(row == n){// 每行都确定了皇后的位置
            ret.push_back(chess_borad);
            return ;
        }
        for(int col = 0; col < n;col++){
            if(isValid(row,col,n)){// 当前位置合法
                chess_borad[row][col] = 'Q';// 放置皇后
                backtracking(n,row+1);
                chess_borad[row][col] = '.';// 撤销皇后
            }
        }
    }

    // 判断当前位置放入皇后,是否合法
    bool isValid(int row,int col,int n){
        // 检查列
        for(int i = 0; i < row;i++){
            if(chess_borad[i][col] == 'Q')return false;
        }

        // 为什么这里没有单独检查行呢,因为递归的时候就是在调整行,递归一下,切换一下行。

        // 45度——也就是往右上判断,为什么不用判断左下判断,因为还没到下面的行。
        // 135度判断同理
        for(int i = row - 1,j = col + 1 ;i >=0 && j < n;i--,j++){
            if(chess_borad[i][j] == 'Q')return false;
        }

        for(int i = row - 1,j = col - 1; i >= 0 && j >= 0;i--,j--){
            if(chess_borad[i][j] == 'Q')return false;
        }
        return true;
    }
    vector<vector<string>> solveNQueens(int n) {
        chess_borad.resize(n,string(n,'.'));
        backtracking(n,0);
        return ret;
    }
};

37. 解数独

  • 时间复杂度: O ( 9 m ) O(9^m) O(9m),m是’.'的数目
  • 空间复杂度: O ( n 2 ) O(n^2) O(n2),递归的深度是 O ( n 2 ) O(n^2) O(n2)
  • 判断棋盘是否合法
    • 同行是否重复
    • 同列是否重复
    • 9宫格里是否重复
  • 就是一个萝卜一个坑,理论上来说有几个空白位置就会递归几层。递归只是为了找到坑位,不会再向后移动,搜寻别的坑。详见下方注释。
class Solution {
public:
    bool isValid(int row,int col,char val,vector<vector<char>> &board){
        for(int i = 0; i < 9;i++){// 检查同行是否有重复
            if(board[row][i] == val)return false;
        }
        for(int i = 0;i < 9;i++){// 检查同列是否有重复
            if(board[i][col] == val)return false;
        }
        
        // 检查九宫格内是否有重复
        int startRow = (row / 3)*3;//定位每个九宫格左上角位置
        int startCol = (col / 3)*3;
        for(int i = startRow;i < startRow + 3;i++){
            for(int j = startCol;j < startCol + 3;j++){
                if(board[i][j] == val)return false;
            }
        }
        return true;
    }
    bool backtracking(vector<vector<char>>& board){
        // 有趣的一点,我们发现在这里并没有控制下标访问,因为我们将某个空白位置放上了数字,board[i][j]就不为'.'啦,递归时就不会覆盖上一层所放的位置
        // 递归的时候会不断找到空的位置,尝试放入合法的数。也就是说每次层递归,都确定一个空位置被放入一个合法的数。
        // 就是一个萝卜一个坑,理论上来说有几个空白位置就会递归几层。递归只是为了找到坑位,不会再向后移动,搜寻别的坑。
        // 当某一层递归中,某一个空白放哪个数字都不行,return false,回溯,尝试将上一层所放置的位置换成别的,继续调整。
        for(int i = 0; i < 9;i++){
            for(int j = 0; j < 9;j++){
                if(board[i][j] == '.'){// 是空白
                    for(char k = '1' ;k <= '9';k++){// 遍历,放置合适的数字
                        if(isValid(i,j,k,board)){// 可以放
                            board[i][j] = k;
                            if(backtracking(board))return true;// 可以了就终止
                            board[i][j] = '.';
                        }
                    }
                    return false;// 九个数都是了,放哪个都不行,return false
                }
            }
        }
        return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

  • 这个回溯拖得时间还是比较长了,抓紧抓紧!Orz

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/73192.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何使用virtualenv实现python环境的隔离?

有关更多的Python 开发内容,可访问:《 Python Flask开发指南》​​​​​​​ virtualenv可以帮助我们来创建一个Python的虚拟环境,虚拟环境可以独立并隔离外部的python环境,方便我们对于不同项目使用不同的python依赖,已经依赖版本的不同而导致的错误。本篇文章主要来了解…

2022全年度冰箱十大热门品牌销量榜单

自2022年年初以来&#xff0c;各地纷纷部署支持包括冰箱在内的家电大宗消费政策措施&#xff0c;其中家电补贴政策是刺激家电消费的主旋律。宏观经济政策环境改善&#xff0c;利好冰箱行业的长期发展。 根据鲸参谋数据统计&#xff0c;今年京东平台冰箱的年度累计销量达到1400多…

论文阅读笔记《Learning Combinatorial Solver for Graph Matching》

核心思想 本文提出一种基于学习的组合求解器来实现图匹配。之前基于学习的图匹配方法都是利用神经网络提取特征构建关联矩阵&#xff0c;然后再利用可微分的Sinkhorn算法求解匹配矩阵。但本文提出的方法没有显式的构建关联矩阵和求解匹配矩阵的过程&#xff0c;而是将其转化成关…

SpringBoot:核心模块盘点

spring-boot-project 是 spring-boot 核心技术包&#xff0c;其中包含了 spring-boot 所有基础源码&#xff0c;其中很多模块都是我们了解 spring-boot 的重点。 &#xff5e; 本篇内容包括&#xff1a;spring-boot-project 包介绍、Spring Boot 核心模块 文章目录一、spring-b…

SpringCloud-Geteway之限流,熔断(超详细篇)

目录 一&#xff0c;Sentinel--服务容错 1.1 高并发带来的问题 1. 使用压测工具,对请求进行压力测试 2.修改配置文件中tomcat的并发数 1.2 服务雪崩效应 1.3 常见容错方案 1.4 Sentinel入门 什么是Sentinel 微服务集成Sentinel 安装Sentinel控制台 实现一个接口的限…

QTextLine、QGlyphRun、QTextLayout

QGlyphRun 一、描述 此类提供对字体中内部字形的直接访问。在某些情况下&#xff0c;开发人员可以对特定字体中的字形绘制到屏幕上进行更低级的控制。 当Qt显示以Unicode编码的文本字符串时&#xff0c;它会将Unicode点转换为基于字体的字形索引列表和位置列表。QGlyphRun 提…

企业对于源代码加密需求分析

需求 随着企业信息化发展的日益增长&#xff0c;软件行业厂商之间的竞争也愈加白热化&#xff0c;加上国内对知识产权的不够重视、山寨模仿产品的横行。保护源代码、保证企业的核心竞争力&#xff0c;成为众多软件研发企业的第一要务。那么企业应该如何保证源代码的安全呢&…

[附源码]计算机毕业设计常见Web漏洞对应PC应用系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

HTTP缓存机制(Cache-Control)

文章目录HTTP缓存机制HTTP缓存机制 HTTP 协议通常应用于分布式信息系统&#xff0c;所谓分布式信息系统&#xff0c;是指以计算机网络为基础&#xff0c;将系统的数据与功能分别布置在不同的地方&#xff0c;然后再通过网络将数据与功能连接的信息系统。由于系统需要处理大量的…

世界杯期间我使用Python生成二维码“为中国队辟谣”

二维码介绍 二维码本质上&#xff0c;就是一段字符串&#xff0c;我们可以把任意字符串&#xff0c;制作成一个二维码图片。在生活中&#xff0c;使用二维码更多的是一个URL&#xff08;网址&#xff09;。 引入 qrcode库 qrocde库网址&#xff1a;qrcode PyPI 进入网址之后…

[附源码]计算机毕业设计贷款申请审核管理系统论文Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

本地字节序与网络字节序的相互转换(IP地址、端口号)

一般数据在内存中是按照字节存储的&#xff0c;存储的方式分为大端和小端。在不知道对方主机的存储方式的情况下&#xff0c;我们不知道是否需要转换数据的存储方式。因此&#xff0c;TCP/IP协议规定&#xff1a;发送到网络的数据流应采用大端字节序&#xff01; 如果当前主机…

年产3000吨原味奶糖生产车间工艺设计

目 录 摘 要 I Abstract II 1绪论 1 1.1原味奶糖的概念及其功能特性 1 1.2国外原味奶糖的现状 3 1.3我国的原味奶糖现状及开发前景 4 1.4原味奶糖原料的生产情况 4 2工艺流程设计 6 2.1 原味奶糖的生产工艺 6 2.1.1 溶糖与混合 7 2.1.2 连续真空薄膜熬糖 7 2.1.3 混合 8 2.1.4 …

阿里国际站-唤端技术的探索与演进

作者&#xff1a;黄昭 阿里ICBU买家增长技术团队 近几年用户增长领域APP推广发展迅猛&#xff0c;而唤端就是其中的重要技术。通过唤端拉新/促活不仅能能够将三方流量规模做大&#xff0c;同时二方流量也能有效利用&#xff0c;给网站不断带来商机&#xff0c;本文将为大家详细…

UE4虚幻引擎关于事件分发器的使用!

UE4虚幻引擎关于事件分发器的使用&#xff01; 首先&#xff0c;这次就讲一下&#xff0c;事件分发器&#xff0c;在UI&#xff0c;Pawn&#xff0c;子actor如何进行事件传递&#xff01; 首先&#xff01;我们如果要使用事件分发器的原因是什么&#xff01; 第一&#xff0…

Django(11):后台管理系统

目录后台管理系统基本操作管理平台的基本操作数据模型的注册和管理数据模型管理后台管理系统操作Xadmin管理平台Django内置了自己的后台管理系统&#xff0c;包含数据库管理和良好的界面。后台管理系统基本操作 在我们前面创建Django项目时&#xff0c;默认会有许多应用组件&a…

一次nginx “time out”故障排查

研发请求协助排查一个nginx故障。 描述如下&#xff1a; 在内部环境测试没问题。 打包到生产环境后&#xff0c;访问nginx接口地址超时。 研发比对后怀疑是nginx版本不一致导致。内部版本1.23.2&#xff0c;生产环境1.23.1. 收到信息第一感觉不是nginx的问题。不过先测试一…

五年测试经验,从二线到一线,下定决心一定要转自动化测试

一七年毕业以来自己工作也快三年左右了&#xff0c;自己最开始其实是想找java开发的&#xff0c;大学的毕设也是用jsp写的一个简单的web网站&#xff0c;无奈面试总被刷而且遇到了大量的培训机构。于是乎就转入软件测试这个行业。没想到啊转眼间一干就三年多了。 第一家公司 …

基于风驱动算法优化的lssvm回归预测-附代码

基于风驱动算法优化的lssvm回归预测 - 附代码 文章目录基于风驱动算法优化的lssvm回归预测 - 附代码1.数据集2.lssvm模型3.基于风驱动算法优化的LSSVM4.测试结果5.Matlab代码摘要&#xff1a;为了提高最小二乘支持向量机&#xff08;lssvm&#xff09;的回归预测准确率&#xf…

物联网开发笔记(57)- 使用Micropython开发ESP32开发板之控制ESP32-CAM摄像头

一、目的 这一节我们学习如何使用我们的ESP32开发板来控制ESP32-CAM摄像头。 二、环境 ESP32 ESP32-CAM摄像头 Thonny IDE 几根杜邦线 ESP32-CAM是一个开发板&#xff0c;它上面集成了Camera摄像头模块&#xff0c;可以直接用Micropython进行操作。从MicroPython官网下载的固…