Leetcode_7【回溯算法】

目录

理论基础 

组合

 77. 组合  

16.组合总和III 

 17.电话号码的字母组合 

 39. 组合总和 

 40.组合总和II 

分割问题 

131.分割回文串  

 93.复原IP地址  

子集

 78.子集  

 90.子集II 

491.递增子序列 

排序

46.全排列 

47.全排列 II 

332.重新安排行程(可跳过) 

棋盘

51. N皇后(可跳过) 

37. 解数独(可跳过) 


SUMMARY

1、什么时候需要sort?

  • 给定一个可包含重复数字的序列nums时需要sort
  • 给定一个无重复元素 的整数数组不需要sort

2、什么时候需要startIndex?

  • 组合问题需要startIndex
  • 不需要startIndex:全排列问题;多个集合取组合,各个集合之间相互不影响(电话号码的字母组合)

3、对于需要startIndex的情况,什么时候iter(nums, i+1),什么时候iter(nums, i)?

  • iter(nums, i+1):同枝不包括自己,组合问题
  • iter(nums, i):同枝包括自己,某些组合总和问题

4、什么时候需要used数组?

  • 同层去重:if(i>0 && used[i] == used[i-1] && used[i-1]==false)continue;(给定一个可包含重复数字的序列nums时需要同层去重
  • 同枝去重:if(used[i] == true) continue;(对于排列问题,同枝一个元素只能用一次)

5、什么时候需要sum参数?

  • 组合总和问题

6、什么时候需要在for中做判断,判断通过才存?

  • 分割问题(分割回文串、复原IP地址)
  • N皇后

7、return的条件

  • 可以考虑path的个数,startIndex的大小,sum==target

理论基础 

其实在讲解二叉树的时候,就给大家介绍过回溯,这次正式开启回溯算法,大家可以先看视频,对回溯算法有一个整体的了解。

题目链接/文章讲解:代码随想录

视频讲解:带你学透回溯算法(理论篇)| 回溯法精讲!_哔哩哔哩_bilibili

组合

 77. 组合  

对着 在 回溯算法理论基础 给出的 代码模板,来做本题组合问题,大家就会发现 写回溯算法套路。本题关于剪枝操作是大家要理解的重点,因为后面很多回溯算法解决的题目,都是这个剪枝套路。 

题目链接/文章讲解:代码随想录

视频讲解:带你学透回溯算法-组合问题(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

剪枝操作:带你学透回溯算法-组合问题的剪枝操作(对应力扣题目:77.组合)| 回溯法精讲!_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combine(int n, int k) {
        iter(n, k, 1);
        return res;
    }
    void iter(int n, int k, int startIndex){
        if(k == path.size()){
            res.push_back(path);
            return ;
        }
        for(int i = startIndex; i <= n - (k - path.size()) + 1; i++){// 至多起始位置
            path.push_back(i);
            iter(n, k, i + 1);
            path.pop_back();
        }
    }
};

16.组合总和III 

如果把 组合问题理解了,本题就容易一些了。 

题目链接/文章讲解:代码随想录

视频讲解:和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    int sumpath = 0;
    vector<vector<int>> combinationSum3(int k, int n) {
        iter(k, n, 0, 1);
        return res;
    }
    void iter(int k, int n, int sumpath, int startIndex){
        if(path.size() == k){
            if(sumpath == n){
                res.push_back(path);
            }
            //sumpath = 0; 
            return ;
        }
        for(int i = startIndex; i <= 9; i++){
            sumpath += i;
            path.push_back(i);
            iter(k, n, sumpath, i + 1);
            path.pop_back();
            sumpath -=i;
        }
    }
};

 17.电话号码的字母组合 

本题大家刚开始做会有点难度,先自己思考20min,没思路就直接看题解。 

这道题更能体现出横向遍历和纵向遍历的含义,这里横向遍历遍历的每个键所代表的字符,纵向遍历遍历的是每个键之间的组合,所以要遍历的是用户输入的数字字符。

什么时候需要startIndex?

  1. 如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window)216.组合总和III (opens new window)
  2. 如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:17.电话号码的字母组合

string.push_back(单个字符)

题目链接/文章讲解:代码随想录

视频讲解:还得用回溯算法!| LeetCode:17.电话号码的字母组合_哔哩哔哩_bilibili

class Solution {
public:
    vector<string> res;
    string path;
    unordered_map<int,string> map={{2,"abc"},{3,"def"},{4,"ghi"},{5,"jkl"},{6,"mno"},{7,"pqrs"},{8,"tuv"},{9,"wxyz"}};
    vector<string> letterCombinations(string digits) {
        if(digits == "") return res;
        iter(digits, 0);
        return res;
    }
    // index: 用户输入的第index个数字字符
    void iter(string digits, int index){
        if(path.size() == digits.size()){
            res.push_back(path);
            return;
        }
        int digit = digits[index] - '0';
        for(int i = 0; i < map[digit].size(); i++){
            path.push_back(map[digit][i]);
            iter(digits, index + 1);
            path.pop_back();
        }
    }
};

 39. 组合总和 

