回溯模板全解

目录

​编辑

什么是回溯法

回溯法效率

回溯法解决的问题

回溯抽象成树

基础模板

题目一:77. 组合

题目二:216. 组合总和 III

 题目三:17. 电话号码的字母组合


什么是回溯法

回溯法也可以叫做回溯搜索法,它是一种搜索的方式。其实是每一次递归后再返回上一层,由单一深度搜索拓宽为深度加广度搜索。

回溯法效率

回溯法其实是一个穷尽所有可能的算法,本质是穷举。那为什么不直接用多层for循环呢? 因为你无法确定for循环的层数,只能通过递归来层层深入。

回溯法解决的问题

  1. 需要穷尽所有可能,并且无法确定for循环层数。
  2. 组合问题:N个数里面按一定规则找出k个数的集合
  3. 切割问题:一个字符串按一定规则有几种切割方式
  4. 子集问题:一个N个数的集合里有多少符合条件的子集
  5. 排列问题:N个数按一定规则全排列,有几种排列方式
  6. 棋盘问题:N皇后,解数独等等

同时此类题必有标注,可以按任意顺序返回。

回溯抽象成树

因为即有深度又有广度,所以所有回溯问题都可以抽象转化成树。

---------------------------------------------------------------------------------------------------------------------------------

基础模板

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

---------------------------------------------------------------------------------------------------------------------------------

题目一:77. 组合

解析:首先提供两个容器 ,第一个容器ans用来保存最终的答案,path用来保存每一条路径上的答案,如果符合条件再将它放入ans容器中。

        vector<vector<int>> ans;
        vector<int> path;

          写终止条件,根据题目要求找寻k个数的组合,所以如果path中的数达到k个的时候就应该将答案放入ans中。

        if(path.size()==k)
        {
            ans.push_back(path);
            return;
        }

      

 for循环代表当 前层数的所有可能选择,比如说你在第一层,则你有四种选择,分别是1,2,3,4

然后通过递归,进入下一层。这里引用startIndex做为起始位置标志,因为当层数不断深入的时候

起始位置是i+1 ,因为第 i 项在上一层已经取过,如果下次递归的起始位置不从i+1开始的话会导致重复。

       for(int i=startIndex;i<=n;i++)
        {
            path.push_back(i);
            backtracking(n,k,i+1);
            path.pop_back();
        }

 当一个path结束后自动上升一层,此时就要进行回溯,实际上是弹出path中的一个元素,这时候for循环在这一层刚好后移一位,同时path缺少一个元素。再push进去一个元素,进入下一层递归,刚好满足要求,以此类推。

       整体

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    void backtracking(int n,int k,int startIndex)
    {
        if(path.size()==k)
        {
            ans.push_back(path);
            return;
        }
        for(int i=startIndex;i<=n;i++)
        {
            path.push_back(i);
            backtracking(n,k,i+1);
            path.pop_back();
        }
    }

    vector<vector<int>> combine(int n, int k) {
        backtracking(n,k,1);
        return ans;
    }
};

---------------------------------------------------------------------------------------------------------------------------------

题目二:216. 组合总和 III

解析:提供容器,第一个容器ans用来保存最终的答案,path用来保存每一条路径上的答案,如果符合条件再将它放入ans容器中。

      vector<vector<int>> ans;
      vector<int> path;

          先明确终止条件是什么,目标是找到相加之和为n的k个元素。分析可知,实际上有两个终止条件,第一个是path的长度必须为k,第二是path元素相加之和为n。

          这里运用逆向思维,相加为n实际上就是,总和n每次都减去元素的值。最后刚好为零的path为所求path。

        if(path.size()==k)
        {
            if(n==0)
            {
                ans.push_back(path);
            }
            return;
        }

       

 如图所示,这幅图我没有标记的很全面,为的是让读者自己认真看图思考一下。其实你会发现,每一个匡都是一个for循环里面可选择的数据。 为什么越往下走,匡里面可循环的元素越来越少呢?是因为你在递归,每次的startIndex都会往前进一位。其实横着看是上一层所有可能选择,竖着看是单一的一条path。

        for(int i=startIndex;i<=9;i++)
        {
            path.push_back(i);
            n=n-i;
            backtracking(k,n,i+1);
            n=n+i;
            path.pop_back();
        }

  每次递归都要把n减去元素i,当满足题目条件,即 n 为 0 的时候证明已经找到了k个元素相加和为n。此时要进行回溯,n 加上 i 返回上一层的for循环直到所有的遍历结束。

  整体

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    void backtracking(int k,int n,int startIndex)
    {
        if(path.size()==k)
        {
            if(n==0)
            {
                ans.push_back(path);
            }
            return;
        }

        for(int i=startIndex;i<=9;i++)
        {
            path.push_back(i);
            n=n-i;
            backtracking(k,n,i+1);
            n=n+i;
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return ans;
    }

};

