【代码随想录】——回溯题目

77. 组合

可以看出这个棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不在重复取。  可以发现n相当于树的宽度,k相当于树的深度图中每次搜索到了叶子节点,我们就找到了一个结果。相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合

递归函数的返回值以及参数

函数里一定有两个参数,既然是集合n里面取k的数,那么n和k是两个int型的参数

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

 回溯函数终止条件

path这个数组的大小如果达到k(path.size()==k),说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。 此时将path放进res中,并return

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(int n,int k,int StartIndex){
        //回溯函数终止条件
        if(path.size()==k){
            res.push_back(path);
            return;
        }
        for(int i=StartIndex;i<=n;i++){
            path.push_back(i);//处理结果
            backtracking(n,k,i+1);// 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return res;
    }
};

 剑指 Offer 38. 字符串的排列

可以类似于39组合综合,可以重复取数 

注意本题和上题全排列的区别,上题中【从左向右取数,取过的数,不在重复取】,但本题中除了总集合不能存在重复字符串,但【之前取过的数,满足一定条件可以重复取】

class Solution {
public:
    //全排列问题
    vector<string> res;
    
    void dfs(string& s,string& str,vector<bool>& visited){
        if(str.size()==s.size()){
            res.push_back(str);
            return;//找到一个结果之后,要return回去找其他结果
        }
        for(int i=0;i<s.size();i++){//由于同一层,元素需要从头开始取
            if(visited[i]==true) continue;
            if(i>0 && s[i]==s[i-1] && visited[i-1]==true){//和前一个一样的元素且前一个已取过值,继续往下
                continue;
            }
            str.push_back(s[i]);
            visited[i]=true;
            dfs(s,str,visited);//递归。每层的visited传递下去的
            str.pop_back();//回溯
            visited[i]=false;
        }
    }
    vector<string> permutation(string s) {
        sort(s.begin(),s.end());
        string str="";
        vector<bool> visited(s.size(),false);
        dfs(s,str,visited);
        return res;
    }
};

216. 组合总和 III

 需要如下参数:

  • targetSum(int)目标和,也就是题目中的n。
  • k(int)就是题目中要求k个数的集合。
  • startIndex(int)为下一层for循环搜索的起始位置。

回溯终止条件

特别注意如果path.size() == k 但sum != targetSum 直接返回 

if(path.size()==k){//树的深度达到
            if(n==0){//和符合要求
                res.push_back(path);
            }
            return;// 如果path.size() == k 但sum != targetSum 直接返回
        }

 整体代码

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(int k,int n,int StartIndex){
        if(path.size()==k){//树的深度达到
            if(n==0){//和符合要求
                res.push_back(path);
            }
            return;// 如果path.size() == k 但sum != targetSum 直接返回
        }
        for(int i=StartIndex;i<=9;i++){//之前自己写成i<=n,但题目给出的和n可能大于9,不符合题目要求的元素在1~9内
            n-=i;
            path.push_back(i);
            backtracking(k,n,i+1);
            n+=i;//回溯
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return res;
    }
};

已选元素总和如果已经大于n了(或者说n已经减到小于0了),那么往后遍历就没有意义了,直接剪掉。那么剪枝的地方一定是在递归终止的地方剪。

另外,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置。如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(int k,int n,int StartIndex){
        if(n<0) return;//已选的元素总和超过了n。剪枝操作

        if(path.size()==k){//树的深度达到
            if(n==0){//和符合要求
                res.push_back(path);
            }
            return;// 如果path.size() == k 但sum != targetSum 直接返回
        }
        for(int i=StartIndex;i<=9-(k-path.size())+1;i++){//之前自己写成i<=n,但题目给出的和n可能大于9,不符合题目要求的元素在1~9内。剪枝操作
            n-=i;//处理
            path.push_back(i);//处理
            backtracking(k,n,i+1);
            n+=i;//回溯
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return res;
    }
};

17. 电话号码的字母组合

解决如下三个问题:

  1. 数字和字母如何映射
  2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来
  3. 输入1 * #按键等等异常情况

数字和字母如何映射

可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,这里定义一个二维数组

回溯法来解决n个for循环的问题

遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果

确定回溯函数参数

需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来

确定终止条件

 终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。然后收集结果,结束本层递归。

