8.回溯算法

1.回溯法模板

1.回溯函数模板返回值以及参数

回溯函数名:backtracking

返回值:一般为 void

1.参数:(1)要遍历哪个数组或者字符串(2)在下一层 for 循环中遍历的起始位置在哪

2.回溯函数终止条件

3.回溯搜索的遍历过程

模板

void backtracking(参数) { // 参数中要有一个值控制树的深度
    if (终止条件) {  // 这里控制 for 循环是几重 for 循环,并返回上一层 for 循环
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {  // 控制最外层 for 循环递归多少次
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

上面的代码还能进一步细化

vector<> res; // 用于存放最后结果
vector<> path; // 用于存放每一次最外层 for 循环产生的结果
void backtracking(参1:题目中的数组,参2:结束循环的变量,参3:第n层 for 循环从谁开始,也就是子数组的开始位置,是一直变化的) { // 参数中要有一个值控制树的深度
    if (终止条件) { 
        // 这里控制 for 循环是几重 for 循环,并返回上一层 for 循环
        存放结果; // 将哪些变量添加到 res 
        return;
    }

    for (int i = 子数组在哪开始;i<要对哪些数进行遍历,并进行相关剪枝操作;i++) {  // 控制最外层 for 循环递归多少次
        // TODO 该元素在 push 之前的相关操作
        path.push_back(i); // 将 i 添加到 path 中
        backtracking(参1:题目中的数组,参2:结束循环的变量,参3:第n层 for 循环从谁开始); // 递归
        // TODO 该元素在弹出之前的相关操作
        path.pop_back();
    }
}

**for 循环的 i++ 就是横向遍历树,每次递归 backtracking 就是纵向遍历树 **

1.path 的长度不是固定的

2.每次怎么选取元素

1.2 回溯问题分类

1.3 回溯问题的时空复杂度

1.3.1 子集问题的时空复杂度

时间复杂度:O(n*2^n)

image-20211028152520381

在求上面那个题子集的时候,就是求一个取不取的问题,完整的集合是 [1,2,3,4] 。每一个数都有取或者不取两种结果,选取一个子集的时间复杂度是 O(2^n),构造每一组子集都需要填进数组,又需要时间复杂度 O(n) ,所以最后时间复杂度是 O(nx2^n)

1.3.2 排列问题的时空复杂度

时间复杂度: O(n!)

在最外层 for 循环一共会遍历 n 个节点,这 n 个节点每向下一层, startIndex 的值传入是 i+1,在这一层遍历 n-1 次,再向下一层遍历 n-2 次

对于一个节点来说最终的遍历次数就是 (n-1)x(n-2)。。。,一共有 n 个节点, 最终结果是 O(n!)

1.3.3 组合问题的时空复杂度

时间复杂度:O(n*2^n)

组合问题其实是一个结果集合变长的选择问题,所以同选择问题一样,时间复杂度是 O(n*2^n)

空间复杂度:

1.4 回溯的画图技巧

S1:将集合放在树根

S2:不断的向下扩展,更新 startIndex 和 i 的值。

对于这个树来说横向看 i 的个数不断在 +1 ,纵向看 i 的值是不变的,就是 for 循环 i 的起始值。

有两种方法会退出此 backtracking :

①终止条件生效 return 的时候

这个时候会返回到调用他的那一个横条

②for 循环执行完终止

image-20220128153546292

2.LeetCode 相关题目

2.1_77组合

2.1.1 算法描述

下图是本回溯算法对应的栈

image-20211021221141727 image-20211021221204489

这里回溯思想体现的地方在于第一次当 i=1 时会跳进一个 for 循环,遍历 [2,3,4] 元素;遍历完成之后会再跳回 i=1的方法,也就是最外层的 for 循环执行剩余的方法 pop

image-20220128153546292 image-20211022162753447

在当前题目中:k 的元素为几就进行几重 for 循环往 path 中拼接值

2.1.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> result; // 存放符合条件的结果集合
    vector<int> path;  // 用来存放中间结果
    void backtracking(int n ,int k ,int startIndex){
        if(path.size()==k){
            result.push_back(path);
            return;
        }
        for(int i=startIndex;i<=n;i++){
            path.push_back(i);
            backtracking(n,k,i+1);
            path.pop_back(); // 回溯
        }
    }
public:
    vector<vector<int>> combine(int n, int k) {
        result.clear();
        path.clear();
        backtracking(n,k,1);
        return result;
    }
};

2.1.3 Python 代码实现

class Solution:
    def combine(self, n: int, k: int) -> List[List[int]]:
        res = [] # 用于存放最后结果
        path = []  
        def backtracking(n,k,start_index):
            if len(path) == k:
                res.append(path[:])
                return
            for i in range(start_index,n+1):
                path.append(i) 
                backtracking(n,k,i+1) # 递归
                path.pop() 
        backtracking(n,k,1)
        return res

2.1.4 剪枝操作

如果这个题改成 k=4 ,也就是子集合取 4 个值,那么结果就只剩下 [[1,2,3,4]] 这一种情况了,本来最外层 for 循环要循环 4 次,但是在第 2,3,4 次的时候就已经不满足条件了,所以可以通过剪枝,在 for 循环中限制循环的次数,不再进行 2,3,4 的操作。

image-20211022164059222

n:本来要处理 n 个数据,但是因为后面处理的数据没有意义了所以要减掉后面生成的数据

k-path.size() :还有多少个值没有处理

n-(k-path.size())+1:最后一次处理的是哪个值,这个值后面的元素都不用再处理了

比如对于 [1,2,3,4] 来说,第二轮应该再开始遍历 2 ,但是 2 其实是不用判断的,所以在 for 循环条件中

startIndex = 2,i<=4-(4-0)+1 这个时候 for 循环就不满足要求了

所以剪枝后的 for 循环就变为了:

for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方
            path.push_back(i); // 处理节点
            backtracking(n, k, i + 1);
            path.pop_back(); // 回溯,撤销处理的节点
        }

2.2_216组合总和3

这个题和上面那个题思路完全一样,只不过这里在 backtracking 的时候还要有进一步的判断,当 sums 不满足 n 的时候就不将 path 添加到 res 中

被遗忘的剪枝操作:

这里不仅仅在 for 循环中进行剪枝,当 sums>targetSums 也要进行剪枝

2.2.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> res ; 
    vector<int> path;
    void backtracking(int n,int k,int stratIndex,int sums){
        // 剪枝操作:sums 超出范围
        if(sums>n){
            return;
        }
        // 终止条件
        if(path.size()==k){
            if(sums==n) res.push_back(path);
            return;
        }
        for(int i =stratIndex;i<=9-(k-path.size())+1;i++){
            path.push_back(i);
            sums+=i;
            backtracking(n,k,i+1,sums);
            sums-=i; // 妙~ 在这一步减掉 sums 的值
            path.pop_back(); // 回溯
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        res.clear();
        path.clear();
        backtracking(n,k,1,0);
        return res;
    }
};

这里对 sums 进行减的操作还是很妙的

2.3_17 电话号码的字母组合

2.3.1 算法描述

首先先想如何用暴力的方式进行解决,将本题的手机按键进行简化,假设说现在只有两个字符串

string ss = {"abc","def"}

那么在遍历的时候就会有双重 for 循环

for (int i = 0;i<ss[0].size();i++){
	string tmp = "";
	tmp.push_back(ss[0][i]);
	for (int j = 0;j<ss[1].size();j++){
		tmp.push_back(ss[1][j]);
	}
}

字符串少还好说,但是如果字符串很多就要 n 重 for 循环,所以为了减少 for 循环的嵌套使用回溯的方式进行解决

1.参数:
(1)每一次要遍历的字符串,这里使用 k 代表进入了第几层 for 循环,也就是用哪个字符串
(2)下一层 for 循环的起始位置可以通过暴力方法进行推导,在暴力中第二层 for 循环是从 0 开始的
2.回溯的终止条件
当按完所有按键后循环终止
3.遍历过程,省略

image-20220210110757131

2.3.2 C++ 代码实现

class Solution {
public:
    vector<string> res;
    string tmp;
    unordered_map<char,string> container{{'1',""},{'2',"abc"},{'3',"def"},{'4',"ghi"},{'5',"jkl"},{'6',"mno"},{'7',"pqrs"},{'8',"tuv"},{'9',"wxyz"},{'*',"*"},{'#',"#"},{'0'," "}};
    void backtracking(string digits,int k){
        if(k==digits.size()){
            res.push_back(tmp);
        }
        string s = container[digits[k]];
        for(int i = 0;i<s.size();i++){
            tmp.push_back(s[i]);
            backtracking(digits,k+1);
            tmp.pop_back();
        } 

    }
    vector<string> letterCombinations(string digits) {
        if(digits=="") return res;
        backtracking(digits,0);
        return res;
    }
};

易错点:

1.这里的 index 用于判断当前处理的是 digits 中哪个元素,所以在下一层 backtracking 的时候应该将 index+1 传入下一个元素

2.对于 digits 进行判空操作

2.3.3 知识扩展

如何将 string 类型的数字转换为 int 类型

'2'-'0'=2

2.4_39组合总和

2.4.1 算法描述

image-20211024220727098

先将数组进行排序;

横向数组取值的时候不能包含上面的元素,但是纵向可以包含上面的重复元素

相关剪枝操作:

如果本层的 sum 也就是 sum+candidates[i] 已经大于 target ,就可以结束 for 循环遍历

image-20211024220921606

这个题的结束条件不一样,结束条件 sum==target 就结束对当前数字的判断

2.4.2 C++ 代码实现

class Solution {
private:
        // 用于存放最后结果的 res
        vector<vector<int>> res;
        // 用于存放中间变量
        vector<int> path;
        void backtracking(vector<int>& candidates,int target,int sums,int startIndex){ // 必传变量:题目数组,结束循环的变量,第二层 for 循环从谁开始
            // 结束循环:将结果添加进去+不将结果添加进去
            if(target==sums){
                res.push_back(path);
                return;
            }
            for(int i = startIndex;i<candidates.size()&&sums+candidates[i]<=target;i++){ // for 循环的三个位置感觉都是固定的
                sums+=candidates[i];
                path.push_back(candidates[i]);
                backtracking(candidates,target,sums,i); // 因为可以重复所以可以把 i 传进去
                // 做相关 pop 操作
                sums-=candidates[i];
                path.pop_back();
            }

        }    
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        sort(candidates.begin(),candidates.end()); // 需要排序
        backtracking(candidates,target,0,0);
        return res;
    }
};

