day27 C++ 回溯算法 组合总和 组合总和Ⅱ 分割回文串

题目1:39组合总和

题目链接:组合总和

对题目的理解

整数数组无重复元素,找出数组中数字的和等于目标数target的不同组合,以列表形式返回,

数组中同一个数字可以无限制重复被选取,保证不同组合数少于150个。

数组中的元素在[2.40]之间,至少包含一个元素,     target在[1.40]

本题抽象成树形结果如图

注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回

回溯法

回溯三部曲

i)递归函数参数

定义两个全局变量,二维数组result存放全部结果集,数组path存放单个符合条件的结果。

题目中给出的参数,集合candidates, 和目标值target,还定义了int型的sum变量来统计单一结果path里的总和,还需要startIndex来控制for循环的起始位置

ii)递归终止条件

sum大于target就return,sum等于target的时候,需要收集结果

iii)单层搜索的逻辑

单层for循环依然是从startIndex开始,搜索candidates集合

代码的详细流程

代码(缺点:对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。)

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int sum, int targetsum, int startindex, vector<int>& candidates){
        //终止条件
        if(sum>targetsum) return;
        if(sum==targetsum){
            result.push_back(path);
            return;
        }
        //单层递归逻辑
        for(int i=startindex;i<candidates.size();i++){
            path.push_back(candidates[i]);
            sum+=candidates[i];
            backtracking(sum,targetsum,i,candidates);//递归。这里一定要是i,不能是startindex,代表这个for循环依次向下进行
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        backtracking(0, target, 0, candidates);
        return result;//整数数组至少包含一个元素,所以不用考虑数组为空的情况
        
    }
};

!!!注意:代码中for循环中的递归语句如果是如下语句,那么最终对应的示例结果中会出现重复的元素组合,这是为什么呢???

 backtracking(sum,targetsum,startindex,candidates);

绘制了一下流程图,可以很清晰地说明,在图最下方蓝框的部分,出现的那个startindex始终等于0,导致后面的元素又和前面已经遍历过的元素去比较,所以会出错

剪枝优化

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

在for循环上做文章,在求和问题中,排序之后加剪枝是常见的套路!

代码

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int sum, int targetsum, int startindex, vector<int>& candidates){
        //终止条件
        if(sum==targetsum){
            result.push_back(path);
            return;
        }
        //单层递归逻辑
        for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
            path.push_back(candidates[i]);
            sum+=candidates[i];
            backtracking(sum,targetsum,i,candidates);//递归。这里一定要是i,不能是startindex,代表这个for循环依次向下进行
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        sort(candidates.begin(), candidates.end());
        backtracking(0, target, 0, candidates);
        return result;//整数数组至少包含一个元素,所以不用考虑数组为空的情况
        
    }
};
  • 时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此
  • 空间复杂度: O(target)

题目2:40组合总和Ⅱ

题目链接:组合总和

对题目的理解

求解整数数组中数字和等于目标数targets的组合,每个数字只能使用1次,所有组合不能重复。数组中至少包含1个元素,并且数组中的元素大于等于1。

注意数组中可能存在重复的元素哦,所以数组中可能出现重复的组合,所以需要去重

本题的难点在于:集合(数组candidates)有重复元素,但还不能有重复的组合

如果把所有组合求出来,再用set或者map去重,这么做很容易超时!所以要在搜索的过程中就去掉重复组合。

组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。

Q:是要同一树层上使用过,还是同一树枝上使用过呢?

A:元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。

所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重,树层去重的话,需要对数组排序!

回溯法

回溯三部曲

i)递归函数参数

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

ii)递归终止条件

终止条件为 sum > target 和 sum == target

iii)单层搜索的逻辑

去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。

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

将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下

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

因为同一树层,used[i - 1] == false 才能表示当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的,而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示:

代码流程

