Leetcode 回溯算法全家桶

回溯:

在分享Leetcode这些经典回溯算法题之前,我们首先来聊聊什么是回溯算法。回溯算法就是一个暴力枚举,搜索尝试的过程,在发现列举情况不符合问题解时,我们这时会回溯,尝试别的路径。既然是暴力枚举,那么时间复杂度必然很高,那么我们为什么要学习回溯算法呢?因为有的题目,能够暴力枚举出来,不重不漏已经是很不容易了。


剪枝:

前面我们说到回溯算法时,已经强调了它有时间复杂度较高这一缺点,而剪枝操作正可以优化我们解题过程中的时间复杂度,具体怎么优化呢?在暴力枚举,递归的过程中,常常会形成一个树形结构,我们可以根据问题情况和已有条件来剪掉树形结构中冗余的枝条,也就是在暴力枚举时省去一些不必要的递归过程,那么我们的时间复杂度和空间复杂度也就优化了


Leetcode.77 组合

 这是解题的核心思想的一个流程图:

image.png

回溯算法代码如下:

class Solution {
public:
    //利用回溯算法来求解
    void backtracking(vector<vector<int>>&ans,vector<int>&res,int cur,int k,int n){
            //回溯终止的条件,加入k个数
            if(res.size()==k){
                ans.push_back(res);
                return;
            }
              for(int i=cur;i<=n;i++)
              {
              res.push_back(i);
              backtracking(ans,res,i+1,k,n);
              //回溯算法的核心,退一步重新选择
              res.pop_back();
              }
    }
    vector<vector<int>> combine(int n, int k) {
           vector<vector<int>>ans;//记录最后的答案并返回
           vector<int>res;//记录不同的组合
           backtracking(ans,res,1,k,n);
           return ans;
    }
};

剪枝:在我们进行某一轮取数时,如果还能取的数小于k个,那么这次取数过程一定不能形成一个组合,这时我们不再进行取数,而是跳过。举个比较简单的例子,如果还需要你4个数一个组合,而此时你只有3,4,5这三个数可选,那么再进行枚举取数是不是毫无意义呢?由具体到一般的,记录组合的数组的长度是我们已取的数的数量,那么k-res.size是我们还需要取的数的数量,如果这个数量大于n-i+1(还可以取数的数量),那么剪枝,代码如下

class Solution {
public:
    //利用回溯算法来求解
    void backtracking(vector<vector<int>>&ans,vector<int>&res,int cur,int k,int n){
            //回溯的终止条件
            if(res.size()==k){
                ans.push_back(res);
                return;
            }

            if(cur>n){
                return;
            }
              //进行剪枝操作,此时i<=n-(k-res.size())+1
              for(int i=cur;i<=n-(k-res.size())+1;i++)
              {
              res.push_back(i);
              backtracking(ans,res,i+1,k,n);
              res.pop_back();
              }
    }
    vector<vector<int>> combine(int n, int k) {
           vector<vector<int>>ans;
           vector<int>res;
           backtracking(ans,res,1,k,n);
           return ans;
    }
};

Leetcode.216 组合总和III

 回溯算法的代码如下:

class Solution {
public:
    //最终返回的答案
    vector<vector<int>>res;
    //保存一组符合的组合
    vector<int>ans;
    void backtraking(int k,int n,int index){
    //回溯的终止条件:保存k个数时回溯,如果相加之和为n保存进答案数组
    if(ans.size()==k){
        if(n==0) res.push_back(ans);
        return;
    }
    for(int i=index;i<=9;i++){
        ans.push_back(i);
        n-=i;
        backtraking(k,n,i+1);
        //回溯算法的核心
        n+=i;
        ans.pop_back();
    }

    }
    vector<vector<int>> combinationSum3(int k, int n) {
    //使用数字1—9,所以初始传入1
    backtraking(k,n,1);
    return res;
    }
};

剪枝操作的核心思想和上一题的大致相同,我们需要保证剩下的数的数量要大于等于我们还需要取的数的数量,否则直接跳过,也就是进行剪枝

class Solution {
public:
    //最终返回的答案
    vector<vector<int>>res;
    //保存一组符合的组合
    vector<int>ans;
    void backtraking(int k,int n,int index){
    //回溯的终止条件:保存k个数时回溯,如果相加之和为n保存进答案数组
    if(ans.size()==k){
        if(n==0) res.push_back(ans);
        return;
    }
    //进行剪枝操作,保证剩余的数(9-i+1)>=我们还需要取的数(k-ans.size())
    for(int i=index;i<=10-k+ans.size();i++){
        ans.push_back(i);
        n-=i;
        backtraking(k,n,i+1);
        //回溯算法的核心
        n+=i;
        ans.pop_back();
    }

    }
    vector<vector<int>> combinationSum3(int k, int n) {
    //使用数字1—9,所以初始传入1
    backtraking(k,n,1);
    return res;
    }
};

