代码随想录1刷—回溯篇(一)

回溯理论基础

回溯是递归的副产品,只要有递归就会有回溯。

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。虽然回溯法是个非常低效的办法,但一些问题只能用回溯来解决。还没有更高效的解法。

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

回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。

回溯算法理论基础
void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

77. 组合

77.组合

每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围。(因此需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历)

n相当于树的宽度,k相当于树的深度图中每次搜索到了叶子节点,我们就找到了一个结果

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;
    }
};
剪枝处理

可以剪枝的地方就在递归中每一层的for循环所选择的起始位置如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了。比如n=4,k=4的情况下,只要选1,2,3,4就完成了,选2,3,4根本无法满足。

优化过程如下:

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

为什么有个+1呢,因为包括起始位置是一个左闭的集合。举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。从2开始搜索都是合理的,可以是组合[2, 3, 4]。

//	将for循环进行优化剪枝
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置
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 - (k-path.size()) + 1;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;
    }
};

216. 组合总和 III

处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k,int n,int startIndex,int sum){
        if(path.size() == k){
            if(sum == n)
                result.push_back(path);
            else
                return;
        }
        for(int i = startIndex;i <= 9;i++){
            sum += i;
            path.push_back(i);
            backtracking(k,n,i + 1,sum);    //递归
            sum -= i;
            path.pop_back();   //回溯
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear();
        path.clear();
        backtracking(k,n,1,0);
        return result;
    }
};
剪枝处理

已选元素总和如果已经大于n了,那么往后遍历就没有意义了,直接剪掉。

if(sum > n){	//剪枝优化
	return;
}
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(int k,int n,int startIndex,int sum){
        if(sum > n){
	        return;
        }
        if(path.size() == k){
            if(sum == n)
                result.push_back(path);
            else
                return;
        }
        for(int i = startIndex;i <= 9;i++){
            sum += i;
            path.push_back(i);
            backtracking(k,n,i + 1,sum);    //递归
            sum -= i;
            path.pop_back();   //回溯
        }
    }
public:
    vector<vector<int>> combinationSum3(int k, int n) {
        result.clear();
        path.clear();
        backtracking(k,n,1,0);
        return result;
    }
};

17. 电话号码的字母组合

数字和字母的映射

可以使用map或者定义一个二维数组。

const string letterMap[10] = {
    "", // 0
    "", // 1
    "abc", // 2
    "def", // 3
    "ghi", // 4
    "jkl", // 5
    "mno", // 6
    "pqrs", // 7
    "tuv", // 8
    "wxyz", // 9
};
17. 电话号码的字母组合
class Solution {
private:
    const string letterMap[10] = {
        "",
        "",
        "abc",
        "def",
        "ghi",
        "jkl",
        "mno",
        "pqrs",
        "tuv",
        "wxyz",
    };

    vector<string>  result;
    string s;
    void backtracking(const string& digits , int index){
        		//const string&为常引用
        //引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。
        //index 为记录遍历第几个数字了,就是用于遍历digits的
        if(index == digits.size()){    //终止条件
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';    //  将digits[index]指向的字符转为int
        string letters = letterMap[digit];  //  取得数字对应的字符集
        for(int i = 0;i < letters.size();i++){
            //注意:这里for循环,可不像是在[77. 组合]和[216. 组合总和 III]中从startIndex开始遍历的。
            //因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而76和216的题都是是求同一个集合中的组合
            s.push_back(letters[i]);
            backtracking(digits,index + 1);
            s.pop_back();
        }
    }
public:
    vector<string> letterCombinations(string digits) {
        s.clear();
        result.clear();
        if(digits.size() == 0){
            return result;
        }
        backtracking(digits,0);
        return result;
    }
};  
回溯藏在递归参数中的写法

在递归参数和递归函数使用两句话中有变化,其他部分都一样,这种写法不直观,不建议这样写,但要理解。

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> result;
    void getCombinations(const string& digits, int index, const string& s) { 
        												// 注意参数的不同
        if (index == digits.size()) {
            result.push_back(s);
            return;
        }
        int digit = digits[index] - '0';
        string letters = letterMap[digit];
        for (int i = 0; i < letters.size(); i++) {
            getCombinations(digits, index + 1, s + letters[i]);  
            										// 注意这里的不同
        }
    }
    vector<string> letterCombinations(string digits) {
        result.clear();
        if (digits.size() == 0) {
            return result;
        }
        getCombinations(digits, 0, "");
        return result;

    }
};

39. 组合总和

本题和77题和216题的区别是:本题没有数量要求,可以无限重复,但是有总和的限制。

在77和216题中知道要递归K层,因为要取k个元素的组合。但本题不是,注意图中叶子节点的返回条件,因为本题没有组合数量要求,是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回!

39.组合总和

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates,int target,int sum,int startIndex){
        if(sum > target){
            return;
        }
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex;i < candidates.size();i++){
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i);  //不用i+1了,因为可以重复读取数值
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        backtracking(candidates,target,0,0);
        return result;
    }
};
剪枝处理

对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。所以可以在for循环的搜索范围上做做文章了。

