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


1.for 循环的 i++ 就是横向遍历兄弟节点
2.每次递归 backtracking 就是遍历该节点的下层相连节点

3.纵向遍历时会增加 tmp 中的节点个数

Tip:visited 的使用
数组的遍历和图的遍历都会用到 visited,如题目:46. 全排列 - 力扣(LeetCode),79. 单词搜索 - 力扣(LeetCode)。
在无环图中 visited 放在 for 循环外面,全排列 visited 放在 for 循环内部。
在无环图中:当以 A 为起点是,B 节点不想被重复遍历,所以 B 只有在整个 A 都遍历完才释放
在全排列中:1–>2–>3 与 1–>3–>2 是被允许的,所以只是在 1 的同一条树枝 2 不可重复遍历,而不是说在以 1 为起点的 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有
单元格单元格
一文搞懂回溯算法,上机考试再也不会慌 文章目录什么是回溯法回溯法的效率回溯法解决的问题如何理解回溯法回溯法模板总结 别看回溯法很难,但回溯法就是暴力解法+剪枝操作 什么是回溯法 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 在二叉树系列中,我们已经不止一次,提到了回溯,例如二叉树:以为使用了递归,其实还隐藏着回溯。 回溯是递归的副产品,只要有递归就会有回溯。 「所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数」。 回溯法的效率 回溯法的性能如何呢,这里要和大家说清楚了,「虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法 阅读详情

相关推荐

蓝桥杯 算法提高 8皇后·改 -- DFS 回溯

蓝桥杯 算法提高 8皇后·改 -- DFS 回溯 算法提高 8皇后·改 时间限制:1.0s 内存限制:256.0MB 问题描述   规则同8皇后问题,但是棋盘上每格都有一个数字,要求八皇后所在格子数字之和最大。 输入格式   一个8*8的棋盘。 输出格式   所能得到的最大数字和 样例输入 1 ...

豆子 310

【算法】回溯算法

回溯算法

Code 小白的博客 6164

回溯算法      8皇后问题 代码

/*     回溯算法      */ // 8皇后问题 static const int n = 8; // 下标表示行,值表示列 int Column_num[n+1] = {0}; int index = 0; // 解的个数 int anwser_num = 0; int Palace(int Col[], int index) {     int i;     int Ro...

goudahai的专栏 277

基础算法(六):回溯算法

Hello大家好,停了半个多月算法学习的荔枝又变菜了,最近决定认认真真地重新学习回溯,在这篇文章中,荔枝也准备仔仔细细梳理相关的问题和知识点,主要通过回溯算法的模板题解来类比回溯算法五大问题:组合、分割、子集、排列和棋盘问题

荔枝当大佬的博客 2836

8皇后以及N皇后算法探究,回溯算法的JAVA实现,递归方案

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。...

张晨光老师的播客 708

BQ25798双电源输入4节电池升降压充电与最大功率跟踪于光伏板应用

介绍一颗具有窄范围直流电压NVDC、路径管理PPMT、光伏板最大功率跟踪及反向放电OTG升降压充电控制器,带I2C接口能对内部寄存器做控制

linkedin_38804946的博客 1676

一文弄懂回溯算法(例题详解)