---------------------------------------------------------------------------------------------------------------------------------

 题目三:17. 电话号码的字母组合

          提供容器ans来接收所有字母组合,让后s 相当于path。

          要提前处理一下按键上的字母组合,把题目上的按键转化为string 数组。同时思考一下,本体和上面题目的区别,本题样本区间取自两个字符串,但是上面题目中的取自都是一个字符串。这一点区别将会为我们如何递归埋下伏笔,也就是startIndex到底该怎么选。

 例如 "abc"  和  "def"  单层先选取 "a"  下一层选取的可不是 "b"  而是 "d"。所以startIndex不能是简单的 i+1 ,需要改变样本区间

    const string letterMap[10]={
        "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
    };
    vector<string> ans;
    string s;

          终止条件非常简单,当组合 s 的长度 等于 digits 的长度的时候就可以结束了。

        if(index==digits.size())
        {
            ans.push_back(s);
            return;
        }

这里重点讲解index的含义是什么,相当于之前的startIndex,但是我们在此基础上动了一些手脚,index实际上是从前到后扫描 digits,让后得到的char 减去起始 ‘0’ 就可以得到到底样本区间是哪一个数字按键。

        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();

        }

到此为止,本题和上面没有什么真正的逻辑上的区别,从逻辑难度来说实际上是比较简单的,因为本体唯一的收取path的限制条件仅仅是长度罢了。其他都是照猫画虎。

看上去复杂是因为,取样本区间竟然不是在一个string当中,而是在两个string区间所取的,同时我们要预先处理一下按键字符。

class Solution {
public:
    const string letterMap[10]={
        "","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"
    };
    vector<string> ans;
    string s;
    void backtracking(const string& digits,int index)
    {
        if(index==digits.size())
        {
            ans.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 ans;
        backtracking(digits,0);
        return ans;
    }
};

---------------------------------------------------------------------------------------------------------------------------------

题目四:90. 子集 II

      

          提供容器,老样子提供单条路径path,让后提供所有情况接收容器ans。

    vector<vector<int>> ans;
    vector<int> path;

          本题没有限制,只要是他的子集就可以。 既然没有要求,那么不用写终止条件,同时并不再是一条路只收取一个path,而是一条路上各层之间的path都要收取。

         同层递进减枝:这一部分是重难点,请读者认真阅读。

 通过树形图展开可以发现,同层元素是不能重复的,不然会有重复的path加入到ans当中。但是上下层是可以有重复元素的比如(1,2,2)。

那么,既然有重复我们怎么去重呢? 我们可以使用条件nums[i]==num[i-1] 的时候直接跳过,有些同学会问,为什么不是nums[i]==nums[i+1]的时候跳过呢?

你仔细想一下,当我们进行对比的时候nums[i]==nums[i+1] 我们进行了元素装填么?我是先判断,才把元素放入path中,那么问题来了如果我直接判断第 i 和 i+1项的时候,实际上第 i 和 i+1 项都还没有处理呢。然后你就直接跳过第 i 项了。那么请问你的(1,2,2)三元素集合怎么来?你不是跳过了一个么??

所以我们需要预先处理再进行判断,如何预先处理? 那就是nums[i-1]==nums[i]时候跳过,你都到第 i 项了,你 i - 1 项必然已经处理了啊。发现 i 和 i - 1项一样,直接跳过第 i 项不就行了,i - 1项预先处理过在path里了。 

            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);
            used[i]=false;
            path.pop_back();

  你以为到这就结束了??太年轻,太年轻了。为什么我要强调同层去重和上下层去重?? 你设立的条件nums[i-1]==nums[i] 针对上下层和同层都适用啊,你上下层startIndex是i+1,那么假如你是(1,2)还余留了一个2,当你进入下一层的时候,本应该是(1,2,2)。结过你一对比,发现俩 2 一样就直接跳过最后一个2了。那么你大错特错!!!