class Solution {
public:
    //二位数组用来实现数字和字母的映射
    const string letterMap[10]={
        "",//0
        "",//1
        "abc",//2
        "def",//3
        "ghi",//4
        "jkl",//5
        "mno",//6
        "pqrs",//7
        "tuv", // 8
        "wxyz", // 9
    };
    vector<string> res;
    string s;
    //index代表digits中的第index个数字
    void backtracking(string& digits,int index){

        if(index==digits.size()){//长度符合要求
            res.push_back(s);
            return;
        }
        
        int digit=digits[index]-'0';//将字符转化成数字
        string letter=letterMap[digit];//数字对应的字符串
        for(int i=0;i<letter.size();i++){
            s.push_back(letter[i]);//处理
            backtracking(digits,index+1);//递归
            s.pop_back();//回溯
        }
    }
    vector<string> letterCombinations(string digits) {
        if(digits.size()==0) return res;//字符串digits本身是空的
        backtracking(digits,0);
        return res;
    }
};

 39. 组合总和

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

//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复 

//下一层递归还是从原来的位置开始,表明可以重复取数

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target,int sum,int startIndex){
        if(sum>target) return;
        if(sum==target){
            res.push_back(path);
            return;
        }
        for(int i=startIndex;i<candidates.size();i++){//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复
            sum+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,i);//下一层递归还是从原来的位置开始,表明可以重复取数
            sum-=candidates[i];
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtracking(candidates,target,0,0);
        return res;
    }
};

剪枝优化 (排序+剪枝+回溯)

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

其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。那么可以在for循环的搜索范围上做做文章了。

 在求和问题中,排序之后加剪枝是常见的套路!!!!(因为排序之后的candicate[i+1]肯定比前一个candicate[i]大,如果candicate[i]这个都不符合要求,后续就可以不做判断了)

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target,int sum,int startIndex){
        
        if(sum==target){
            res.push_back(path);
            return;
        }
        for(int i=startIndex;i<candidates.size() && sum+candidates[i]<=target;i++){//for循环中,用startIndex控制起始点位置,这是避免2,2,3和3,2,2这样的重复
            sum+=candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates,target,sum,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,0);
        return res;
    }
};

40. 组合总和 II

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的,而39.组合总和 (opens new window)是无重复元素的数组candidates

最后本题和39.组合总和 (opens new window)要求一样,解集不能包含重复的组合。

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

所谓去重,其实就是使用过的元素不能重复选取。

***那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

***如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

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

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target,int sum,int startIndex,vector<bool>& used){
        
        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] && used[i-1]==false) continue;//同一树层使用过candidates[i - 1],去重

            sum+=candidates[i];//处理
            path.push_back(candidates[i]);//处理
            used[i]=true;//处理
            backtracking(candidates,target,sum,i+1,used);//递归
            sum-=candidates[i];//回溯
            path.pop_back();//回溯
            used[i]=false;//回溯
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(),false);//初始化为false
        sort(candidates.begin(),candidates.end());
        backtracking(candidates,target,0,0,used);
        return res;
    }
};

131. 分割回文串

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

  1. 切割问题,有不同的切割方式
  2. 判断回文(可利用双指针法判断)

 其实切割问题类似组合问题

递归函数参数:全局变量数组path存放切割后回文的子串二维数组res存放结果集。递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的

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

在循环内,先判断是否是回文串,如果是就截取放进path里面,不是的话就continue跳出

class Solution {
public:
    vector<vector<string>> res;
    vector<string> path;
    void backtracking(string& s,int stratIndex){
        //终止条件
        if(stratIndex>=s.size()){
            res.push_back(path);
            return;
        }
        for(int i=stratIndex;i<s.size();i++){
            if(isPalindrome(s,stratIndex,i)){//判断是否是回文串
                string str=s.substr(stratIndex,i-stratIndex+1);
                path.push_back(str);
            }else{
                continue;//如果不是回文串,就跳过
            }
            backtracking(s,i+1);//递归
            path.pop_back();
        }
    }
    bool isPalindrome(string& s,int begin,int end){//双指针法判断是否回文
        for(int ii=begin,j=end;ii<j;ii++,j--){
            if(s[ii]!=s[j]) return false;
        }
        return true;
    }
    vector<vector<string>> partition(string s) {
        backtracking(s,0);
        return res;
    }
};

93. 复原 IP 地址

切割问题

参数

startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。

本题我们还需要一个变量pointNum,记录添加逗点的数量

终止条件

本题明确要求只会分成4段,以分割的段数作为终止条件。pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。然后验证一下第四段是否合法,如果合法就加入到结果集里

递归逻辑

需要判断【startIndex,i】区间内子串是否合法。如果合法就在字符串后面加上符号.表示已经分割。

递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符.,同时记录分割符的数量pointNum 要 +1。 

最后就是在写一个判断段位是否是有效段位了