Leetcode.17 电话号码的字母组合

对于本题,其本质仍然是一个简单的组合问题,可以用回溯算法来解决,但是它又绕了一个弯,需要我们通过数字获取对应的字符串,再进行组合

回溯算法的代码如下:

class Solution {
public:
    //建立一个映射表,通过索引获取相应的字符串
    vector<string>numToString{"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};

    //储存最终答案的数组
    vector<string>res;

    //储存每一组符合的字符串
    string combination;
    void backtraking(string digits,int index){
    //回溯的终止条件
    if(index==digits.size()){
        res.push_back(combination);
        return;
    }
    //取出对应的字符串,数字2对应的数组索引为0,以此类推
    string tmp=numToString[digits[index]-'0'-2];
    for(int i=0;i<tmp.size();i++){
        combination.push_back(tmp[i]);
        backtraking(digits,index+1);
        combination.pop_back();
    }
    }
    vector<string> letterCombinations(string digits) {
    //特殊判断:如果是空字符串,返回空数组
    if(digits==""){
        return res;
    }
    backtraking(digits,0);
    return res;
    }
};

Leetcode.39 组合总和

这题与以上组合题的最大不同点是可以无限重复选取数字,因此我们再进行暴力搜索时需要一直对当前索引进行重复尝试

class Solution {
public:     
    vector<vector<int>>res;
    vector<int>combination;

    void backtraking(vector<int>&candidates,int index,int target){
    //回溯的终止条件
    if(target<=0){
    if(target==0)res.push_back(combination);
    return;
    }

    for(int i=index;i<candidates.size();i++){
    //剪枝,如果当前数已经比target大了,那么跳过
    if(target<candidates[i]){
        continue;
    }
    combination.push_back(candidates[i]);
    target-=candidates[i];
    //此时不再是i+1,而依然是i,对当前索引进行重复尝试
    backtraking(candidates,i,target);
    target+=candidates[i];
    combination.pop_back();
    }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
    backtraking(candidates,0,target);
    return res;
    }
};

Leetcode.40 组合总和II

这道题的集合当中是存在重复元素的,因此我们再按照以往的方法来进行回溯的话会算进去重复组合,因此对于这道题,我们需要掌握一种新的技巧:去重

去重:

去重,简单来说就是不要选取重复的元素,那么去重也分为两种:一种是树枝去重,一种是树层去重。下面是我对这两个去重的解释:树枝去重就是指在一次递归中前面选取完这一元素,后面就不可以再次选取,这更像是一种纵向去重。树层去重就是说在同一层递归的for循环中,如果前面某一相同元素被选取过,自己就不再被选取。

而本题很显然是让你进行树层上的去重,举个最简单的例子,加入有三个元素1,7,1,如果让你选取元素使总和达到9,那么1,7,1都可以选,为一种组合方案。但是如果让你选取元素使总和达到8,那么选取的1,7和7,1就重复了,我们要对这部分进行去重处理。

 本题的代码如下:

class Solution {
public:

    vector<vector<int>>res;
    vector<int>combination;

    void backtraking(vector<int>&candidates,int index,int target,vector<bool>&used){
    if(target<=0){
        if(target==0)res.push_back(combination);
        return;
    }
    for(int i=index;i<candidates.size();i++){
        //去重加剪枝操作
        //进行树层的去重,而不用进行树枝上的去重
        if(i>=1&&candidates[i]==candidates[i-1]&&!used[i-1])continue;
        //因为已经排过序(升序),后面的数都大于target,因此可以直接跳过此次循环
        if(target<candidates[i])break;
        target-=candidates[i];
        used[i]=true;
        combination.push_back(candidates[i]);
        backtraking(candidates,i+1,target,used);
        combination.pop_back();
        used[i]=false;
        target+=candidates[i];
    }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
    vector<bool>used(candidates.size(),false);
    sort(candidates.begin(),candidates.end());
    backtraking(candidates,0,target,used);
    return res;
    }
};

Leetcode.131 分割回文串

lass Solution {
public:
    vector<vector<string>>res;
    vector<string>ans;
    //判断该字符串是否是回文串
    bool isValid(string str){
    int left=0;
    int right=str.size()-1;
    while(left<=right){
        if(str[left]!=str[right])return false;
        left++;
        right--;
    }
    return true;
    }
    void backtraking(string s,int index){
    if(index==s.size()){
        res.push_back(ans);
        return;
    }
    for(int i=index;i<s.size();i++){
    string str=s.substr(index,i-index+1);
    //如果不有效就直接跳过
    if(!isValid(str))continue;
    ans.push_back(str);
    backtraking(s,i+1);
    ans.pop_back();
    }
    }
    vector<vector<string>> partition(string s) {
    backtraking(s,0);
    return res;
    }
};

Leetcode.93 复原IP地址

class Solution {
public:
    vector<string>res;
    //判断字符串s从i到j部分是否是回文串
    bool isValid(string s,int i,int j){
    //i>j只有在一种可能时会发生,那就是第三个.后面不再有字符
    if(i>j) return false;
    //不能含有前导0
    if(s[i]=='0'&&i!=j) return false;
    int sum=0;
    while(i<=j){
    //我们的字符只能是数字
    if(s[i]<'0'||s[i]>'9'){
        return false;
    }
    sum=sum*10+(s[i]-'0');
     //每个整数的范围是0到255
      if(sum>255||sum<0){
        return false;
    }
    i++;
    }
    return true;
    }

