【学习笔记】【Leetcode 分门别类讲解】——递归和回溯

解决一个问题有多个步骤,每一个步骤有多种方法,题目又要我们找出所有的方法:回溯
在这里插入图片描述
回溯法并不是什么高效的算法,本质是穷举,穷举所有可能,然后选出我们想要的答案,
因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。

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

「回溯法解决的问题都可以抽象为树形结构」

  • 模板
    for循环横向遍历,递归纵向遍历,回溯不断调整结果集
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

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

组合问题

【77. 组合】

  • 用for循环嵌套连暴力都写不出来!
  • 回溯法就是解决这种k层for循环嵌套的问题
    在这里插入图片描述
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;
    }
};

——剪枝优化

优化过程如下:

  1. 已经选择的元素个数:path.size();
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1 开始遍历

从i开始还需要k-path.size()个元素,但此时i还没加进path中,所以实际起始元素是i-1,即i-1+(k-path.size()) <= n

for(int i = startIndex;i<=n - (k-path.size()) +1;i++)

【216. 组合总和 III】

  • 思路同上,多个sum罢了
class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    int sum = 0;
    void backTracking(int k, int n,int startIndex){
        if(sum>n) return;//剪枝
        if(path.size() == k){
            if(sum == n) res.push_back(path);
            return;
        }
        for(int i = startIndex;i<=9-(k-path.size())+1;i++){//剪枝
            path.push_back(i);
            sum += i;
            backTracking(k,n,i+1);
            sum -= i;
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backTracking(k,n,1);
        return res;
    }
};

【17. 电话号码的字母组合】

在这里插入图片描述

  • 数字和字⺟如何映射
    可以使⽤map或者定义⼀个⼆位数组
class Solution {
private:
    const string letterMap[10] = {
        "", // 0
        "", // 1
        "abc", // 2
        "def", // 3
        "ghi", // 4
        "jkl", // 5
        "mno", // 6
        "pqrs", // 7
        "tuv", // 8
        "wxyz", // 9
    };
public:
    vector<string> res;
    string path;
    void backTracking(string digits,int index){//这个index是记录遍历第⼏个数字了,就是⽤来遍历digits的
                                                //(题⽬中给出数字字符串),同时index也表示树的深度。
        if(index==digits.size()){
            res.push_back(path);
            return;
        }
        int digit = digits[index] - '0';// 将index指向的数字转为int
        string letter = letterMap[digit];// 取数字对应的字符集
        for(int i = 0;i<letter.size();i++){
            path.push_back(letter[i]);// 处理
            backTracking(digits,index+1);// 递归,注意index+1,下一层要处理下⼀个数字
            path.pop_back();// 回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        res.clear();
        path.clear();
        if(digits.size()==0) return res;
        backTracking(digits,0);
        return res;
    }
};

【39. 组合总和】

  • 本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

    ⼀个集合求组合,就需要startIndex,例如:求组合总和!。
    多个集合取组合,各个集合之间相互不影响,就不⽤startIndex,例如:电话号码的字⺟组合

  • 注意以上我只是说求组合的情况,如果是排列问题,⼜是另⼀套分析的套路,后⾯我再讲解排列的时候
    就重点介绍。

  • 加个startIndex,就不会出现答案重复了,思路也很清晰:如下图
    在这里插入图片描述

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    void backTracking(vector<int>& candidates, int target,int index){
        if(sum>target) return;
        if(sum==target) {
            res.push_back(path);
            return;
        }
        for(int i = index;i<candidates.size();i++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            backTracking(candidates,target,i);// 不⽤i+1了,表示可以重复读取当前的数
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backTracking(candidates,target,0);
        //unordered_set<vector<int>> st(res.begin(),res.end());
        return res;
    }
};

——剪枝优化,需要排序

在求和问题中,排序之后加剪枝是常⻅的套路!

在这里插入图片描述

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    void backTracking(vector<int>& candidates, int target,int index){
        if(sum>target) return;
        if(sum==target) {
            res.push_back(path);
            return;
        }
        // 如果 sum + candidates[i] > target 就终⽌遍历
        for(int i = index;i<candidates.size() && sum+candidates[i]<=target;i++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            backTracking(candidates,target,i);
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());//需要排序
        backTracking(candidates,target,0);
        return res;
    }
};

【40. 组合总和 II】

——去重,树枝和树层,记得排序!

在这里插入图片描述
在这里插入图片描述

  • 可以用i>startIndex && candidates[i-1] == candidates[i]作为判断条件,而不用辅助数组,前提是要sort