回溯是递归的副产品,只要有递归就会有回溯」,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都使用了递归。详细地说:可以将回溯算法过程理解成一颗多叉树的遍历过程, 回溯法按深度优先策略搜索问题的解空间树。首先从根节点出发搜索解空间树,当算法搜索至解空间树的某一节点时,先利用剪枝函数判断该节点是否可行(即能得到问题的解)。如果不可行,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则,进入该子树,继续按深度优先策略搜索。void backtrack(参数) {

2302_79862386的博客 4280

C#利用S7netplus实现西门子S7_200_SMART PLC高效点位监控与操作

本文详细介绍了如何使用C#结合S7netplus开源库高效监控与操作西门子S7-200 SMART PLC的点位。内容涵盖从开发环境搭建、建立通信连接,到核心的点位读写、批量数据处理及字节与位数组转换等关键技术,并提供了构建稳定监控程序的架构与避坑指南,帮助开发者快速实现上位机与PLC的稳定数据交互。

rum55的博客 315

回溯算法——>8皇后问题

一、算法的描述 回溯算法实际是一个类似枚举的搜索尝试方法,它的主题思想是在搜索尝试中找问题的解,当不满足求解条件就”回溯”返回,尝试别的路径。回溯算法是尝试搜索算法中最为基本的一种算法,其采用了一种“走不通就掉头”的思想,作为其控制结构。 二、算法的设计思想 回溯法是在包含问题的所有解的解空间树中。按照深度优先的策略,从根结点出发搜索解空间树,算法搜索至解空间树的任一结点时,总是先判断该结点是否满...

yo_u_niverse的博客 559

游戏开发实战:5种AOI算法对比(九宫格 vs 灯塔 vs 十字链表)

本文深入对比了游戏开发中五种主流AOI算法(九宫格、灯塔、十字链表等)的性能与适用场景,帮助开发者根据项目规模选择最优解决方案。通过详细的技术分析和实战数据,揭示不同算法在万人同屏等极端场景下的表现差异,为MMORPG等大型多人在线游戏提供关键性能优化指南。

weixin_29268385的博客 474

Workbench中直接调用ICEM CFD进行网格划分

自从ANSYS 12.0之后,ICEM CFD就从Workbench中被分离出去,作为一个独立的程序使用了。取而代之的是Meshing模块。 在Meshing的属性节点菜单中右键点击Mesh,选择Insert > Method,插入方法。 选择需要划分网格的几何体,点击apply。此时Geometry显示为1 Body。 设置Method为MultiZone,如果不设置成这个的话,找不到进入ICEM CFD的入口。如果要划分四面体,可以在Mapped Mesh Type中选择其他类型。

Yan的博客 6511

回溯算法--8皇后问题

一、回溯算法主要用到递归调用 1、递归概念:方法自己调用自己,每次调用时,传入不同的参数。有便于解决复杂问题,同时可以简洁代码 2、能解决的问题和规则 二、迷宫回溯 /** * @author chengxn * @date 2020/11/26 * 迷宫回溯问题 */ public class MiGong { public static void main(String[] args) { //数组模拟地图 int[][] map = new int[

ChengChengxn的博客 311

保姆级教程:手把手带你玩转英飞凌TC397开发板的板载资源(CAN FD、LCD、SD卡)

本文提供英飞凌TC397开发板的全面开发指南,涵盖CAN FD通信、LCD显示、SD卡文件系统和以太网通信等核心功能。通过详细的代码示例和硬件连接说明,帮助开发者快速上手并充分利用开发板的丰富资源,实现高效嵌入式系统开发。

weixin_33726313的博客 241

回溯算法-8皇后问题

8皇后问题表述:在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后,任何2个皇后不放在同一行或同一列或同一斜线上。求不同的解的个数。 解向量:(x1, x2, … , xn) 显约束:xi = 1,2, … ,n 隐约束: 1)不同列:xi != xj ...

weixin_30241919的博客 111

基于Ardupilot/PX4固件,APM/PIXhawk硬件的VTOL垂直起降固定翼软硬件参数调试(第一篇)安装调试

基于Ardupilot/PX4固件,APM/PIXhawk硬件的VTOL垂直起降固定翼调试 本文内容大部分来自Kris,我们的K大,在VTOL领域的大牛,在此,非常感谢K大。能引用K大的成果也是自己的荣幸,不便之处还请K大多多包涵,在此引用只是为方便自己日常学习,部分参数表里根据自身情况,增加了相应参数。 1.安装及下载Ardupilot官方固件: VTOL固件的来源可分为Ardupilot官方固...

u011322358的博客 3万+

2025数学建模美赛C题完整建模思路——奥运会奖牌榜的模型

2025数学建模美赛C题完整建模思路——奥运会奖牌榜的模型

Yilu264的博客 1万+

8-回溯算法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。回溯是递归的副产品,只要有递归就会有回溯。回溯法其实就是暴力查找,并不是什么高效的算法。

学习使我快乐 1477

Linux 服务器上部署 OpenClaw完整教程

2. 全局安装 OpenClaw 3. 初始化配置 配置模型提供商 方式一:使用配置命令(推荐) 方式二:直接编辑配置文件 在 部分添加: 配置多个模型提供商 如果你使用 Antigravity 本地代理,可以同时配置多个模型: 配置飞书集成 1. 创建飞书应用 访问 飞书开放平台 创建企业自建应用 记录 App ID 和 App Secret 2. 配置应用权限 在飞书开放平台,为应用添加以下权限:重要: 必须使用 长连接(WebSocket) 模式,不要使用 HTTP 回调!在飞书开放

Maynor的博客 5247

关于递归回溯算法的案例(8皇后问题案例)

关于递归回溯算法的案例 在8×8格的国际象棋上摆放8个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。如果经过±90度、±180度旋转,和对角线对称变换的摆法看成一类,共有42类。计算机发明后,有多种计算机语言可以编程解决此问题。 public class Queue8 { int max = 8 ; // 定义一个max表示共有多少个皇

dbbigdata的博客 292

ssm框架搭建

这个是一个在idea上成功跑起来的ssm框架项目,保存在这里方便以后使用直接下载

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

8皇后回溯算法递归方案探究及JAVA实现

8皇后问题简介 八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。高斯认为有76种方案。1854年在柏林的象棋杂志上,不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。 第一种方案 算法思考,初步思路: (1) 构建二维int或者short型数组,在内存.

WorldMvp的专栏 402

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

算法设计-回溯法——8皇后问题

算法介绍 回溯法: 回溯法又称试探法。回溯法的基本做法是深度优先搜索,是一种组织得井井有条的、能避免不必要重复搜索的穷举式搜索算法。 回溯算法的基本思想:从一条路往前走,能进则进,不能进则退回来,换一条路再试。 问题实例 问题描述: 题目: 要求用回溯法求解 8-皇后问题,使放置在 8*8 棋盘上的 8 个皇后彼此不受攻击,即:任 何两个皇后都不在同一行、同一列或同一斜线上。请输出 8 皇后问题的所有可行解。 问题分析 在解决八皇后问题时,如果采用穷举法,则需要列举8^8种情形。采用回溯法,则是对解空间使用

weixin_44034024的博客 2516

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

力扣hot100—系列8-回溯算法

画树形图:回溯问题一定要在纸上画出递归树。死记模板:先模仿->recursion->path.pop这个固定套路。区分去重:搞清楚什么时候需要startIndex(组合/子集问题),什么时候需要used数组(排列问题)。调试:如果逻辑乱了,在入口打印path,能直观看到程序是怎么“跳”的。

qq_60957452的博客 781

算法学习笔记Day8——回溯算法

1.回溯算法是什么?回溯算法就是个多叉树的遍历问题,关键在于在前序和后序时间点做一些操作,本质是一种暴力枚举算法,它和DFS非常相似,区别在于,回溯算法关注点在于树的树枝,DFS关注点在于树的节点。2.回溯算法的技巧站在一棵决策树的节点,需要考虑三个问题:1、路径:也就是已经做出的选择。2、选择列表:也就是你当前可以做的选择。3、结束条件:也就是到达决策树底层,无法再做选择的条件。3. 回溯算法的框架(规律)def backtrack(路径,选择列表){if(满足结束条件){

m0_61940795的博客 992

24点游戏回溯算法与8个8组成1000问题的C++,java实现

-

zorchp 1084
上一篇: 7.树相关
下一篇: 9.贪心算法
许菠萝
博客等级 码龄10年 263粉丝 77原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值