Bootstrap

暴力搜索——回溯

理论

回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案。一般可以解决如下几种问题:
组合问题:N个数里面按一定规则找出k个数的集合
切割问题:一个字符串按一定规则有几种切割方式
子集问题:一个N个数的集合里有多少符合条件的子集
排列问题:N个数按一定规则全排列,有几种排列方式
棋盘问题:N皇后,解数独等等
组合是不强调元素顺序的,排列是强调元素顺序。因此组合无序,排列有序。

例题

77. 组合
回溯就是递归,所以按照递归三部曲来做

  1. 参数:n,k固定参数,startIndex,从startindex开始往后取元素,path,存放一条支路的结果,re:存放所有支路的结果
    返回值:因为要遍历整棵树,所以不需要返回值
  2. 终止条件:当到达终止条件时存放结果,也就是path里的元素数等于k
  3. 本层递归:从startindex开始往后选择,递归调用下一层,回溯。
    for循环次数就是树的分支数,递归的深度就是树的深度。
class Solution {
public:
    vector<vector<int>>re;
    vector<int>path;
    void backtracking(int startIndex,int n,int k){
        if(path.size()==k){//到达叶子结点
            re.push_back(path);
            return ;
        }
        for(int i=startIndex;i<=n;i++){
            path.push_back(i);
            backtracking(i+1,n,k);//下一层就要从i+1开始选择了
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(1,n,k);
        return re;
    }
};

优化:
当startIndex开始往后的元素个数小于要找的个数,就不需要再找了。
即 n-i+1(还剩的个数) < k-path.size() (还要找的个数)
因此只需要从startIndex开始查找到n+1-k+path.size()

class Solution {
public:
    vector<vector<int>>re;
    vector<int>path;
    void backtracking(int startIndex,int n,int k){
        if(path.size()==k){//到达叶子结点
            re.push_back(path);
            return ;
        }
        for(int i=startIndex;i<=n+1-k+path.size();i++){
            path.push_back(i);
            backtracking(i+1,n,k);
            path.pop_back();//回溯
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtracking(1,n,k);
        return re;
    }
};

216. 组合总和 III

  1. 参数:n,k:找k个数并且和为n,startIndex:从startindex位置开始找
    返回值:空
  2. 终止条件:如果要找和为负数的集合,直接返回
    如果要找和0的集合并且当前已经找到了k个,保存结果
    如果n为0或者path.size()==k,直接返回
  3. 本层递归:从startindex开始往后取,递归,回溯
class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void backtracking(int k,int n,int startIndex){//找k个数,并且和为n的集合
        if(path.size()==k){//找够了k个数
            if(n==0){
                re.push_back(path);
            }
            return ;
        }
        for(int i=index;i<=9;i++){
            if(n-i<0){//当前位置往后的都比n大,自然也就没有必要再找了
                break;
            }
            path.push_back(i);
            f(k,n-i,i+1);//找从i+1开始,组成n-i的组合
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum3(int k, int n) {
        backtracking(k,n,1);
        return re;
    }
};

17. 电话号码的字母组合
每一种组合中集合的个数就是digits的长度,从当前位置对于的字符串中找一个字母,然后再去找下一个位置的字母

  1. 参数:digits 固定参数,index:从index开始找

  2. 终止条件:当找到digits长度大小的集合时,就保存一下

  3. 本层递归:映射出当前位置的数字字符对于的字符串,然后穷举每一个可能。

class Solution {
public:
    string number [10]={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//数字字符串映射表
    vector<string>re;
    string path;
    void f(string & digits,int index){//从digits中的index位置开始找
        if(path.size()==digits.size()){
            re.push_back(path);
            return ;
        }
        string cur=number[digits[index]-'0'];//index位置对应字符串,对于digits="23",index==0,cur就是"abc"
        for(int i=0;i<cur.size();i++){
            path.push_back(cur[i]);
            f(digits,index+1);
            path.pop_back();
        }
        
       
    }
    vector<string> letterCombinations(string digits) {
        if(digits==""){
            return re;
        }
        f(digits,0);
        return re;
    }
};

39. 组合总和

和选硬币问题完全一样