易错点:

1.不要忘记给集合排序

​ 如果不排序很可能在一个前面就 return ,后面的数就不能判断了

2.backtracking 是否要传入 startIndex 的问题,这个值需要传多大

3.这里操作的是 candidates[i] 不直接值 i 了

2.4.3 相关知识

1.C++ 如何对数组排序

sort(candidates.begin(),candidates.end());
image-20211027165614088

sort 方法接收两个的迭代器,指向最初的指针和最后的指针。而 vector 对象的 .begin 和 .end 方法可以得到第一个元素和最后一个元素的迭代器

2.5_40 组合总和2

2.5.1 算法描述

这里的 index=0 是 value =1,index=1,value=1,两个值重复。在最外层 for 循环中,当对 index=0 判断了就不用再对 index=1 再进行判断了

去重去掉的是“同一树层上使用过的元素”

关键:当判断 index=0 之后的元素时,如果 cur 的值等于 cur-1 的值,则这个数直接跳过

if(i>startIndex&&candidates[i]==candidates[i-1]) continue;

这句话不会影响到后面的拼接,比如说 [1,2,2,2] 这里有 3 个 2 ,在对 2 计算完子集后不会再对后面两个 2 计算子集,但是第一个 2 子集的结果是包含后两个 2 的,因为这里只对 i-1 进行判断,对 i+1 是没有影响的

