LeetCode回溯法专题

基本思路

前序:

在搞清楚基本思路之前,我们先根据每个回溯题目的大概思路,思考一下回溯法的难点在哪里。

难点1:回溯函数的参数的决定

回溯函数能如何进行下一步最关键的就是参数要怎么变化,找到每一步有哪些参数进行改变了,进行下一步要改变的参数又是拿些。这就可以确定了回溯函数的参数

难点2:每一步回溯函数的逻辑

在一步的回溯法中,要找到这步的选择列表,然后思考要不要使用到后面回溯的结果,最后再决定每个数要进行操作

难点3:回溯法的回溯操作

回溯操作就是回归到上一步的状态。例如在数组中就删除最后进去的数,在计算总和就减去最后的数等等。这步实际不难,但是很容易忘记

难点4:剪枝

剪枝就是判断一些条件,让搜索提前结束,让效率变高。重点就是条件的寻找,有些题的关键就是找到剪枝的条件。没有基本的规律,需要根据题意来寻找

 

几种算法方法得区别

1.递归法:从最典型的递归例子斐波纳契数列可以看出,递归就是实体自己和自己建立关系

2.回溯法:以某种方式一直计算,计算到无法计算的时候,就返回上一个状态,继续按着另一种方式计算,直到无法计算且全部方式都计算过以后

3.贪心算法:把问题分解成子问题,通过子问题的局部最优解求出问题的最优解。问题必须具有无后效性才能使用贪心算法

4.动态规划法:重叠子问题最优解是dp的最重要两个性质

 


例题1:给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。

实例:[1 , 2 , 3]

1.找到递归的规律(画出递归树)

画出递归树,可以让我们大概了解每一步的逻辑,然后就是直观的看到每步的选择列表

2.确定选择列表

选择列表就是在进行这步的过程中,可以选择的数值是哪些。选择列表很重要,以后会根据选择列表来决定回溯函数的参数和每一步的逻辑。

3.找到回溯函数的参数

回溯函数参数根据选择列表判断每一步那些参数有变化,在选择列表中就是对数组进行遍历,每一步的变化也是遍历数组的索引值而已,所以这题的参数就是一个数组的遍历索引值,设为start

4.找到结束条件

这题要寻找的就是全部可能的子集,所以结束条件就是数组遍历完。

5.每一步的逻辑

要找到全部的子集,再根据选择列表,每一步就是把索引为start的数加入结果列表中即可

6.剪枝

这里没有重复的步骤,所以不需要剪枝

7.进行下一步

和上面说的一样,进行下一步就是把start索引值+1即可

8.回溯操作

数组的回溯操作就是删除最后进去的数

代码:

result = []
void backtrack(路径, 选择列表):
    if (满足结束条件) {
        result.push_back(路径);
        return;
    }:
        
    
    for 选择 in 选择列表 {
        做选择
        backtrack(路径, 选择列表)
        撤销选择
    }
        
作者:labuladong
链接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-xiang-jie-by-labuladong-2/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

总结:

每次做回溯题目的时候就按着这些步骤来完成,可能有不完善,但是起码可以提供一个基本思路:

  1. 画出递归树
  2. 找出选择列表
  3. 确定函数参数
  4. 找出结束条件
  5. 每一步的逻辑
  6. 找出剪枝条件
  7. 递归下一步
  8. 回溯操作

下面就是leetcode上的回溯法题目练习记录,花了一大段时间来总结基本思路,因为回溯法确实是不简单,很难找到入手点,提供一个基本思路,以后做题就不用无从下手了

 

分割回文串

思路:

1.画出递归树

2.选择列表

选择列表就是遍历整个string

3.函数参数

遍历string,判断回文串,所以参数就只有一个string索引值

4.结束条件

当string被遍历完就结束回溯函数

5.每一步逻辑

先使用substr来获取每一个子串,然后再判断子串是否是回文串,如果是则继续递归进行下一步

6.剪枝

没有重复步骤,所以不需要剪枝

7.递归下一步

把索引值 + 1即可进行下一步

8.回溯操作

直接删除最近进去的数即可

代码:

class Solution {
    int sz;
    vector<vector<string>> res;
public:
    vector<vector<string>> partition(string s) {
        sz = s.size();
        if(!sz)    return res;
        vector<string> temp;
        backtrack(s , 0 , temp);
        return res;
    }

    bool isPalin(string &tmp , int l , int r) {
        while(l <= r) {
            if(tmp[l] != tmp[r])  return false;
            l++;
            r--;
        }
        return true;
    }

    void backtrack(string s , int start , vector<string> &temp) {
        if(start >= sz) {
            res.push_back(temp);
            return;
        }
        for(int i = start; i < sz; i++) {
            if(isPalin(s , start , i)) {
                temp.push_back(s.substr(start , i - start + 1));
                backtrack(s , i + 1 , temp);
                temp.pop_back();
            }
        }
    }
};

 

有重复字符串的排列组合

思路:

1.递归树

2.选择列表

就是遍历整个数组

3.函数参数

变化得参数只有索引值

4.结束条件

这题得不是需要全部子集,而是全集不同得排列方式,所以当数量达到全集元素的数量时,我们就结束函数

5.每一步的逻辑

直接把当前的char加到temp中,进行下一步即可

6.剪枝

剪枝时这题的关键,参考递归树中的红色方框,这四个方块两两都是重复的,说明当前一步的数和后面的数值相同时,我们就直接剪枝即可。

这里有两种情况剪枝:

  1. 就是当这个数在前面循环中已经重复过了的,就直接跳过
  2. 当这个数与前一个数相等,且前一个数的标识位变成false,证明前一个数已经经过一次计算了,就剪枝

7.递归下一步

没啥说的,直接带着改变后的temp进行下一步即可

8.回溯操作

这里的回溯只要把标识位设置为原本状态即可,因为我们是在进行下一步递归函数中完成对temp的修改,证明在这一步中temp的值是没有变化的,所以temp值不需要回溯

代码:

class Solution {
public:
    vector<string> res;
    void backtrack(string& S , string temp , vector<bool> &flags) {
        if(S.size() == temp.size()) {
            res.push_back(temp);
            return;
        }
        for(int i = 0; i < S.size(); i++) {
            if( (i > 0 && S[i-1] == S[i] && !flags[i - 1]) || flags[i])      continue;    //重点
            flags[i] = true;
            backtrack(S , temp + S[i] , flags);
            flags[i] = false;
        }
    }

    vector<string> permutation(string S) {
        vector<bool> flags(S.size() , false);
        sort(S.begin() , S.end());
        backtrack(S , "" , flags);
        return res;
    }
};

优化:

我们很容易发现这里存在很多重复性的遍历,在每一步中我们都要从第一个数开始重复的遍历,在使用标识位来判断是否已经使用过该数。

1.两两交换

我们不用temp作为递归下一步的参数,我们使用index。思路很简单,就是我们把第一个数和第二个数交换,然后回溯,再用第一个数和第三个数交换,再回溯。以此类推完成对全部数的交换。

class Solution {
public:
    vector<string> res;
    string temp;
    void backtrack(string& S , int index , int sz) {
        if(index >= sz) {
            res.push_back(temp);
            return;
        }
        for(int i = index; i < sz; i++) {
            if( i > index && (S[i-1] == S[i] || S[i] == S[index]))      continue;
            temp += S[i];
            swap(S[i] , S[index]);
            backtrack(S , index + 1 , sz);
            swap(S[i] , S[index]);
            temp.pop_back();
        }
    }

    vector<string> permutation(string S) {
        int sz = S.size();
        sort(S.begin() , S.end());
        backtrack(S , 0 , sz);
        return res;
    }
};

这个方法有 点需要注意的:

  1. 剪枝条件:
    1. 两个数不能是同一个数
    2. 两个数不能是相等的数,这样交换就没有意义了
    3. 前一个数也是相同的数
  2. 选择列表是从index到size-1 , 不再是都是从 0 到 size - 1,这样即提高了效率,也避免重复
  3. 回溯操作,string删除最后一位,直接使用pop_back也是可以的

2.源函数法:

其实全列表有一个源函数直接表示,即next_permutation,返回的就是vector值