  1. 参数:candidates 固定参数,index:从index往后开始找和为rest的组合
  2. 终止条件:rest小于0,直接返回
    rest等于0,保存结果
  3. 本层递归:从index往后选一个元素,然后去找减去index位置元素的组合

和上几个题的区别是本题元素可以重复选

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& candidates,int index,int rest){
        if(rest<0){//非法情况
            return ;
        }
        if(rest==0){
            re.push_back(path);
            return ;
        }
        for(int i=index;i<candidates.size();i++){
            path.push_back(candidates[i]);
            f(candidates,i,rest-candidates[i]);//从i开始,找和
            //为rest-candidates[i]的组合
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        f(candidates,0,target);
        return re;
    }
};

优化:先对candidates进行排序,如果rest-candidates[i]已经小于0了,后面就没有必要再尝试了

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& candidates,int index,int rest){
        if(rest<0){//非法情况
            return ;
        }
        if(rest==0){
            re.push_back(path);
            return ;
        }
        for(int i=index;i<candidates.size();i++){
            if(rest-candidates[i]<0){//剪枝
                break;
            }
            path.push_back(candidates[i]);
            f(candidates,i,rest-candidates[i]);//从i开始,找和
            //为rest-candidates[i]的组合
            path.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        f(candidates,0,target);
        return re;
    }
};

40. 组合总和 II

本题难点在于如何去重,可以先排序,这样相同的元素肯定紧挨着,在每一层递归下设一个哈希表,用来标记有没有使用过这个元素,只有没有使用过的才去跑递归
因为candidates里可能有重复的元素,所以同一个组合里会有重复元素,但是不同的组合元素要不同,也就是同一树层去重
剪枝的操作和上一题一样。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& candidates,int index,int rest){//从index开始往后找和为rest的组合
        if(rest==0){
            re.push_back(path);
            return ;
        }
        unordered_map<int,bool>visit;
        for(int i=index;i<candidates.size() && rest-candidates[i]>=0 ;i++){
            if(!visit[candidates[i]]){//去重,如果已经使用了candidates[i]元素就剪掉
                visit[candidates[i]]=true;
                path.push_back(candidates[i]);
                f(candidates,i+1,rest-candidates[i]);
                path.pop_back();
            }
            
        }
        
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        f(candidates,0,target);
        return re;
    }
};

省空间的写法,因为已经排过序了,所以相同的元素肯定紧靠着,只要判断它和它的前一个元素是否相等(同一树层下)

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& candidates, int index, int rest) {//从index开始往后找和为rest的组合
        if (rest == 0) {
            re.push_back(path);
            return;
        }
        unordered_map<int, bool>visit;
        for (int i = index; i < candidates.size() && rest - candidates[i] >= 0; i++) {
            if (i > index && candidates[i] == candidates[i - 1]) {//i>index是保证在同一树层下
                continue;
            }
            path.push_back(candidates[i]);
            f(candidates, i + 1, rest - candidates[i]);
            path.pop_back();
        }

    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        f(candidates, 0, target);
        return re;
    }
};

131. 分割回文串

  1. 参数:s 固定参数,index:从index位置开始分割
  2. 终止条件:当index等于s的长度时保存结果
  3. 本层递归:要找从index开始的回文子串,i初始为index 表示从index位置开始分割,例如s=“aab”,index=0,i
    开始等于0,就是a作为一个子串,然后去切割ab;如果i等于1,就是aa作为一个子串,然后去切割ab。注意要判断子串是否为回文串。
class Solution {
public:
    vector<string>path;
    vector<vector<string>>re;
    bool isPalindrome(const string& s){//判断是否为回文串
        int l=0;
        int r=s.size()-1;
        while(l<r){
            if(s[l++]!=s[r--]){
                return false;
            }
        }
        return true;
    }
    void f(string& s,int index){//找从index位置开始分割的子串
        if(index==s.size()){
        
            re.push_back(path);
            return ;
        }
        for(int i=index;i<s.size();i++){
            string substr=s.substr(index,i-index+1);//截取从index到i的子串
            if(isPalindrome(substr)){//只有substr是回文串才去跑递归
                path.push_back(substr);
                f(s,i+1);
                path.pop_back();
            }
        }
    }
    vector<vector<string>> partition(string s) {
        f(s,0);
        return re;
    }
};

93. 复原 IP 地址

类似于切割字符串,直接在原字符串上修改就好了

  1. 参数:s,startindex:从startindex位置开始找,pointNum:s中 . 的个数
  2. 终止位置:pointNum为3,判断startindex到结束的字符串是否合法
  3. 本层递归:也是先判断从startindex位置到i的字符串是否合法,若合法,在i+1位置插入 . ,然后找i+2开始字符串。