主要考虑到如下三点:

  • 段位以0为开头的数字不合法
  • 段位里有非正整数字符不合法(>9或者<0)
  • 段位如果大于255了不合法
class Solution {
public:
    vector<string> res;
    //string str;
    void backtracking(string& s,int startIndex,int pointNum){
        if(pointNum==3){//分成了4个部分
            if(isValid(s,startIndex,s.size()-1)){//判断第四部分是否合法
                res.push_back(s);
            }
            return;
        }
        for(int i=startIndex;i<s.size();i++){
            if(isValid(s,startIndex,i)){//判断 [startIndex,i] 这个区间的子串是否合法
                s.insert(s.begin()+i+1,'.'); // 在i的后面插入一个逗点
                pointNum++;
            }else{
                return;//否则,跳过
            }
            backtracking(s,i+2,pointNum);//下一层递归,由于插入了逗点,因此下一层递归要从i+2处开始
            s.erase(s.begin()+i+1);// 回溯删掉逗点
            pointNum--;
        }
    }
    bool isValid(string& s,int begin,int end){
        if(begin>end) return false;
        if(s[begin]=='0' && begin!=end) return false;//表明是0开头的字段,begin!=end避免了单独是'0'的字段
        int num=0;
        for(int i=begin;i<=end;i++){
            if(s[i]>'9' || s[i]<'0') return false;
            num=10*num+(s[i]-'0');
            if(num>255) return false;
        }
        return true;
    }
    vector<string> restoreIpAddresses(string s) {
        backtracking(s,0,0);
        return res;
    }
};

78. 子集

如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点 

找树的所有节点就不需要剪枝,把所有的path都放进res中

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    void backtracking(vector<int>& nums,int startIndex){
        res.push_back(path);//不需要剪枝,所有节点都存放到结果集中去
        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类似

给出的数组里有重复元素,但结果集里不能有重复元素。

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

所谓去重,其实就是使用过的元素不能重复选取。

***那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重。

***如果candidates[i] == candidates[i - 1] 并且 used[i - 1] == false,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]

  • used[i - 1] == true,说明同一树枝candidates[i - 1]使用过
  • used[i - 1] == false,说明同一树层candidates[i - 1]使用过
class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex,vector<bool>& used){
        res.push_back(path);
        for(int i=startIndex;i<nums.size();i++){
            if(i>0 && nums[i]==nums[i-1] && used[i-1]==false) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            path.pop_back();
            used[i]=false;
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums,0,used);
        return res;
    }
};

 491. 递增子序列

本题求自增子序列,是不能对原数组经行排序的,排完序的数组都是自增子序列了。所以不能使用之前的去重逻辑!(之前是排序+标记数组达到去重目的)

单层搜索逻辑

  

同一父节点下的同层上使用过的元素就不能在使用了

习惯写回溯的同学,看到递归函数上面的uset.insert(nums[i]);,下面却没有对应的pop之类的操作,应该很不习惯吧。这也是需要注意的点,unordered_set<int> uset; 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex){
        if(path.size()>=2){
            res.push_back(path);
            // 注意这里不要加return,因为要取树上所有长度大于等于2的节点。(也就是话可以往下走)
        }
        unordered_set<int> used;//用来记录本层节点,看看是否使用过。// 使用set来对本层元素进行去重
        for(int i=startIndex;i<nums.size();i++){
            if((!path.empty() && nums[i]<path.back()) || used.find(nums[i])!=used.end()) continue;//待放入的元素小于末尾元素,构不成升序。或者在set中存在同一父节点下的重复节点
            used.insert(nums[i]);//不存在重复节点,就把nums[i]存入used中
            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里都放了哪些元素了

 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]==true) continue;//跳出本层循环,到i++那步
            used[i]=true;
            path.push_back(nums[i]);
            backtracking(nums,used);
            used[i]=false;
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(),false);//直接利用used数组去重
        backtracking(nums,used);
        return res;
    }
};

 47. 全排列 II

此题和上题全排列的区别在于:此题原数组元素有可能有重复,因此用数组used不仅要去同一层的重,还要去同一树枝的重。

排序+回溯+used数组去重

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((i>0 && nums[i]==nums[i-1] && used[i-1]==false) || used[i]==true) continue;//去同一层和同一树枝的重
            used[i]=true;//处理
            path.push_back(nums[i]);//处理
            backtracking(nums,used);//开启下一层递归
            used[i]=false;//回溯
            path.pop_back();//回溯
        } 
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(),nums.end());//先排序
        vector<bool> used(nums.size(),false);
        backtracking(nums,used);
        return res;
    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值