2.5.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& candidates,int target,int sums,int startIndex){
        if(sums==target){
            res.push_back(path);
            return;
        }
        for(int i =startIndex;i<candidates.size()&&sums+candidates[i]<=target;i++){
            if(i>startIndex&&candidates[i]==candidates[i-1]){ // 同一层树的相同元素
                continue;
            }
            sums+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sums,i+1);
            sums-=candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        res.clear();
        path.clear();
        sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0,0);
        return res;
    }
};

2.6 131分割回文串

input:"aab’’

output:[ [“aa”,“b”], [“a”,“a”,“b”] ]

回文串+回溯

2.6.1 算法描述

image-20211026161856858

需要注意的是 for 循环每次取的时候还是取一个字符,但是在判断回文的时候一次会放入多个字符

第一次指向 a ,剩余 ab

​ 第一层循环:指向 a(第二个),这时候 path 当中是有一个 a 的

​ 第三层循环:指向 b

第二次指向 a ,说明这次要判断 aa,剩余 b ,只能再切分 b

第三次指向 b ,没有办法继续被切分,直接判断 aab

如何生成 [[“a”,“a”,“b”]]

startIndex = 0 , i = 0 path = “a”

startIndex = 1 , i = 1 截取字符串是从 1 开始的 path =“a”

startIndex = 2,i=2 ,截取字符串从 2 开始,截取一个 path =“b”

如何生成 [[“aa”,“b”]]

startIndex = 0, i = 1 str = aa

2.6.2 代码实现

class Solution {
  private:
  vector<vector<string>> res;
  vector<string> path;
  void backtracking(string s,int startIndex){
    if(startIndex==s.size()){
      res.push_back(path);
      return;
    }
    for(int i=startIndex;i<s.size();i++){
      if(isHui(s,startIndex,i)){ // 是回文
        string str = s.substr(startIndex,i-startIndex+1); // 找到子串
        path.push_back(str);
        backtracking(s,i+1); // 需要所有的字符都参与,所以在判断是回文之后
        path.pop_back(); // 开始弹出 path 中的元素
      }
    }
  }
  bool isHui(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;
  }
  public:
  vector<vector<string>> partition(string s) {
    res.clear();
    path.clear();
    backtracking(s,0);
    return res;
  }
};

2.6.4 知识扩展

1.如何判断是回文字符串

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;
    }

2.7_93复原 IP 地址

2.7.1 算法概述

1.终止条件

当 pointNum = 3 也就是 ‘.’ 的个数等于 3 的时候就不用再继续判断了。在打了 3 个 . 后还要判断最后一段,然后再将 s 放入 res

2.递归参数

startIndex 用于指向下一个打 ‘.’ 的地方

3.单层搜索逻辑

image-20211025222334549

在判断一个 IP 地址的时候 ‘.’ 要一位一位的加。

就拿上面的字符串举例,在 2 后面加一个 ‘.’ , 在 5 后面加一个 ‘.’ ,这样一个个的判断,如下面所示