class Solution {
public:
    vector<string>re;
    bool isValid(string& s,int start,int end){//判断是否合法
        if(start>end){
            return false;
        }
        if(s[start]=='0'){
            if(start==end){
                return true;
            }
            else{
                return false;
            }
        }
       int num=0;
       for(int i=start;i<=end;i++){
           num=num*10+(s[i]-'0');
           if(num>255){//当前num已经超过255了,后续一定也大于255
               return false;
           }
       }
       return true;
    }
    void f(string s,int startIndex,int pointNum){
        if(pointNum==3){//已经添加了3个点了,只需要判断startIndex到结束是否是合法的
            if(isValid(s,startIndex,s.size()-1)){
                re.push_back(s);
            }
            return ;
        }

        for(int i=startIndex;i<s.size();i++){
            if(isValid(s,startIndex,i)){//相当于在i后面加. 需要提前判断startindex到i是否是合法的
                s.insert(s.begin()+i+1,'.');//在i后面插入 . 
                pointNum++;
                f(s,i+2,pointNum);//因为插入了一个.  所以从i+2开始
                pointNum--;
                s.erase(s.begin()+i+1);
            }
             else {//不符合的话,后续一定也是不符合的,因为后续都包含这一部分。
                break;
            }
        }
    }
    vector<string> restoreIpAddresses(string s) {
        f(s,0,0);
        return re;
    }
};

78. 子集