    void backtraking(string s,int index,int cn){
    //第三个.后面的字符串有效,保存到结果里
    if(cn==3){
        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,'.');
        cn++;
        //因为插入了一个点,由i+1变为i+2
        backtraking(s,i+2,cn);
        cn--;
        s.erase(s.begin()+i+1);
    }
    else{
        break;
    }
       }
    }
    
    vector<string> restoreIpAddresses(string s) {
    backtraking(s,0,0);
    return res;
    }
};

Leetcode.78 子集

class Solution {
public:
    vector<vector<int>>res;
    vector<int>gather;
    void backtraking(vector<int>&nums,int index){
    if(gather.size()!=0){
        res.push_back(gather);
    }
    for(int i=index;i<nums.size();i++){
    gather.push_back(nums[i]);
    backtraking(nums,i+1);
    gather.pop_back();
    }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
    res.push_back({});
    backtraking(nums,0);
    return res;
    }
};

Leetcode.90 子集II

这道题与上一道题最大的区别就是数组中可能包含重复元素,需要进行去重操作,去重的步骤我们之前提到了,仍然是进行树层上的去重

class Solution {
public:
    vector<vector<int>>res;
    vector<int>combination;
    void backtraking(vector<int>&nums,int index,vector<bool>&used){
            res.push_back(combination);
            for(int i=index;i<nums.size();i++){
                if(i>=1&&nums[i]==nums[i-1]&&used[i-1]==false){
                    continue;
                }
                combination.push_back(nums[i]);
                used[i]=true;
                backtraking(nums,i+1,used);
                used[i]=false;
                combination.pop_back();
            }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        vector<bool>used(nums.size(),false);
        backtraking(nums,0,used);
        return res;
    }

};

Leetcode.491 递增子序列

该题仍然是要进行去重,但不论是去重方法还是去重思想都与之前的题目不同,首先,该题无法再向以前一样排序,因为排序后会改变原来的序列,而本题正要求递增序列。第二点,该题的去重是去同一父节点下相同元素的重,其本质与前面并不同,该题可采用在每层设置哈希表查重

class Solution {
public:
    vector<vector<int>>res;
    vector<int>ans;
    bool isvalid(int a){
    if(!ans.size()) return true;
    return a>=ans.back();
    }
    void backtraking(vector<int>&nums,int index){
    if(ans.size()>1)
    res.push_back(ans);
    
    unordered_map<int,int>Map;
    for(int i=index;i<nums.size();i++){
    if(!isvalid(nums[i])||Map.count(nums[i])) continue;
    Map[nums[i]]++;
    ans.push_back(nums[i]);
    backtraking(nums,i+1);
    ans.pop_back();
    }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
    backtraking(nums,0);
    return res;
    }
};

Leetcode.46 全排列

该题是排列问题,有着一个不同于以往的新的变化,那就是返回的集合的顺序,在排列问题中,我们是需要考虑顺序的

class Solution {
public:
    vector<vector<int>>res;
    vector<int>ans;
    void backtraking(vector<int>&nums,int index,vector<bool>&used){
    //当尝试的索引到达数组的边界时,遍历完成,将答案保存到res中
    if(ans.size()==nums.size()){
        res.push_back(ans);
        return;
    }
    //本次是排列与组合最大的不同时每次不再从index开始,而是从0开始
    for(int i=0;i<nums.size();i++){
    if(used[i])continue;
    ans.push_back(nums[i]);
    used[i]=true;
    backtraking(nums,i+1,used);
    used[i]=false;
    ans.pop_back();
    }
    }
    vector<vector<int>> permute(vector<int>& nums) {
    vector<bool>used(nums.size(),false);
    backtraking(nums,0,used);
    return res;
    }
};