[“2.5.5.25511135”,“2.5.52.5511135”,“2.5.525.511135”,“2.5.5255.11135”,“2.5.52551.1135”,“2.5.525511.135”,“2.5.5255111.35”,“2.5.52551113.5”,“2.55.2.5511135”,“2.55.25.511135”,“2.55.255.11135”,“2.55.2551.1135”,“2.55.25511.135”,“2.55.255111.35”,“2.55.2551113.5”,“2.552.5.511135”,“2.552.55.11135”,“2.552.551.1135”,“2.552.5511.135”,“2.552.55111.35”,“2.552.551113.5”,“2.5525.5.11135”,“2.5525.51.1135”,“2.5525.511.135”,“2.5525.5111.35”,“2.5525.51113.5”,“2.55255.1.1135”,“2.55255.11.135”,“2.55255.111.35”,“2.55255.1113.5”,“2.552551.1.135”,“2.552551.11.35”,“2.552551.113.5”,“2.5525511.1.35”,“2.5525511.13.5”,“2.55255111.3.5”,“25.5.2.5511135”,“25.5.25.511135”,“25.5.255.11135”,“25.5.2551.1135”,“25.5.25511.135”,“25.5.255111.35”,“25.5.2551113.5”,“25.52.5.511135”,“25.52.55.11135”,“25.52.551.1135”,“25.52.5511.135”,“25.52.55111.35”,“25.52.551113.5”,“25.525.5.11135”,“25.525.51.1135”,“25.525.511.135”,“25.525.5111.35”,“25.525.51113.5”,“25.5255.1.1135”,“25.5255.11.135”,“25.5255.111.35”,“25.5255.1113.5”,"25.52551.1.1…

2.7.2.C++ 代码实现

class Solution {
private:
    vector<string> res;
    void backtracking(string& s,int startIndex,int pointNum){
        if(pointNum==3){ // 这里在结束的时候不能用 startIndex ,因为很有可能会有 2.5.5.2 的情况,在这种情况下就不用继续再分割了,所以要按照 . 的数量为准
            // 判断 IP 地址是否合法
            if(isIP(s,startIndex,s.size()-1)){
                res.push_back(s);
            }
            return;
        }
        for(int i = startIndex;i<s.size();i++){
            if(isIP(s,startIndex,i)){
                s.insert(s.begin()+i+1,'.'); // 插入一个点
                pointNum++;
                backtracking(s,i+2,pointNum); // 判断下一个子字符串是否满足要求
                pointNum--;
                s.erase(s.begin()+i+1); // 将 . 去掉
            }
        }
    }
    bool isIP(string& s ,int start,int end){
        if (start > end) return false; // 这句话千万不要落下
        // 对于单个 0 的判断
        if(s[start]=='0'&&start!=end) return false;
        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'); // 将每一个子串的内容都添加到 num 中,并判断 num 是否合法
            if(num>255) return false;
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        res.clear();
        if (s.size() > 12) return res; // 算是剪枝了
        backtracking(s, 0, 0);
        return res;
    }
};

2.7.3 知识扩展

1.如何判断合法 IP 地址段

// 判断相应区间内的字符是否合法
    bool isValid(const string& s,int start,int end){
        // 情况1:start>end
        if(start>end){
            return false;
        }
        // 情况2:开始位置是0,但是段中还有其他元素
        if(s[start]=='0'&&start!=end){ // 0 只能当单独的一个数,所以如果 0 在开头但是又有很多数的话
            return false;
        }
        int num = 0;
        for(int i=start;i<=end;i++){
            // 非数字
            if(s[i]>'9'||s[i]<'0'){ 
                return false;
            }
            // num 的值不合法
            num = num*10+(s[i]-'0');
            if(num>255){
                return false;
            }
        }
        return true
    }

首先判断:

①start 指针和 end 指针的位置是否不合法

②字段开头是不是 0 元素

③完成上面步骤后进入 for 循环

​ 判断这个字符是否是数字

​ 判断 IP 地址是否在 255 之内

2.7_78子集

2.7.1 算法描述

本算法是求每个元素的子集,并且子集中的元素是没有重复的

所以对 1,2, 3 分别求子集。而且每一个叶子节点都是结果

image-20211026214540496

递归思路:

此处有图

关键点:

1.每一个叶子节点都要添加到 res ,所以要在每一次遍历 backtracking 的时候将结果进行添加,平常都是到了最后才添加

2.path 的长度每增加 1 都会进入一个 for 循环

3.每一次 for 循环结束后 i++ 退出到上一层 for 循环

2.7.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex){
        res.push_back(path);
        // 结束条件
        if(startIndex==nums.size()){
            return; // 因为是实时的将叶子节点的值进行保存,所以在最后就不用添加到 res 中了
        }
        for(int i=startIndex;i<nums.size();i++){
            path.push_back(nums[i]); // 先添加再递归
            backtracking(nums,i+1);
            path.pop_back();  
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        res.clear();
        path.clear();
        backtracking(nums,0);
        return res;
    }
};

2.8_90子集2

2.8.1 算法描述

本题的关键点就是求子集,然后不重复,不重复的话在 40 组合2 已经涉及到了,而求子集又是上一题的思路

2.8.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int>path;
    void backtracking(vector<int>& nums,int startIndex){
        res.push_back(path);
        if(startIndex==nums.size()){
            return;
        }
        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();
        }
    }
public:
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        res.clear();
        path.clear();
        sort(nums.begin(),nums.end()); // 排序,将相同的元素排在一起
        backtracking(nums,0);
        return res;
    }
};

易错点:

最后别忘了对 vector 进行排序

2.9_491递增子序列

2.9.1 算法描述

1.这个题和以往求子序列有两点不同:

path 内的子集需要递增,candidate 是无序的

①要想组成 path 这里的条件是 path 中的顺序必须是递增的,所以要判断 nums[i] 和 path[i-1] 是否是递增的

②判断 nums[i] 是否在整个 candidate 中出现过,因为 candidate 不是顺序的,所以需要用 set 进行判断,如果用 nums[i-1] 和 nums[i] 的判断,必须保证 candidate 有序

这里产生重复是因为树的横向产生重复,所以在每一层 for 循环最外面增加 set 进行去重判断

比如 [4,7,6,7] 如果 [7] 已经判断完 ,[7] 就不能再出现了

容器的选择:

这里有两种方法,第一种是用哈希表,第二种是用数组,但是当数据比较少的时候,数组的速度比哈希表快很多

手模树:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZDSnIndc-1644592316406)(https://gitee.com/xuboluo/images/raw/master/img/202202062340021.png)]

2.9.2 C++ 算法实现

1.数组实现

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex){
        if(path.size()>1){
            result.push_back(path); // 把每一个过程也添加到 path 中
        }
        int used[201]={0}; //[-100,100]
        for(int i=startIndex;i<nums.size();i++){
            // 后面的那个数小于前面的数  或  cur 在前面已经判断过了
            if((!path.empty()&&nums[i]<path.back())||used[nums[i]+100]==1) continue; // back 得到最后一个元素
            used[nums[i]+100]=1;
            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }    
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

2.Hash 实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex){
        if(path.size()>1){ // 只要 path 中的值大于2就添加到 res 中
            res.push_back(path); // 这里不可以 return ,因为要将每次的中间结果进行添加
        }
        unordered_set<int> uset; // 使用 set 进行去重
        for(int i=startIndex;i<nums.size();i++){
            // 判断是否是递增序列以及判断是否重复
            if((!path.empty()&&nums[i]<path.back())||uset.find(nums[i])!=uset.end()){ // 先找到 nums[i] 在 set 中的位置,如果不是上一个保存的元素这就可以继续执行
                continue;
            }else{
                uset.insert(nums[i]); // 这个值已经被判断过了
                path.push_back(nums[i]);
                backtracking(nums,i+1);
                path.pop_back();
                
            }
        }
    }    