集合是无序的,也就是{1,2}和{2,1}是同一个集合,所以每次for循环都是从index开始,如果是排列的话,就要从0开始。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& nums,int index){
        re.push_back(path);//子集出现在递归过程中,不仅仅只是终止情况
        if(index==nums.size()){
            return ;
        }
        for(int i=index;i<nums.size();i++){
            path.push_back(nums[i]);
            f(nums,i+1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        f(nums,0);
        return re;
    }
};

90. 子集 II

和组合中II类似,都是同一层去重

class Solution {
public:
    vector<vector<int>>re;
    vector<int>path;
    void f(vector<int>& nums,int index){
        re.push_back(path);
        if(index==nums.size()){
            return ;
        }
        for(int i=index;i<nums.size();i++){
            if(i>index &&nums[i]==nums[i-1]){//同一层去重
                continue;
            }
                path.push_back(nums[i]);
                f(nums,i+1);
                path.pop_back();
          
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(),nums.end());
        f(nums,0);
        return re;
    }
};

491. 递增子序列

需要注意的是递增子序列至少2个元素。
去重逻辑和上一个一样了,因为相同的不一定是紧挨着的,所以采用set去重。

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& nums, int index) {
        if (path.size() > 1) {
            re.push_back(path);
        }
        if (index == nums.size()) {
            return;
        }
        unordered_set<int> uset;
        for (int i = index; i < nums.size(); i++) {
            if (uset.find(nums[i]) == uset.end()) {//本层第一次出现
                uset.insert(nums[i]);//标记使用
                if (path.size() > 0) {//path不为空要判断是否递增
                    int lastNum = path.back();//path最后一个元素
                    if (nums[i] >= lastNum) {
                        path.push_back(nums[i]);
                        f(nums, i + 1);
                        path.pop_back();
                    }
                }
                else {//path为空的时直接放

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

        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {

        f(nums, 0);
        return re;
    }
};

优化:用数组做哈希表

class Solution {
public:
    vector<int>path;
    vector<vector<int>>re;
    void f(vector<int>& nums, int index) {
        if (path.size() > 1) {
            re.push_back(path);
        }
        if (index == nums.size()) {
            return;
        }
        //unordered_set<int> uset;
        int used[201]={0};//0表示为使用过,1表示使用过
        for (int i = index; i < nums.size(); i++) {
            if (used[nums[i]+100]==0) {//本层第一次出现
                used[nums[i]+100]=1;//标记使用
                if (path.size() > 0) {//path不为空要判断是否递增
                    int lastNum = path.back();//path最后一个元素
                    if (nums[i] >= lastNum) {
                        path.push_back(nums[i]);
                        f(nums, i + 1);
                        path.pop_back();
                    }
                }
                else {//path为空的时直接放

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

        }
    }
    vector<vector<int>> findSubsequences(vector<int>& nums) {

        f(nums, 0);
        return re;
    }
};

46. 全排列

注意排列和组合的区别:排列有顺序,组合无顺序

排列问题可以不需要index,但需要借助used数组
这个去重是同一树枝下去重,也就是在同一条支路下,一个元素只能选择一次。

class Solution {
public:
    vector<vector<int>>re;
    vector<int>path;
    
    void f(vector<int>& nums,vector<int>& used){
        if(path.size()==nums.size()){
            re.push_back(path);
            return ;
        }
        for(int i=0;i<nums.size();i++){
            if(used[i]==0){//只选择本树枝下没有用过的
                used[i]=1;
                path.push_back(nums[i]);
                f(nums,used);
                path.pop_back();
                used[i]=0;
            }
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<int>used(nums.size());
        f(nums,used);
        return re;
    }
};

解2 :交换实现
如果一个集合有3个元素,那么它的排列方式有6种,第一个位置3种选择,第二个位置2种选择,第三个位置1种选择,相乘起来就是6种选择。

class Solution {
public:
    vector<vector<int>>re;
    void f(vector<int>& nums,int index){//从index开始的排列
        if(index==nums.size()){
            re.push_back(nums);
            return ;
        }
        for(int i=index;i<nums.size();i++){//从index开始往后的元素都可以放在index位置,相当于在index位置选一个元素
            swap(nums[index],nums[i])
            f(nums,index+1);//找index+1的全排列
            swap(nums[index],nums[i]);
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        f(nums,0);
        return re;
    }
};

47. 全排列 II

只需要去重就好了

class Solution {
public:
    vector<vector<int>>re;
    vector<int>path;
    void f(vector<int>& nums,int index){
        if(index==nums.size()){
            re.push_back(nums);
            return ;
        }
        //unordered_set<int>uset;
        int visit[21]={0};
        for(int i=index;i<nums.size();i++){
            if(visit[nums[i]+10]==0){
                visit[nums[i]+10]=1;
                swap(nums[index],nums[i]);
                f(nums,index+1);
                swap(nums[index],nums[i]);
            }
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        f(nums,0);
        return re;
    }
};

332. 重新安排行程

  1. 参数:vectorre;
    nordered_map<string,map<string,int>>targets;//unordered_map<开始地,map<目的地,航班数(0:表示不能再飞往此地了)>>targets targets表示从开始地到目的地所有航班的映射
    vector<vector>& tickets
    返回值:bool,找到一个解法就返回
  2. 终止条件:re.size()==tickets.size()+1,n趟航班就n+1个目的地。
  3. 本层递归:
    优先考虑目的地字典序靠前的航班,可以选择的条件是:本航班还有机票——target.second>0

-为什么第一次找到的解法就是字典序靠前的解法?
map默认是按字典序进行排序的,所以如果有多种解法,会先尝试字典序小的。

class Solution {
public:
    vector<string>re;
    unordered_map<string,map<string,int>>targets;//unordered_map<开始地,map<目的地,航班数(0:表示不能再飞往此地了)>>targets  targets表示从开始地到目的地所有航班的映射
    bool f(vector<vector<string>>& tickets){
        if(re.size()==tickets.size()+1){
            return true;
        }
        string curPlace=re[re.size()-1];//从curPlace出发
        for(pair<const string,int>&target:targets[curPlace]){//从curPlace出发的所有目的地
            if(target.second>0){//表示从curPlace还可以飞的航班
                target.second--;
                re.push_back(target.first);
                if(f(tickets)){//找到了一个就返回,因为map默认是按字典序排的,所以第一个找到的一定是符合要求的
                    return true;
                }
                re.pop_back();
                target.second++;
            }
        }

        return false;
    }
    vector<string> findItinerary(vector<vector<string>>& tickets) {
        for(int i=0;i<tickets.size();i++){
            vector<string>curFlight=tickets[i];//<开始地,目的地>
            targets[curFlight[0]][curFlight[1]]++;//增加映射关系
        }
        re.push_back("JFK");
        f(tickets);
        return re;
    }
};

51. N 皇后

思路:一行一行的摆,只需要判断是否共列 共斜线就好了
共斜线的简便判法:abs(row1 - row2) == abs(col1,col2) (row1,col1)和(row2,col2)共斜线

  1. 参数:vector<vector>re存结果;vectorpath;//每一行摆皇后的情况;ectorrecord;//记录每一行皇后摆的位置 record[i]=j:表示i行j列放皇后;string ss;//固定参数,哪一列上放皇后,就将相应位置修改为Q;
  2. 终止位置:index==n表示已经摆完了n行皇后(0-n-1)
  3. 本层递归:从本行的第一列到第n列尝试,如果当前位置合法,就记录下皇后的位置,然后递归跑下一行。
class Solution {
public:
    vector<vector<string>>re;
    vector<string>path;//每一行摆皇后的情况
    vector<int>record;//记录每一行皇后摆的位置 record[i]=j:表示i行j列放皇后
    bool isValid(int i, int j, int n) {//判断i行j列放皇后合不合法
        for (int row = 0; row < i; row++) {
            if (record[row] == j || abs(row - i) == abs(record[row] - j)) {//共列 共斜线
                return false;
            }
        }
        return true;
    }
    void f(int index, int n, string ss) {
        if (index == n) {
            re.push_back(path);
            return;
        }
        for (int j = 0; j < n; j++) {
            if (isValid(index, j, n)) {
                record[index] = j;
                string cur = ss;
                cur[j] = 'Q';
                path.push_back(cur);
                f(index + 1, n, ss);
                path.pop_back();
            }
        }
    }
    vector<vector<string>> solveNQueens(int n) {
        string ss;//固定参数,哪一列上放皇后,就将相应位置修改为Q
        for (int i = 0; i < n; i++) {
            ss += '.';
        }
        record.resize(n);
        f(0, n, ss);
        return re;

    }
};

37. 解数独
和N皇后类似,但比N皇后更复杂,N皇后每行只放一个皇后,而本题是要填满整个方格。
N皇后是一行一行的填,本题要一格一格的填,因此至少需要两层for循环

  1. 参数:board
    返回值:找到一个方案就返回,题目也说了只有一种方案。
  2. 终止条件:不需要写,表格填满了自然就终止
  3. 本层递归:从0行0列到8行8列,找到第一个空白的格子,然后尝试1~9的数字,若合法,继续放下一个格子。
class Solution {
public:
    bool isValid(int i,int j,char val,vector<vector<char>>& board){//判断(i,j)填k是否合法,此时i行j列位置是空的
        for(int col=0;col<9;col++){//判断同行
            if(board[i][col]==val){
                return false;
            }
        }
        for(int row=0;row<9;row++){//判断同列
            if(board[row][j]==val){
                return false;
            }
        }
        int startRow=i/3*3;//(i,j)所在的3*3
        int startCol=j/3*3;
        for(int row=startRow;row<startRow+3;row++){//判断同3*3
            for(int col=startCol;col<startCol+3;col++){
                if(board[row][col]==val){
                    return false;
                }
            }
        }
        return true;
    }
    bool f(vector<vector<char>>& board){
        for(int i=0;i<9;i++){
            for(int j=0;j<9;j++){
                if(board[i][j]!='.'){
                    continue;
                }
                for(char k='1';k<='9';k++){//从1~9尝试
                    if(isValid(i,j,k,board)){
                        board[i][j]=k;
                        if(f(board)){//找到一个方案就返回
                            return true;
                        }
                        board[i][j]='.';

                    }
                }
                return false;//1到9都不行,说明该方案没有解
            }
        }
        return true;//所有的格都填满了
    }
    void solveSudoku(vector<vector<char>>& board) {
        f(board);
        
    }
};

总结

回溯是递归的副产品,只要有递归就会有回溯,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。回溯法就是暴力搜索,并不是什么高效的算法,最多再剪枝一下。

  1. 组合问题
    组合无顺序
    for循环代表树的分支,横向遍历,递归深度代表树的高度,纵向遍历,回溯不断调整结果集

  2. 切割问题
    类似与组合问题,参数的index就代表切割线

  3. 子集问题
    子集问题也属于组合,不仅仅在终止收集结果

  4. 排列问题
    排列有顺序
    可以用交换的方式,也可以加一个used数组

  5. 去重问题
    同一树层去重,只需要在函数内部创建一个visit数组,只供本层使用
    这个visit数组也可以是set,如果题目中没有明确规定结点的范围,就要使用set了

  6. 安排行程问题
    有递归的地方就有回溯,深度优先搜索也是用递归来实现的,所以往往伴随着回溯。
    本题难在如何选择正确的容器

  7. 棋盘问题
    一层for循环的递归:N皇后,一行只放一个
    二层for循环的递归:解数独,一格放一个
    这两道题目的树的分支都是固定的,N皇后的分支固定为n,每行就有n个位置可以选择
    解数独的分支固定为9,从1到9选择一个数来填
    但是解数独的递归深度明显大于N皇后。

;