Leetcode.47 全排列II

class Solution {
public:
    vector<vector<int>>res;
    vector<int>ans;
    void backtraking(vector<int>&nums,int index,vector<bool>&used){
    if(ans.size()==nums.size())
    {
        res.push_back(ans);
        return;
    }
    for(int i=0;i<nums.size();i++){
    //去重操作
    if(i>=1&&nums[i]==nums[i-1]&&!used[i-1]) continue;
    //去重和查重并不冲突,再次进行查重
    if(used[i]) continue;
    used[i]=true;
    ans.push_back(nums[i]);
    backtraking(nums,i+1,used);
    ans.pop_back();
    used[i]=false;
    }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
    vector<bool>used(nums.size(),false);
    sort(nums.begin(),nums.end());
    backtraking(nums,0,used);
    return res;
    }
};

Leetcode.51 N皇后

最经典的一道回溯题目,对于这题的分析以及后续的优化思路有太多细节了,我单独写了一篇博客来分析这道题,如果有不太清楚的可以去看看

class Solution {
public:
    vector<vector<string>>res;
    //判断摆放的皇后是否有效
    bool isValid(vector<int>&record,int i,int j){
    for(int k=0;k<i;k++){
        //不能同列也不能同斜线
        if(record[k]==j||i-k==abs(j-record[k]))return false;
    }
    return true;    
    }
    void backtraking(vector<int>&record,int i){
    //棋盘上的皇后已经摆放完毕,这时候准备进行记录
    if(i==record.size()){
    vector<string>ans;
    for(int i=0;i<record.size();i++){
        string str;
        for(int j=0;j<record.size();j++){
            if(record[i]==j){
               str.push_back('Q');
            }
            else{
                str.push_back('.');
            }
        }
        ans.push_back(str);
    }
    res.push_back(ans);
    return;
    }
    for(int j=0;j<record.size();j++)
    {
        if(isValid(record,i,j)){
        record[i]=j;
        backtraking(record,i+1);
        }
    }
    }
    vector<vector<string>> solveNQueens(int n) {
    vector<int>record(n,0);
    //record[i]=j:在第i行第j列摆放一个皇后
    backtraking(record,0);
    return res;
    }
};

 该题还有一个位运算的优化,感兴趣的可以去我的那篇博客看看


Leetcode.37 解数独

class Solution {
public:
    bool isValid(vector<vector<char>>& board,int i,int j,char k){
    for(int m=0;m<9;m++){
        //判断行上的是否重复
        if(board[i][m]==k)return false;
        //判断列上的是否重复
        if(board[m][j]==k)return false;
    }
    //判断9宫格里的是否重复
    for(int row=i/3*3;row<i/3*3+3;row++){
        for(int col=j/3*3;col<j/3*3+3;col++){
             if(board[row][col]==k)return false;
        }
    }
    return true;
    }
    bool backtraking(vector<vector<char>>& board){
    for(int i=0;i<9;i++){
        for(int j=0;j<9;j++){
             //如果第i行第j列已经有数字了,那么跳过
             if(board[i][j]!='.')continue;
          for(char k='1';k<='9';k++){
             if(isValid(board,i,j,k)){
                 board[i][j]=k;

                //如果找到合适的那组返回true
                if(backtraking(board)){
                    return true;
                }
                //回溯撤销k,重新尝试
                    board[i][j]='.';
             } 
          }
          //9个数都试完了依然不行,返回false
          return false;
        }
    }
    return true;
    }
    void solveSudoku(vector<vector<char>>& board) {
        backtraking(board);
    }
};

此外还有一种思路:

class Solution {
public:
    void solveSudoku(vector<vector<char>>& board) {

        back(board,0,0);
    }
    bool isvalid(vector<vector<char>>& board,int x,int y,int n)
    {
        for(int i=0;i<9;i++)
        {
            if(board[x][i]==n)return false;
            if(board[i][y]==n)return false;
        
         if (board[(x/3)*3 + i/3][(y/3)*3 + i%3] == n)
            return false;
        }
        return true;
    }
    bool back(vector<vector<char>>& board,int x,int y)
    {
        if(y==9)
        {
            x+=1;
            y=0;
        }
        if(x==9)return true;//这是递归终止条件,从这回溯
        if(board[x][y] != '.') return back(board,x,y+1);
        else 
        {
            for(char a='1';a<='9';a++)
            {
                if(isvalid(board,x,y,a))
                {
                    board[x][y]=a;
                    if(back(board,x,y+1))return true;
                    board[x][y]='.';
                }

            }
            return false;
        }

        //这行代码没有意义,单纯是为了编译成功,与上个题解不同
        return true;
    }
};

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值