public:
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        res.clear();
        path.clear();
        backtracking(nums, 0);
        return res;
    }
};

2.9.4 知识扩展

1.得到 vector的最后一个元素

path.back(); // 假设 path 是 vector 类型的

如何判断它是一个递增序列:

将nums 当前值和 path 最后一个值进行比较

!path.empty()&&nums[i]<path.back()

2.使用 Set 的方法如何判断这个值之前是否被判断过

这里使用 nums 中的值当做 valus,key 值是通过 hash 表映射得到的

①先找到 nums[i] 保存的 index

②然后找到uset.end() ,因为前面已经判断了是否是递增序列,所以只要判断最后一个元素即可

uset.find(nums[i])!=uset.end()

find 方法是根据 value 的值返回某个元素的下标

3.Set 的初始化什么时候写在 for 循环里面什么时候写在 for 循环外面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jWKBVRJ6-1644592316406)(/Users/xuguagua/Documents/typora_image/image-20211030205435981.png)]

就像这个题,我们不在乎嵌套 for 循环的时候后面的 7 会不会被拼接上,就比如其中一个答案 [4,7,7]

当集合第一个7 的时候,其后面还是可以接 7 的

但是答案只有一个 [7,7] 也就是说在最外层 for 循环是不允许集合两次 7 的。所以在最外层 for 循环迭代的时候,只能操作一个 7

所以在组外层 for 循环 set 的值是不能重复的

而 46 题全排列这个题是在内层嵌套时是不能重复的,所以要把这个 set 定义在每一次 backtracking 的时候

image-20211030205331018

最后选择 set 初始化位置时要看是 path 内不允许有重复还是 path 之间不允许有重复

2.10_46 全排列

2.10.1 算法描述

关键点:

[1,2,3] 和 [1,3] 都 比较容易得到,但是 [1,3,2] 怎么得到,所以就不能使用 startIndex 作为 i 的开始了。

相当于添加了 3 之后又添加了 2 ,但是 i 要从 0 开始,但是重复出现的 1 又是怎么过滤掉的

解决方法:

为了将 1 过滤需要使用一个 used 数组

在 path 进行 pop 的时候需要将 used 的元素设为 false

错误思路:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-umGlQr4M-1644592316406)(https://gitee.com/xuboluo/images/raw/master/img/202202062340059.png)]

正确思路:

image-20220101115025894

2.10.2 C++ 代码实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    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]==true) continue;
            used[i] = true;
            path.push_back(nums[i]);
            backtracking(nums,used);
            path.pop_back();
            used[i]=false; // 弹出这个元素这个元素就设置为 false
        }
    }

public:
    vector<vector<int>> permute(vector<int>& nums) {
        res.clear();
        path.clear();
        vector<bool> used(nums.size(), false); // 用于判断 cur 是否存在于 path 中
        backtracking(nums, used);
        return res;
    }
};

因为这里 nums 中存储的元素个数并不多,所以使用数组存储每一个元素的使用用情况

2.11_47全排列2

2.11.1 算法描述

每一层 for 循环嵌套里面值不能重复:

used[i] = false

有两种东西不能重复:

① 在同一层 for 循环中,如果 nums[i-1] 和 nums[i] 重复,则 nums[i-1] 不判断了 —>nums[i-1]!=nums[i]

② 在递归过程中,i 从 0 开始,path 中如果重复放入则 nums[i] 就不再放了 —> used

2.11.2 C++代码实现

class Solution {
private:
    vector<vector<int>> res;
    vector<int> path;
    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++) {
            // used[i - 1] == true,说明同一树支nums[i - 1]使用过
            // used[i - 1] == false,说明同一树层nums[i - 1]使用过
            // 如果同一树层nums[i - 1]使用过则直接跳过
            if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) {
                continue;
            }
            if (used[i] == false) {
                used[i] = true;
                path.push_back(nums[i]);
                backtracking(nums, used);
                path.pop_back();
                used[i] = false;
            }
        }
    }
public:
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        res.clear();
        path.clear();
        sort(nums.begin(), nums.end()); // 排序
        vector<bool> used(nums.size(), false);
        backtracking(nums, used);
        return res;
    }
};

2.12_332 重新安排行程

2.12.1 算法描述

题目中有一个数组:

image-20220102103331842

[[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]] 从 JFK 出发,找到下一个可以去的机场为 SFO 和 ATL ,这两个机场应该优先去 ATL ,因为这个机场的排序小。从 ATL 出发可以去 JFK 和 SFO ,最后选择去 JFK 因为 JFK 的排序小。按照上面的规律一个个将头尾衔接起来

行程重复的情况:

再从 JFK 出发可以去 ATL 和 SFO ,因为 ATL 排序小所以 JFK 去 ATL ,但是又可以从 ATL 返回到 JFK ,然后又从 JFK 到 ATL 这样就会出现一种无限循环的方式,需要一个记录机票使用次数的 int 变量

1.终止条件

当存放结果的数组 res 中形成个数 = 机票个数 + 1 ,就说明所有的机票都使用过一次了

2.bool 类型的 backtracking

有一种情况是没有办法到达的,也就是说无法进行下一个判断了。如

image-20220102104134306

从 JFK 出发如果先到 KUL 但是 KUL 已经无路可走了,所以需要向上返回 false

3.数据结构

这里是用的数据结构为 unordered_map<string, map<string, int>> targets,其中unordered_map<出发机场, map<到达机场, 航班次数>> targets