class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    int sum = 0;
    void backTracking(vector<int>& candidates, int target,int startIndex){
        if(sum>target) return;
        if(sum==target){
            res.push_back(path);
            return;
        }
        for(int i = startIndex;i<candidates.size()&& sum+candidates[i]<=target;i++){
            if(i>0 && candidates[i]==candidates[i-1] && i>startIndex){continue;}//体会这里的去重
            path.push_back(candidates[i]);
            sum += candidates[i];
            backTracking(candidates,target,i+1);
            sum -= candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        backTracking(candidates,target,0);
        return res;
    }
};

分割问题

【131.分割回文串】

  • 切割问题类似组合问题。
    在这里插入图片描述
    在处理组合问题的时候,递归参数需要传⼊startIndex,表示下⼀轮递归遍历的起始位置,这个
    startIndex就是切割线。所以终⽌条件代码如下:
void backtracking (const string& s, int startIndex) {
	 // 如果起始位置已经⼤于s的⼤⼩,说明已经找到了⼀组分割⽅案了
	 if (startIndex >= s.size()) {
		 result.push_back(path);
		 return;
	 }
}
  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线 :startIndex
  • 切割问题中递归如何终⽌:startIndex >= s.size()
  • 在递归循环中如何截取⼦串:利用startIndex && substr
  • 如何判断回⽂:双指针
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;
    }
//需要分割字符,利用startIndex && substr
    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(isPalindrome(s,startIndex,i)){
                string str = s.substr(startIndex,i-startIndex+1);// 获取[startIndex,i]在s中的⼦串
                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 res;
    }
};

【93. 复原 IP 地址】

回溯三部曲
- 递归参数
startIndex⼀定是需要的,因为不能重复分割,记录下⼀层递归分割的起始位置。
本题我们还需要⼀个变量pointNum,记录添加逗点的数量。

- 递归终⽌条件
本题明确要求只会分成4段,所以不能⽤切割线切到最后作为终⽌条件,⽽是分割的段数作为终⽌条件。
pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。
然后验证⼀下第四段是否合法,如果合法就加⼊到结果集⾥

- 单层搜索的逻辑
在 for (int i = startIndex; i < s.size(); i++) 循环中 [startIndex, i]这个区间就是截取的⼦串,需要判断这个⼦串是否合法。
如果合法就在字符串后⾯加上符号 . 表示已经分割。
如果不合法就结束本层循环
然后就是递归和回溯的过程:
递归调⽤时,下⼀层递归的startIndex要从i+2开始(因为需要在字符串中加⼊了分隔符 . ),同时pointNum 要 +1。
回溯的时候,就将刚刚加⼊的分隔符 . 删掉就可以了,pointNum也要-1。

class Solution {
public:
//需要按.分割
//需要判断有效
//
    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;// 如果⼤于255了不合法
        }
        return true;
    }
    vector<string> res;
    void bT(string &s,int index,int pointNums){
        if(pointNums==3){ // 逗点数量为3时,分隔结束
 // 判断第四段⼦字符串是否合法,如果合法就放进result中
            if(isValid(s,index,s.size()-1)){
                res.push_back(s);
            }
            return;
        }
        for(int i = index;i<s.size();i++){
            if(isValid(s,index,i)){
                s.insert(s.begin()+i+1,'.');// 在i的后⾯插⼊⼀个逗点
                pointNums++;
                bT(s,i+2,pointNums);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2
                pointNums--;
                s.erase(s.begin()+i+1);// 回溯删掉逗点
            }
            else break;
        }

    }
    vector<string> restoreIpAddresses(string s) {
        if(s.size()<4 || s.size()>12) return res;
        bT(s,0,0);
        return res;
    }
};

子集问题——树的所有节点

【78. 子集】

组合问题、分割问题是收集树形结构中叶⼦节点的结果
⽽⼦集是收集树形结构中树的所有节点的结果。

class Solution {
public:
    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++){//求取⼦集问题,不需要任何剪枝!因为⼦集就是要遍历整棵树
            path.push_back(nums[i]);
            backTracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backTracking(nums,0);
        return res;
    }
};

【90. 子集 II】

——和【40. 组合总和 II】一样的思路

class Solution {
public:
    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>0 && nums[i]==nums[i-1] && i>startIndex) 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 res;
    }
};

【491. 递增子序列】——要把过程吃透

在这里插入图片描述

  • 小总结:
  • 因为没有sort,所以if(i>0 && candidates[i]==candidates[i-1] && i>startIndex){continue;}这样不能去同一树层的重,要用set或者数组,小数据就用数组,快!
  • for循环是遍历树层的,但是是从startIndex开始的,而且是要一枝一枝树枝的看的,从nums[i]<path.back()这里看出
  • 遍历一整颗树res那里push_back是不用return的,接着往下走