本题是 集合里元素可以用无数次,那么和组合问题的差别 其实仅在于 startIndex上的控制

iter(candidates, target, i, sum);//i包括自己;i+1不包括自己

题目链接/文章讲解:代码随想录

视频讲解:带你学透回溯算法-组合总和(对应「leetcode」力扣题目:39.组合总和)| 回溯法精讲!_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        iter(candidates, target, 0, 0);
        return res;
    }
    void iter(vector<int>& candidates, int target, int startIndex, int sum){
        if(sum > target) return;// 一旦超过target了一定不对了return但不报错
        if(sum == target){//找到目标就返回
            res.push_back(path);
            return ;
        }
        for(int i = startIndex; i < candidates.size(); i++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            iter(candidates, target, i, sum);//i包括自己;i+1不包括自己
            path.pop_back();
            sum -= candidates[i];
        }

    }
};

 40.组合总和II 

本题开始涉及到一个问题了:去重。

注意题目中给我们 集合是有重复元素的,那么求出来的 组合有可能重复,但题目要求不能有重复组合。 

这道题目和39.组合总和 (opens new window)如下区别:

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

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

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

题目链接/文章讲解:代码随想录

视频讲解:回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        vector<bool> used(candidates.size(), false);
        iter(candidates, target, 0, 0, used);
        return res;
    }
    void iter(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used){
        //if(sum > target) return;
        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 - 1] == candidates[i] && used[i - 1] == false){
                continue;
            }//因为涉及到i-1了所以还得加个i>0的条件
            path.push_back(candidates[i]);
            used[i] = true;
            sum += candidates[i];
            iter(candidates, target, sum, i + 1, used);
            path.pop_back();
            used[i] = false;
            sum -= candidates[i];
        }
    }
};

分割问题 

131.分割回文串  

本题较难,大家先看视频来理解 分割问题,明天还会有一道分割问题,先打打基础。 

str.substr(index, legth)//而不是startIndex,endIndex

代码随想录

视频讲解:带你学透回溯算法-分割回文串(对应力扣题目:131.分割回文串)| 回溯法精讲!_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<string>> res;
    vector<string> path;
    vector<vector<string>> partition(string s) {
        iter(s, 0);
        return res;
    }
    void iter(string s, int startIndex){
        if(startIndex >= s.size()){
            res.push_back(path);
            return;
        }
        for(int i = startIndex; i < s.size(); i++){
            if(isPalindrome(s, startIndex, i)){
                path.push_back(s.substr(startIndex, i - startIndex + 1));
            }else continue;
            iter(s, i + 1);
            path.pop_back();
        }
    }
    bool isPalindrome(string s, int startIndex, int endIndex){
        for(int i = startIndex, j = endIndex; i < j; i ++,j --){
            if(s[i] != s[j]) return false;
        }
        return true;
    }
};

 93.复原IP地址  

本期本来是很有难度的,不过 大家做完 分割回文串 之后,本题就容易很多了 

注意:单独的0是ok的;为了不让stoll超过限制可以先限制str的长度。

题目链接/文章讲解:代码随想录

视频讲解:回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址_哔哩哔哩_bilibili

我的:

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

    void iter(string s, int startIndex){
        if(startIndex >= s.size() && path.size() == 4){
            string str ="";
            int i;
            for( i = 0; i < path.size() - 1; i++){
                str += path[i];
                str +='.';
            }
            str += path[i];
            res.push_back(str);
            return;
        }
        for(int i = startIndex; i < s.size(); i++){
            string str = s.substr(startIndex, i - startIndex + 1);
            if(right(str)){
                path.push_back(str);
            }else continue;
            iter(s, i + 1);
            path.pop_back();
        }
    }
    bool right(string str){
        if(str.size() == 1) return true;
        if(str.size() >= 4) return false;
        if(str[0] == '0') return false;
        long long data = stoll(str);
        if(data < 0 || data > 255) return false;
        return true;
    }
};

子集

 78.子集  

子集问题,就是收集树形结构中,每一个节点的结果。 整体代码其实和 回溯模板都是差不多的。 

题目链接/文章讲解:代码随想录

视频讲解:回溯算法解决子集问题,树上节点都是目标集和! | LeetCode:78.子集_哔哩哔哩_bilibili