①里面用 map 而不用 unordered_map 的原因是 map 是顺序的,可以通过字母顺序对机场进行排序
②这里用 map 而不用 set 是因为 set 无法记录次数,如果不能记录次数容易出现死循环

记录航班次数的原因是判断这条线路之前是否走过,每走一次里面的值就会-1

大于 0 :没有走过

小于0:走过

可以理解为还有几张剩余的

关键代码:

头尾相接

for (pair<const string, int>& target : targets[result[result.size() - 1]]) 

pair 可以接受一个 key-value 的 map item

image-20211101183530607

当前 result 中存放的值是 “JKF”,所以 result.size() - 1 中的值是 JKF ,所以 targets[result[result.size() - 1]] 的值就是 JKF 的 value 的值,MUC,那么这个 pair 映射的值就是 [JFK,MUC];

执行 for 循环中的方法 result 就将 MUC 拼接在最后

for 循环的条件就可以根据 MUC 找到以 MUC 为 key 值的 value

一直往下一直往下头尾相接

为什么是 bool 值的 backtracking

举例:

[[“JFK”,“KUL”],[“JFK”,“NRT”],[“NRT”,“JFK”]]

这里判断结束的条件不是通过子集合的元素个数,而是根据是这个飞行路线是否是通的。

如果在 backtracking 中发现这个路线可以走通则直接 return true

如果发现某个地点走不通,比如上面的 KUL ,则需要将 KUL pop 出来

4.整体步骤

(1)形成 sour->(target,num) 的映射,用来记录出发地目的地以及走的次数
(2)在 backtracking 中根据首尾相接原则不断找到下一个要判断的地点

2.12.2 C++ 代码实现

class Solution {
private: 
    unordered_map<string,map<string,int>> targets; // 用于将题干中的地点重新映射
    vector<string> result;  // 保存结果
    bool backtracking(int ticketNum){
        if(result.size()==ticketNum+1){ // 从题干中找到规律,result 中 str 个数是 ticket 中元素个数 +1 
            return true;
        }
        for(pair<const string,int>& target:targets[result[result.size()-1]]){ // pair 代表 map 中的一个 item ,得到最后一个元素
            if(target.second>0){ // 没有飞过这个地方
                result.push_back(target.first);
                target.second--; // 这个地方飞过了
                if(backtracking(ticketNum)) return true; 
              	// 剩下就是 false 的情况
                result.pop_back(); // 存在 value 不是下一个出发点,那么将这个 value 去掉
                target.second++;
            }
        }
        return false;
    }
public:
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        targets.clear();
        // map 赋值
        for(vector<string>& vec:tickets){
            targets[vec[0]][vec[1]]++; // 记录映射关系
        }
        result.push_back("JFK"); // 起始节点是固定的
        backtracking(tickets.size());
        return result;
    }
};

2.13_51N皇后

2.13.1 算法描述

image-20211028221729822

N 皇后一共有三个要求:

1.不能同行

2.不能同列

3.不能同斜线:正对角线和副对角线

整体思路就是去试每一个位置上的元素是否符合三个要求。上图所示每一层都是 for 循环的循环变量+1,每一列都会嵌套一层 for 循环。下面只对上图 ①②③④ 四个棋盘进行讲解

棋盘①

row = 0 ;col = 0; isValid 三个 for 循环全部略过,所以这个方法返回 true ,Q 被写在了 [0] [0] 的位置上;这时候嵌套一层 for 循环,并将 row+1 的值传入,说明这一行已经判断完了

棋盘②

row=1;col=0,也就是判断 [1] [0]下标的这个位置是否可行;调用 isValid 方法,这个方法中判断的是同列是否有相同元素,因为同行是在同一层 for 循环进行判断的。对于同列进行判断二维下标:【变化】【固定】即可判断同一列的不同行是否有重复。因为这里产生重复所以返回 false ,就不对这个位置进行任何赋值。

棋盘③

row=1;col=1 ,因为 ② 不可行所以向下判断下一个格子。由上可知同列的不同行是没有重复元素的,所以先判断 45 度对角线。就用③的这个 Q 进行判断,起始判断的位置肯定不是它自己现在的位置,是①的位置,所以一开始先进行两个减1 的操作,然后再判断对应的位置是否有 Q 。

棋盘④

row=1;col=3,是接茬棋盘③的同一个 for 循环执行的

棋盘⑤

row=2;col=1 是从棋盘 ④的位置嵌套的 for 循环,这里判断的是 145 度角,副对角线,判断的第一个位置就是它右斜上方的 Q ,其他判断不变

棋盘⑥

起始在棋盘⑤的时候 row 的值就是 n 了,其实棋盘⑥是没有判断的

上面的图是没有撤销皇后操作的,下面的图会涉及到撤销皇后的操作:

image-20211028224741101

关键:

1.如何判断主对角线和副对角线

起始位置是当前元素所对应对角线的位置,该位置的后侧都是没有元素的,所以只需要判断上侧

2.isValid 同行同列方法中每次判断的都是相同列中不同行是否一致

3.res.push_back(chessboard); 有几种排棋方法,这个就会被执行多少次

chessboard 保存一种可能性

4.这里的 isValid 操作只是判断当前情况下是否满足三个条件,也许在下一个 row 就不满足了

N 皇后判断代码:

易错点:

①只要判断该位置所在列是否满足,不用判断所在行,因为棋子在判断的时候是横向走的,该位置之前的格子一定没有棋子

②判断 45 和 135 度角的时候判断的下标要是 Q 经历过的下标,没经历的下标不用判断

③为什么 chessboard 放置在递归中