class Solution {
public:
    vector<string> permutation(string S) {
		vector<string> result;
        sort(S.begin(), S.end());
        result.push_back(S);
        while (next_permutation(S.begin(), S.end())) {
            result.push_back(S);
        }
		return result;
    }
};

全排列 II

这题和上一题差不多的类型,所以就不在赘述前面的思路了,直接到代码,然后说下注意点

代码

class Solution {
public:
    vector<vector<int>> res;
    void backtrack(vector<int> nums , int index , int sz) {    //难点1
        if(index == sz - 1) {
            res.push_back(nums);
            return;
        }
        for(int i = index; i < sz; i++) {
            if(i != index && nums[index] == nums[i])     continue;
            swap(nums[i] , nums[index]);    //难点2
            backtrack(nums , index + 1 , sz);
        }
    }

    vector<vector<int>> permuteUnique(vector<int>& nums) {
        int sz = nums.size();
        sort(nums.begin() , nums.end());
        backtrack(nums , 0 , sz);
        return res;
    }
};

总结:

难点1:因为当结束走到尽头回溯得回溯操作时,我们没有必要延续之前得操作,所以我们nums不需要引用,所以在难点2就不需要回溯步骤(这仅是个人理解,如有不对,请指点!

 

复原IP地址

思路:

1.递归树

2.选择列表

就是对字符串进行分段然后判断,选择列表就是字符串

3.函数参数

这题有点特殊,虽然我们得选择列表是字符串,但是我们不使用字符串来遍历操作,而是使用分段得思想来遍历是否合理。所以我们得参数有:

s:字符串

n:合理字符串的个数

ip:合理得字符串

4.结束条件

n == 4且s为空串时,储存并结束

5.每一步的逻辑

  1. 使用substr分段然后转化成int类型,设为val
  2. 判断val > 255 或者 val的字符串值是否首位有0的存在

6.剪枝

s串的数量不能小于i

7.递归下一步

s改为substr(i) ,n+1 ,ip + 合理字符串 + ‘.’ ,但是最后的一个ip值不需要加 . ,所以进行判断

8.回溯操作

无回溯操作,直接都在参数中完成

代码:

class Solution {
    vector<string> res;
public:
    vector<string> restoreIpAddresses(string s) {
        string ip;
        backtrack(s , 0 , ip);
        return res;
    }

    void backtrack(string s , int n  , string ip) {
        if(n == 4) {
            if (s.empty()) {
                res.push_back(ip);
            }
            return;
        }
        for(int i = 1; i < 4; i++) {
            if(s.size() < i)    break;
            int val = stoi(s.substr(0 , i));
            if(val > 255 || i != to_string(val).size())     continue;
            backtrack(s.substr(i) , n + 1 , ip + s.substr(0 , i) + (n == 3 ? "" : "."));
        }
    }
};

 

八皇后

思路:

这个8皇后的问题主要运用的是位运算方法,没有太多回溯法的思想。

这个图的思想,但第一行的Q在第二个的位置时,下一行的标识X的位置就不能摆放皇后,后面同理。

所以我们使用left,right,down三个数值来记录下一行不能放皇后的位置,然后使用 | 操作进行合并,这样就是下一行不能放皇后的全部位置。

left = ( (1 << i) | left ) << 1 :i 为皇后的位置。right ,down同理

 

还需要一个tmp容器来记录每一行皇后的位置,不用进行复值操作,最后在进行一个结果的储存时,在tmp的位置加上皇后即可

代码:

class Solution {
    vector<vector<string>> res;
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<int> tmp(n);
        backtrack(n , tmp , 0 , 0 , 0 , 0);
        return res;
    }
    void backtrack(int n , vector<int> &tmp , int left , int right , int down , int row) {
        if(row == n) {
            vector<string> ans(n, string(n,'.'));
            for(int i = 0; i < n; i++)   ans[i][tmp[i]] = 'Q';
            res.push_back(ans);
            return;
        }
        int isVaild = left | right | down;
        for(int i = 0; i < n; i++) {
            if(((1 << i) & isVaild) == 0) {
                tmp[row] = i;
                backtrack(n , tmp , ((1 << i) | left) << 1 , ((1 << i) | right) >> 1 , ((1 << i) | down) , row + 1);
            }
        }
    }
};

 

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值