class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backTracking(vector<int>& nums,int startIndex){
        if(path.size()>1) {//递增子序列中 至少有两个元素 
            res.push_back(path);
        }
        //unordered_set<int> st;//同一树层去重
        int st[201] = {0};//给的范围是-100到100
        for(int i = startIndex;i<nums.size();i++){
            if((!path.empty() && nums[i]<path.back())) continue;//树枝要递增
            else if(st[nums[i]+100]==1) continue;//同一树层要去重
            else{
                //st.insert(nums[i]);
                st[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 res;
    }
};

排序问题

【46. 全排列】

  • 每层都是从0开始搜索⽽不是startIndex
  • 需要used数组记录path⾥都放了哪些元素了
    在这里插入图片描述
class Solution {
public:
    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]==1) continue;
            used[i]=1;
            path.push_back(nums[i]);
            backTracking(nums,used);
            path.pop_back();
            used[i]=0;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(),0);
        backTracking(nums,used);
        return res;
    }
};

【47. 全排列 II】——去重前先排序!

  • 要强调的是去重⼀定要对元素经⾏排序,这样我们才⽅便通过相邻的节点来判断是否重复使⽤了。
  • 排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更⾼!
    树层上去重(used[i - 1] == false)
    树枝上去重(used[i - 1] == true)
class Solution {
public:
    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]==0) continue;
            if(used[i]==1) continue;
            used[i]=1;
            path.push_back(nums[i]);
            backTracking(nums,used);
            path.pop_back();
            used[i]=0;
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<bool> used(nums.size(),0);
        backTracking(nums,used);
        return res;
    }
};

【332. 重新安排行程】

棋盘问题

【51. N 皇后】

  • 在单层搜索的过程中,每⼀层递归,只会选for循环(也就是同⼀⾏)⾥的⼀个元素,所以同行不⽤去重
class Solution {
public:
    vector<vector<string>> res;
    // n 为输⼊的棋盘⼤⼩
    // row 是当前递归到棋牌的第⼏⾏了
    bool isValid(int row,int col,int n ,vector<string>& CB){
        for(int i = 0;i<n;i++){// 检查列
            if(CB[i][col]=='Q') return false;
        }
        for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){// 检查 45度⻆是否有皇后
            if(CB[i][j]=='Q') return false;
        }
        for(int i = row-1,j=col+1;i>=0&&j<n;i--,j++){// 检查 135度⻆是否有皇后
            if(CB[i][j]=='Q') return false;
        }
        return true;
    }
    void BT(int n ,int row,vector<string>& CB){
        if(row==n){
            res.push_back(CB);
            return;
        }
        for(int col=0;col<n;col++){
            if(isValid(row,col,n,CB)){
                CB[row][col]='Q';
                BT(n,row+1,CB);
                CB[row][col]='.';
            }
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        vector<string> CB(n,std::string(n,'.'));
        BT(n,0,CB);
        return res;
    }
};

【37. 解数独】

  • 递归函数以及参数
    递归函数的返回值需要是bool类型,为什么呢?
    因为解数独找到⼀个符合的条件(就在树的叶⼦节点上)⽴刻就返回,相当于找从根节点到叶⼦节点⼀条唯⼀路径,所以需要使⽤bool返回值

  • 注意这⾥return false的地⽅,这⾥放return false 是有讲究的。
    因为如果⼀⾏⼀列确定下来了,这⾥尝试了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[i][col]==val) return false;// 判断列⾥是否重复
        }
        for(int j = 0;j<9;j++){
            if(board[row][j]==val) return false;// 判断⾏⾥是否重复
        }
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++) { // 判断9⽅格⾥是否重复
            for (int j = startCol; j < startCol + 3; j++) {
                if (board[i][j] == val ) return false;
            }
        }
        return true;
    }
    bool BT(vector<vector<char>>& board){
        for(int i = 0;i<board.size();i++){// 遍历⾏
            for(int j = 0;j<board[0].size();j++){// 遍历列
                if(board[i][j]!='.') continue;
                for(char k ='1';k<='9';k++){
                    if(isValid(i,j,k,board)){// (i, j) 这个位置放k是否合适
                        board[i][j]=k;// 放置k
                        if(BT(board)) return true;// 如果找到合适⼀组⽴刻返回
                        board[i][j]='.';// 回溯,撤销k
                    }
                }
                return false;// 9个数都试完了,都不⾏,那么就返回false
            }
        }
        return true;// 遍历完没有返回false,说明找到了合适棋盘位置了
    }
    void solveSudoku(vector<vector<char>>& board) {
        BT(board);
    }
};

——回溯问题复杂度分析

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值