第二十九天 | 39.组合总和 40.组合总和|| 131.分割回文串

题目:39.组合总和

读完题感觉不是那么难?与77.组合的区别不就是每层递归for循环所遍历的范围么?本题是不用去重的。

也就是说这道题深度未知

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    int sum = 0;
    void backtracking(vector<int>& candidates, int index, int target){
        if(sum == target){
            result.push_back(path);
        }
        if(index == 4) return;     //这个终止条件是有问题的。谁说的最多只能放四个元素在path里?

        for(int i = 0; i < candidates.size(); i++){
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, index + 1, target);
            path.pop_back();
            sum -= candidates[i];
        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        if(candidates.size() == 0) return result;
        backtracking(candidates, 0, target);
        return result;
    }
};

写出了这样的代码,错了,因为例如[2,3,2][2,2,3]重复统计

怎样避免统计重复的情况呢?

其实这道题的数据本身就帮我们省了很多事:不含0元素,无重复元素(不用去重)

本题正确的终止条件应该是:sum > target

那么index在这道题里有什么用呢?

以上这两个问题,归结于一个解决方法(也是纠正错误),那就是本题还是需要startIndex而是不是index。

首先辨析一下本题和77.组合,对于”不重复“这一要求有什么不同。77.组合要求不同的path不能是同样的元素组成,每一个path里也不能出现相同的元素。而这一道题只是要求不同的path不能是同样的元素组成,但在一个path里可以出现相同的数。

深刻理解这一点,就可以找到避免两个path相同的办法,那就是设置startIndex,规定下一个for循环从哪里开始搜索。

修改后的代码如下:

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    int sum = 0;
    void backtracking(vector<int>& candidates, int startIndex, int target){
        if(sum == target){
            result.push_back(path);
        }
        if(sum > target) return;

        for(int i = startIndex; i < candidates.size(); i++){
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, i, target);   //不用i + 1,表示可以去与当前重复的数
            path.pop_back();
            sum -= candidates[i];
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        if(candidates.size() == 0) return result;
        backtracking(candidates, 0, target);
        return result;
    }
};

剪枝的操作:

        对数组先排序,当有一个组合总和sum大于target了,那么他后面的元素也没有必要再进行递归了。

剪枝代码如下:

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    int sum = 0;
    void backtracking(vector<int>& candidates, int startIndex, int target){
        if(sum == target){
            result.push_back(path);
        }
        if(sum > target) return;

        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){     //如果不满足,提前终止for循环,实现剪枝
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, i, target);   //不用i + 1,表示可以去与当前重复的数
            path.pop_back();
            sum -= candidates[i];
        }
    }

    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        if(candidates.size() == 0) return result;
        backtracking(candidates, 0, target);
        return result;
    }
};

题目:40.组合总和||
读完题一阵兴奋,在深刻理解77.组合和39.组合总和||的联系与差别之后,这道题可以说是信手拈来了,与77.组合相比,则不规定每一条path的元素个数,与39.组合总和||相比,每一条path里可以出现相同的元素。

for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){

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

            path.push_back(candidates[i]);

            sum += candidates[i];

            backtracking(candidates, i + 1, target, sum);     //传入i + 1而不是i,不能取与当前相同的数

            path.pop_back();

            sum -= candidates[i];

        }

但这样是不对的,因为这样的话在下一层里会直接跳过1了,漏掉了[1,1,6]这种情况。

怎么解决?

猜测:如果要改的话应该将去重写在for循环里,而不能在循环体内,应该怎么写呢?(该猜测是错的)

这道题涉及到了回溯中一个重要的操作:去重

去重分两种:数层去重和树枝去重。本题需要做的是树层去重

第一种改法(来自b站评论):

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

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

        修改完的代码,可以保证每一层的递归for循环里,第一个遍历到的元素candidates[startIndex]肯定会被用一次,避免了因为candidates[startIndex] = candidates[startIndex - 1]而被忽略的情况,也就是说candidates[startIndex]至少会被用一次。

        这样写统一了每一层for循环里的if判断。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int startIndex, int target, int sum){
        if(sum == target){       //收获结果
            result.push_back(path);
            return;
        }

        if(sum > target){
            return;
        }
        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++){
            if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, i + 1, target, sum);     //传入i + 1而不是i,不能取与当前相同的数
            path.pop_back();
            sum -= candidates[i];
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        if(candidates.size() == 0) return result;
        backtracking(candidates, 0, target, 0);
        return result;
    }
};

第二种改法(来自标答):

借助used数组。

定义bool类型数组used来标记每一个数字被用过没有。

class Solution {
public:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int startIndex, int target, int sum, vector<bool>& used){
        if(sum == target){       //收获结果
            result.push_back(path);
            return;
        }

        if(sum > target){
            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;
            }
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            path.push_back(candidates[i]);
            sum += candidates[i];
            used[i] = true;
            backtracking(candidates, i + 1, target, sum, used);     //传入i + 1而不是i,不能取与当前相同的数
            path.pop_back();
            sum -= candidates[i];
            used[i] = false;
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);
        //首先把candidates排序,让其相同的数紧挨在一起
        sort(candidates.begin(), candidates.end());
        if(candidates.size() == 0) return result;
        backtracking(candidates, 0, target, 0, used);
        return result;
    }
};

比较而言,利用used来标记状态是更普遍的做法。(第一种不一定每次都能想到)

题目:131.分割回文串(有点难理解,主要切割线很陌生)

尝试作答:

正常递归加回溯,每一层的终止条件里判断目前path是否是回文串,如果是则加入result。这样设置终止条件是否略显冗杂?

其实以上想法是错误的,如果还是像以前做过的题那种收集path的话,每次都是用第一个元素开头。本题应该考虑的是选取切割的位置。

那么,代码怎么来表示切割呢?

传入的startIndex就是切割线。本层字串的范围为startIndex到i。在单层递归逻辑里进行是否满徐回文的判断。

substr是C++语言函数,主要功能是复制子字符串,要求从指定位置开始,并具有指定的长度。

        substr的两个参数分别是:起始位置和长度

class Solution {
public:
    vector<string> path;
    vector<vector<string>> result;
    bool isPalidrome(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;
    }
    void backtracking(const string& s, int startIndex){
        // 如果起始位置已经等于s的大小,说明已经找到了一组分割方案了
        if(startIndex == s.size()){
            result.push_back(path);
        }

        for(int i = startIndex; i < s.size(); i++){
            if(isPalidrome(s, startIndex, i)){              //这一坨传参数好绕,主要想不明白切割线的位置
                string str = s.substr(startIndex, i - startIndex + 1);
                path.push_back(str);
                //path里装了本条切割路径得到的所有子串
                //if判断放在循环体内,保证了加入path的所有子串必是回文的
            }
            else{
                continue;
            }
            backtracking(s, i + 1);
            path.pop_back();
        }
    }
    vector<vector<string>> partition(string s) {
        backtracking(s, 0);
        return result;
    }
};

终止条件有点难理解

传参很绕:传startIndex还是startIndex + 1? 传 i 还是 i + 1?

本题难点:

  • 切割问题其实类似组合问题
  • 如何模拟那些切割线
  • 切割问题中递归如何终止
  • 在递归循环中如何截取子串
  • 如何判断回文
  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值