class Solution {
public:
    vector<int> path;
    vector<vector<int>> res;
    vector<vector<int>> subsets(vector<int>& nums) {
        iter(nums, 0);
        return res;
    }
    void iter(vector<int>& nums, int startIndex){
        res.push_back(path);
        if(startIndex >= nums.size()){    //不加这个条件也ok
            return ;
        }
        
        for(int i = startIndex; i < nums.size(); i++){
            path.push_back(nums[i]);
            iter(nums, i + 1);
            path.pop_back();
        }
    }
};

 90.子集II 

大家之前做了 40.组合总和II 和 78.子集 ,本题就是这两道题目的结合,建议自己独立做一做,本题涉及的知识,之前都讲过,没有新内容。 

题目链接/文章讲解:代码随想录

视频讲解:回溯算法解决子集问题,如何去重?| LeetCode:90.子集II_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<bool> used(nums.size(), false);
        iter(nums, 0, used);
        return res;
    }
    void iter(vector<int>& nums, int startIndex, vector<bool>& used){
        res.push_back(path);
        if(startIndex >= nums.size()){
            return;
        }
        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;
            iter(nums, i + 1, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

491.递增子序列【难】

本题和大家刚做过的 90.子集II 非常像,但又很不一样,很容易掉坑里。 

难点在于不能排序还得去重

代码随想录

视频讲解:回溯算法精讲,树层去重与树枝去重 | LeetCode:491.递增子序列_哔哩哔哩_bilibili

排序

46.全排列 

本题重点感受一下,排列问题 与 组合问题,组合总和,子集问题的区别。 为什么排列问题不用 startIndex 

代码随想录

视频讲解:组合与排列的区别,回溯算法求解的时候,有何不同?| LeetCode:46.全排列_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<int>> res;
    vector<int> path;
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        iter(nums, used);
        return res;
    }
    void iter(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;// 一个排列里一个元素只能使用一次
            used[i] = true;
            path.push_back(nums[i]);
            iter(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

47.全排列 II 

本题 就是我们讲过的 40.组合总和II 去重逻辑 和 46.全排列 的结合,可以先自己做一下,然后重点看一下 文章中 我讲的拓展内容。 used[i - 1] == true 也行,used[i - 1] == false 也行 

代码随想录

视频讲解:回溯算法求解全排列,如何去重?| LeetCode:47.全排列 II_哔哩哔哩_bilibili

class Solution {
public:
vector<vector<int>> res;
vector<int> path;
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        vector<bool> used(nums.size(), false);
        iter(nums, used);
        return res;
    }

    void iter(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) continue;
            if(used[i] == true) continue;
            path.push_back(nums[i]);
            used[i] = true;
            iter(nums, used);
            path.pop_back();
            used[i] = false;
        }
    }
};

今天这三道题都非常难,那么这么难的题,为啥一天做三道? 

因为 一刷 也不求大家能把这么难的问题解决,所以 大家一刷的时候,就了解一下题目的要求,了解一下解题思路,不求能直接写出代码,先大概熟悉一下这些题,二刷的时候,随着对回溯算法的深入理解,再去解决如下三题。 

大家今天的任务,其实是 对回溯算法章节做一个总结就行。 

重点是看 回溯算法总结篇:

代码随想录

332.重新安排行程(可跳过) 

代码随
想录

棋盘

51. N皇后(可跳过) 

代码随想录

视频讲解:这就是传说中的N皇后? 回溯算法安排!| LeetCode:51.N皇后_哔哩哔哩_bilibili

class Solution {
public:
    vector<vector<string>> res;
    
    vector<vector<string>> solveNQueens(int n) {
        vector<string> chess(n,string(n,'.'));
        iter(n, 0, chess);
        return res;

    }
    void iter(int n, int row, vector<string>& chess){
        if(row == n){
            res.push_back(chess);
            return;
        }
        for(int col = 0; col < n; col++){
            if(isVaild(row, col, chess)){
                chess[row][col] = 'Q';
                iter(n, row+1, chess);
                chess[row][col] ='.';
            }
        }
    }
    bool isVaild(int row, int col, vector<string>& chess){
        // 同行无需判断
        // 同列
        for(int i = 0 ; i < chess.size(); i++){
            if(chess[i][col] == 'Q') return false;
        }
        //45°对角线
        for(int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){
            if(chess[i][j] == 'Q') return false;
        }
        // 135°对角线
        for(int i = row - 1, j = col + 1; i >= 0 && j < chess.size(); i--, j++){
            if(chess[i][j] == 'Q') return false;
        }
        return true;
    }
};

37. 解数独(可跳过) 

代码随想录

视频讲解:回溯算法二维递归?解数独不过如此!| LeetCode:37. 解数独_哔哩哔哩_bilibili

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值