因为有多种棋盘的情况,每一种棋盘的情况都是互不干扰的

bool isValid(int n ,int row,int col,vector<string>chessboard){
  // 判断本行的列
  for(int i =0;i<row;i++){
    if(chessboard[i][col]=='Q') return false;
  }
  // 判断 45 
  for(int i = row-1,j = col-1;i>=0&&j>=0;i--,j--){
    if(chessboard[i][j]=='Q') return false;
  }
  // 判断 145
  for(int i = row,j = col;i>=0&&j<n;i--,j++){
    if(chessboard[i][j]=='Q') return false;
  }
  return true;
}

2.13.2 C++ 代码实现

class Solution {
private:
    vector<vector<string>> res;
    void backtracking(int n,int row,vector<string>chessboard){
        if(row==n){ // 所有行都判断过了
            res.push_back(chessboard);
            return;
        }
        for(int col=0;col<n;col++){ // 一行一行的进行判断
            // 判断这个位置是否可行
            if(isValid(col,row,chessboard,n)){ 
                chessboard[row][col] = 'Q'; // 放置皇后
                backtracking(n,row+1,chessboard); // 判断下一行
                chessboard[row][col] = '.'; // 将该位置还原准备下一个棋盘
            }
        }
        }
    bool isValid(int col,int row,vector<string> chessboard,int n){
        // 判断是否是同列不同行
        for(int i =0;i<row;i++){
            if(chessboard[i][col]=='Q') return false;
        }
        // 判断是否是 45 度角
        for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q') return false;
        }
        // 判断是否是 145 度角
        for(int i=row-1,j=col+1;i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q') return false;
        }
        return true;
    }    
public:
    vector<vector<string>> solveNQueens(int n) {
        res.clear();
        std::vector<std::string> chessboard(n, std::string(n, '.')); // 全控棋盘
        backtracking(n, 0, chessboard);
        return res;
    }
};

2.13.3 知识扩展

1.创建一个棋盘

std::vector<std::string> chessboard(n, std::string(n, '.'));

2.14_37解数独

2.14.1 算法描述

image-20211030225712156

1.关于嵌套 for 循环的个数

这里是一个二维棋盘,所以每判断一个值要是用双重 for 循环,用于定位行列。与此同时,每一个格子都要判断 1-9 哪个值合适,所以还要再嵌套一个 for 循环去判断每一个格子;那么对于这个格子就是用三重 for 循环进行一次递归

2.判断是否是数独要满足三点:

判断是数独的话需要每一次对二维棋盘中每一个点进行判断,判断是否是数独的条件是该值是否和在这一行或者这一例或者九宫格中出现过

(1)固定行,判断每一列是否是数独

(2)固定列,判断每一行是否是数独

(3)每段每一个小的 3*3 的格子是否是数独

3.关于 return 与终止循环的条件

这里是需要回溯的反馈结果的,使用三重 for 循环填完一个值,但是这个值不一定就确定下来,还要通过后面的判断结果对这个值进行反馈

所以 backtracking 是 bool 类型的,需要有3个地方返回 return 的结果:

①在 9 个 k 值全部判断完后没有合适的字符,则返回 false

②在三个 for 循环全部都判断完后返回 true

③每次递归时接收 backtracking 的结果:这个 true 代表后面的数字结果也是成功的

2.14.2C++ 代码实现

class Solution {
private:
bool backtracking(vector<vector<char>>& board) {
    // 因为是一个九宫格所以用双重 for 循环去写
    for (int i = 0; i < board.size(); i++) {        // 遍历行
        for (int j = 0; j < board[0].size(); j++) { // 遍历列
            // 九宫格中有数就不用继续判断了
            if (board[i][j] != '.') continue;
            // 每个格子都要放 1-9 9 个数判断是否合适
            for (char k = '1'; k <= '9'; k++) {     
                if (isValid(i, j, k, board)) {
                    board[i][j] = k;                // 放置k
                    if (backtracking(board)) return true; // 如果这个指可以放在这个位置,则这个值返回 true 并且继续进行递归
                    board[i][j] = '.';              // 执行了剩余函数,说明这个位置是错的
                }
            }
            return false;  // 9个数都试完了,都不行,那么就返回false
        }
    }
    return true; // 遍历完没有返回false,说明找到了合适棋盘位置了
}

public:
    void solveSudoku(vector<vector<char>>& board) {
        backtracking(board);
    }
};

易错:

注意所有 return 的位置和值

3.其他题目

3.1_2049找出三位偶数

LeetCode 题目链接

3.1.1 算法描述

回溯+Hash

1.回溯问题

从题目最后的输出结果看出让输出某个数字组合的全部组成答案,这个就必须对每个数字进行一个个的遍历判断。所以用到回溯

2.HashMap

解决重复问题:

测试用例:[2,2,8,8,2]

如果仅仅使用 vector 会出现下面的结果:

[222,228,228,222,228,228,282,282,288,282,282,288,222,228,228,222,228,228,282,282,288,282,282,288,222,228,228,222,228,228,282,282,288,282,282,288,822,822,828,822,822,828,822,822,828,882,882,882,822,822,828,822,822,828,822,822,828,882,882,882]

其中有多个 228 ,这是因为分别使用 index=0 的 2 ,index=1 的 2 index=-1 的 2 全部被当成了不同的 2 ,然后组成了多个 228。

对于上面的问题可以使用 set 进行解决。

3.1.2 C++ 代码实现