代码

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex, vector<bool>& used){
        //终止条件
        if(sum>targetsum) return;
        if(sum==targetsum){
            result.push_back(path);
            return;
        }
        //单层遍历逻辑
        for(int i=startindex;i<candidates.size();i++){
            //去重
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
            path.push_back(candidates[i]);
            sum+=candidates[i];
            used[i]=true;
            backtracking(sum,targetsum,candidates,i+1,used);//递归
            used[i]=false;
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        sort(candidates.begin(), candidates.end());
        vector<bool> used(candidates.size(), false);
        backtracking(0, target, candidates,0,used);
        return result;
    }
};

剪枝(在for循环上做文章)

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex, vector<bool>& used){
        //终止条件
        //if(sum>targetsum) return;
        if(sum==targetsum){
            result.push_back(path);
            return;
        }
        //单层遍历逻辑
        for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
            //去重
            // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
            // used[i - 1] == false,说明同一树层candidates[i - 1]使用过
            // 要对同一树层使用过的元素进行跳过
            if(i>0 && candidates[i]==candidates[i-1] && used[i-1]==false) continue;
            path.push_back(candidates[i]);
            sum+=candidates[i];
            used[i]=true;
            backtracking(sum,targetsum,candidates,i+1,used);//递归
            used[i]=false;
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        sort(candidates.begin(), candidates.end());
        vector<bool> used(candidates.size(), false);
        backtracking(0, target, candidates,0,used);
        return result;
    }
};
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n)

使用startindex去重(很巧妙的一种思想)

去重的程序流程

代码

class Solution {
public:
    vector<int> path;
    vector<vector<int>> result;
    void backtracking(int sum,int targetsum,vector<int>& candidates,int startindex){
        //终止条件
        //if(sum>targetsum) return;
        if(sum==targetsum){
            result.push_back(path);
            return;
        }
        //单层遍历逻辑
        for(int i=startindex;i<candidates.size() && sum+candidates[i]<=targetsum;i++){
            //去重
            
            // 要对同一树层使用过的元素进行跳过
            if(i>0 && i>startindex && candidates[i]==candidates[i-1]) continue;
            path.push_back(candidates[i]);
            sum+=candidates[i];
        
            backtracking(sum,targetsum,candidates,i+1);//递归
        
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        path.clear();
        result.clear();
        sort(candidates.begin(), candidates.end());

        backtracking(0, target, candidates,0);
        return result;
    }
};

题目3:131分割回文串

题目链接:分割回文串

对题目的理解

将字符串s分割成回文串(正读反读相同)。列出所有的分割结果,字符串长度大于等于1

绘制树形结构图

切割问题类似于组合问题,也可以抽象为1个树形结构,如图

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

判断回文串

可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。

回溯法

回溯三部曲

i)递归函数参数

全局变量数组path存放切割后单个回文的子串,二维数组result存放所有结果集。

递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题保持一致的。

ii)递归函数终止条件

切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。

iii)单层搜索的逻辑

Q:在递归循环中如何截取子串呢?

for (int i = startIndex; i < s.size(); i++)循环中定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。

首先判断这个子串是不是回文,如果是回文,就加入在vector<string> path

注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1

代码逻辑

代码

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){
        //终止条件
        if(startindex==s.size()){
            result.push_back(path);
            return;
        }
        //单层遍历逻辑
        for(int i=startindex;i<s.size();i++){
            if(ispalidrome(s,startindex,i)){
                string str = s.substr(startindex, i-startindex+1);//使用substr提取子串,第一个数代表起始位置,第二个是代表提取子串的长度
                path.push_back(str);
            }
            else continue;
            backtracking(s,i+1);//递归
            path.pop_back();
        }
    }
    vector<vector<string>> partition(string s) {
        path.clear();
        result.clear();
        backtracking(s,0);
        return result;

    }
};
  • 时间复杂度: O(n * 2^n)
  • 空间复杂度: O(n^2)

本题难点

  • 切割问题可以抽象为组合问题
  • 如何模拟那些切割线,使用startindex模拟切割线,注意切割过的地方不能重复切割
  • 切割问题中递归如何终止,startindex切割到最后一个字符串之后
  • !!!!在递归循环中如何截取子串  子串就是[startindex, i]
  • 如何判断回文,使用双指针法,同时从首尾向中间移动,比较字符是否相等
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值