对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历。注意:是排序之后!!

39.组合总和1

for循环剪枝代码如下:

for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates,int target,int sum,int startIndex){
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex;i < candidates.size() && sum + candidates[i] <= target;i++){
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i);  //不用i+1了,因为可以重复读取数值
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(),candidates.end());      //记得排序
        backtracking(candidates,target,0,0);
        return result;
    }
};

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

40. 组合总和 II

  1. 本题candidates 中的每个元素在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的
  3. 解集不能包含重复的组合。

难点就在于2和3,在搜索的过程中需要去掉重复组合,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。所以要去重的是同一树层上的“使用过”,而同一树枝上的都是一个组合里的元素,不用去重。另一方面需要注意的是:树层去重的话,需要对数组排序!

40.组合总和II

bool型数组used:是用来记录同一树枝上的元素是否使用过。这个集合去重的重任就是used来完成的。

40.组合总和II1

可以看出在candidates[i] == candidates[i - 1]相同的情况下:

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过

如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,说明前一个树枝使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1],此时for循环里就应该做continue的操作。

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates,int target,int sum,int startIndex,vector<bool>& used){
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex;i < candidates.size() && sum + candidates[i] <= target;i++){
            if(i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false){
                continue;
            }   //去重
            sum += candidates[i];
            path.push_back(candidates[i]);
            used[i] = true;
            backtracking(candidates,target,sum,i + 1,used); //i是元素可以重复,i+1是元素不可以重复
            used[i] = false;
            sum -= candidates[i];
            path.pop_back();
        }
    }
public:
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(),false);
        path.clear();
        result.clear();
        sort(candidates.begin(),candidates.end());  //一定要记得排序!
        backtracking(candidates,target,0,0,used);
        return result;
    }
};
题外话:continue和break的区别

continue语句的作用是跳过本次循环体中余下尚未执行的语句,立即进行下一次的循环条件判定,可以理解为仅结束本次循环。而break会直接结束该循环。

131. 分割回文串

本题涉及到两个关键问题:

  1. 切割问题
  2. 切割后判断回文

其实切割问题类似组合问题。例如对于字符串abcdef:

  • 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中在选组第三个…。
  • 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…。
131.分割回文串

递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。

class Solution {
private:
    bool isPalindrome(const string& s,int start,int end){
        for(int i = start,j = end;i<j;i++,j--){
            if(s[i]!=s[j]){
                return false;
            }
        }
        return true;
    }
    vector<vector<string>>  result;
    vector<string>  path;
    void backtracking(const string& s,int startIndex){
        if(startIndex >= s.size()){
            result.push_back(path);
            return;
        }
        for(int i = startIndex;i < s.size();i++){
            if(isPalindrome(s,startIndex,i)){   //是回文子串
                //获取[startIndex,i]在s中的子串
                string str = s.substr(startIndex,i - startIndex + 1);  
                             //substr 复制子字符串(从指定位置开始,指定的长度)
                path.push_back(str);
            }else{
                continue;
            }
            backtracking(s,i + 1);  //切割过的位置不可以重复切割,所以i+1
            path.pop_back();
        }
    }
public:
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        backtracking(s,0);
        return result;
    }
};

93. 复原 IP 地址

131.分割回文串
class Solution {
private:
    vector<string>  result;
    void backtracking(string& s, int startIndex,int pointNum){
        if(pointNum == 3){   // 三个点分成四段
            //判断第四段是否合法
            if(isValid(s,startIndex,s.size()-1)){
                result.push_back(s);
            }
            return;
        }
        for(int i = startIndex;i < s.size();i++){
            if(isValid(s,startIndex,i)){
                s.insert(s.begin() + i + 1,'.');
                pointNum++;
                backtracking(s,i + 2,pointNum);   //因为加了个点 所以从i+1变成i+2了
                pointNum--;
                s.erase(s.begin() + i + 1);
            }else break;    //不合法直接结束本层循环
        }
    }
    bool isValid(const string& s,int start,int end){
        if(start > end) return false;
        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');
            if(num > 255){
                return false;
            }
        }
        return true;
    }
public:
    vector<string> restoreIpAddresses(string s) {
        result.clear();
        if(s.size() < 4 || s.size() > 12) 
            return result;
        backtracking(s,0,0);
        return result; 
    }
};

78. 子集

78.子集

剩余集合为空的时候,就是叶子节点。那么什么时候剩余集合为空呢?实际上,当startIndex已经大于数组的长度了,就终止了,因为没有元素可取了。

需要注意的是,for (int i = startIndex; i < nums.size(); i++) 中如果startIndex >= nums.size(),for循环也就结束了,所以这个终止条件可加可不加,不影响最终结果。

求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex){
        result.push_back(path); 
        	//注意要在终止条件之前收集子集,否则会漏掉终止时的那个集合
        if(startIndex >= nums.size()){  
            //由于后面for循环的条件,此终止条件判断可加可不加,为了逻辑的完整性还是加上
            return;
        }
        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) {
        result.clear();
        path.clear();
        backtracking(nums,0);
        return result;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

97Marcus

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值