class Solution {
  public:
  vector<int> findEvenNumbers(vector<int>& digits) {
    unordered_set<int> nums;   // 目标偶数集合
    int n = digits.size();
    // 遍历三个数位的下标
    for (int i = 0; i < n; ++i){
      for (int j = 0; j < n; ++j){
        for (int k = 0; k < n; ++k){
          // 判断是否满足目标偶数的条件
          if (i == j || j == k || i == k){
            continue;
          }
          int num = digits[i] * 100 + digits[j] * 10 + digits[k];
          if (num >= 100 && num % 2 == 0){
            nums.insert(num);
          }
        }
      }
    }
    // 转化为升序排序的数组
    vector<int> res;
    for (const int num: nums){
      res.push_back(num);
    }
    sort(res.begin(), res.end());
    return res;

  }
};

3.1.3 时空复杂度

时间复杂度:O(n3+MlogM)

n 是 digit 的长度。M 是偶数的个数,对其进行排序。这里将偶数的处理放在最后,而不是放在最前面

空间复杂度:

O(M):Set 的存储空间

3.2_79单词搜索

LeetCode 题目链接

**带有返回值的回溯 **

3.2.1 算法描述

本题用到回溯是因为要判断从哪个字母开始,就如下面的字符串来说ABCCED 为什么要从第一个 A 开始,不从左下角的第一个 A 开始,所以这里用到回溯对每一个字母判断 " 是否从我开始 "

为什么要使用回溯,DP 不行

因为对于每个字母都要去判断是否可以从我开始,是需要重新判断的。但是 DP 的话当前值是从前一个字母中得到的,没有对该字母进行重新判断。

image-20211218083100465

3.2.2 C++ 代码实现

class Solution {
  public:
  bool exist(vector<vector<char>>& board, string word) {
    for(int i =0;i<board.size();i++){
      for(int j = 0;j<board[i].size();j++){
        if(dfs(board,word,0,i,j)) return true; // 每一个字母都有可能是 word 的起始字母
      }
    }
    return false;
  }
  // 方向数组
  int dx[4] = {-1,0,1,0},dy[4] = {0,1,0,-1};
  bool dfs(vector<vector<char>>& board,string& word,int u,int x,int y){
    if(board[x][y]!=word[u]) return false; // 起始字母不相等
    if(u == word.size()-1) return true ; // 遍历到了 word 的最后一个字母
    char t = board[x][y];
    board[x][y] = '.';
    for(int i =0;i<4;i++){
      int a = x+dx[i],b=y+dy[i]; // 当前 x,y 的四个方向的格子
      // a,b 指向的下标越界或者当前格子已经被使用过
      if(a < 0 || a >= board.size() || b < 0 || b >= board[0].size() || board[a][b] == '.') continue;
      if(dfs(board,word,u+1,a,b)) return true; // 判断下一个格子
    }
    board[x][y] = t;
    return false;
  }
};

3.2.3 时空复杂度

时间复杂度:

image-20211218093828761

3.3_22括号生成

3.3.1 算法描述

1.递归的函数及其参数

这里的函数使用 backtracking 作为递归函数,因为这里涉及左右括号所以要传入当前所判断的左右括号的个数,以及 n 的个数

void backtracking(vector<string>& res,string& path,int open,int close,int n){}

2.递归的终止条件

当 path 中括号的个数到了 n*2 就可以终止了,并且将其保存在 res 中

if(path.size()==n*2){
  res.push_back(path);
  return;
}

3.单层递归逻辑

3.3.2 代码实现

class Solution {
  vector<string> res; // 结果
  string path; // 中间过程
  void backtracking(vector<string>& res,string& path,int open,int close,int n){
    if(path.size()==n*2){
      res.push_back(path);
      return;
    }
    if(open<n){ // 左括号的个数 < n
      path.push_back('(');
      backtracking(res,path,open+1,close,n);
      path.pop_back();
    }
    if(close<open){ // 右括号的处理
      path.push_back(')');
      backtracking(res,path,open,close+1,n);
      path.pop_back();
    }
  }
  public:
  vector<string> generateParenthesis(int n) {
    backtracking(res,path,0,0,n);
    return res;
  }

};

3.3.3 时空复杂度

时间复杂度:image-20220128132812232

空间复杂度:O(n)

这里不需要判断

1.回溯算法参数

2.终止条件

左边括号的个数和右边括号的个数都不能大于 n

括号的总个数不能大于 2*n

总结:

1.组合问题

组合问题题目:在候选数组中找到所有满足某个条件的组合。如在 candidates = [10,1,2,7,6,1,5] 中找到 target = 8 的组合;输出

[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

组合问题是从一堆数中选出有可能的组合,所以组合问题 path 和 path 是不能重复的,其次如果 candidates 中有重复的数

代表题目有:

77,216,39,40

应用题目:17 131 分割回文串 93 复原 IP 地址

2.子集问题

candidate 中如果有相同的值,path 中是否可以有相同的值出现

在递归 for 循环内如果 [4,6,7,7] 第一个 7 判断过了第二个 7 可以继续判断;所以可以出现结果 [4,7,7]

在同一轮 for 循环内第一个 7 判断过了,第二个 7 就不用再判断,所以不会出现结果 [7,7],[7,7]

分类:

candidate 中是否有重复:

starIndex 的值是多少,是否有 startIndex ,i 的值是 startIndex 还是 0

46(没有重复)

47(有重复)

Uesed 数组的作用,在 for 循环中如果 for 的初始化条件从 i=0 开始则递归过程中会有重复。用来判断 index 是否重复

set 数组的作用,在 candidate 无序时,没有办法使用 nums[i-1] ,nums[i] 是否是重复的。用来判断值是否重复

如果

题号/条件题目candidate 是否有重复for 循环 i 的起始位置startIndex 的起始位置used 数组
46input:[1,2,3];out: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2],[3,2,1]]i=0无 startIndex
单元格单元格
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值