这里引入bool 标志,上下层处理过就把used标明true,代表可以重复。递归结束后,就要进行回溯了,那么必然会回到上层的for循环中,同层不能重复,所以used标明false。

让后判断的时候有两个条件 nums[i-1]==nums[i] && used=false;

lass Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex,vector<bool>& used)
    {
        ans.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);
            used[i]=false;
            path.pop_back();
        }
    }


    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        sort(nums.begin(),nums.end());
        backtracking(nums,0,used);
        return ans;
    }
};

到此分析完毕,掰开揉碎,只为精品。

---------------------------------------------------------------------------------------------------------------------------------

题目五:46. 全排列

          提供容器

    vector<vector<int>> ans;
    vector<int> path;

          终止条件

        if(path.size()==nums.size())
        {
            ans.push_back(path);
            return;
        }

         同层起始减枝叶

        for(int i=0;i<nums.size();i++)
        {
            if(used[i]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            used[i]=false;
            path.pop_back();
        }

          整体

class Solution {
public:
    vector<vector<int>> ans;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex,vector<bool>& used)
    {
        if(path.size()==nums.size())
        {
            ans.push_back(path);
            return;
        }

        for(int i=0;i<nums.size();i++)
        {
            if(used[i]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            used[i]=false;
            path.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        backtracking(nums,0,used);
        return ans;
    }
};

题目六:491. 递增子序列

          提供容器

    vector<vector<int>> ans;
    map<vector<int>,int> mp;
    vector<int> path;

          终止条件

        if(path.size()>=2&&path[path.size()-2]<=path[path.size()-1])
        {
            mp[path]++;
        }else if(path.size()>=2&&path[path.size()-2]>path[path.size()-1])
        {
           return;
        }

       递归

        for(int i=startIndex;i<nums.size();i++)
        {

            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }

     最后 整体 去重

class Solution {
public:
    vector<vector<int>> ans;
    map<vector<int>,int> mp;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex)
    {
        if(path.size()>=2&&path[path.size()-2]<=path[path.size()-1])
        {
            mp[path]++;
        }else if(path.size()>=2&&path[path.size()-2]>path[path.size()-1])
        {
           return;
        }
        for(int i=startIndex;i<nums.size();i++)
        {

            path.push_back(nums[i]);
            backtracking(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {
        backtracking(nums,0);
        for(auto a:mp)
        {
            ans.push_back(a.first);
        }
        return ans;
    }
};

---------------------------------------------------------------------------------------------------------------------------------

题目七:47. 全排列 II
 

           提供容器

    vector<vector<int>> ans;
    map<vector<int>,int> mp;
    vector<int> path;

         递归

        for(int i=0;i<nums.size();i++)
        {
            if(used[i]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            used[i]=false;
            path.pop_back();
        }       

          终止条件

        if(path.size()==nums.size())
        {
            mp[path]++;
            return;
        }

         双重去重

class Solution {
public:
    vector<vector<int>> ans;
    map<vector<int>,int> mp;
    vector<int> path;
    void backtracking(vector<int>& nums,int startIndex,vector<bool> & used)
    {
        if(path.size()==nums.size())
        {
            mp[path]++;
            return;
        }

        for(int i=0;i<nums.size();i++)
        {
            if(used[i]==true) continue;
            path.push_back(nums[i]);
            used[i]=true;
            backtracking(nums,i+1,used);
            used[i]=false;
            path.pop_back();
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(),false);
        backtracking(nums,0,used);
        for(auto a:mp)
        {
            ans.push_back(a.first);
        }
        return ans;
    }
};

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值