代码随想录算法训练营第十二天|今天的回溯味儿有点冲

Leecode 40. 组合总和 II

链接:https://leetcode.cn/problems/combination-sum-ii/description/

刚开始做这道题真的遇到过很多问题:本质上是对递归流程的不熟悉

事实上不熟悉递归流程玩儿技巧都是耍流氓

并且,这道题真正有价值的地方就是我们发现问题,然后去debug的过程。通过这个过程我们能学到很多东西

而不是直接看答案

首先看最基础版本的代码

class Solution {
public:
    vector<vector<int>> res;
    vector<int> vec;
    // 题目规定了每个元素就只能用一次,所以我们一定要加上step
    // 并且每次递归的时候都从当前元素的下一个元素开始搜
    void recursion(vector<int>& candidates,int tar,int sum,int step)
    {
        if(sum > tar) return;
        if(sum == tar) 
        {
            res.push_back(vec);
            return;
        }
        for(int i=step;i<candidates.size();i++) 
        {
            vec.push_back(candidates[i]); 
            recursion(candidates,tar,sum + candidates[i],i+1);
            vec.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        recursion(candidates,target,0,0);
        return res;
    }
};
// candidates = [10,1,2,7,6,1,5]
// sort ->    = [1,1,2,5,6,7,10]
// target = 8
// 预期结果 = [[1,1,6],[1,2,5],[1,7],[2,6]]
// 输出    = [[1,1,6],[1,2,5],[1,7],[1,2,5],[1,7],[2,6]]

第二个[1,2,5]出现的原因显然就是因为我们将vec中的第一个元素1替换成了第二个1,所以也没有再出现[1,1,6]嘛

那么我们为什么不采用set去重呢?这是最直接的想法

然后看set改进后的代码

class Solution {
public:
    set<vector<int>> res;
    vector<int> vec;
    void recursion(vector<int>& candidates,int tar,int sum,int step)
    {
        if(sum > tar) return;
        if(sum == tar) 
        {
            res.insert(vec);
            return;
        }
        for(int i=step;i<candidates.size();i++) 
        {
            vec.push_back(candidates[i]);
            recursion(candidates,tar,sum + candidates[i],i + 1);
            vec.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        recursion(candidates,target,0,0);
        vector<vector<int>> out (res.begin(),res.end());
        return out;
    }
};
// 有超时的情况
// candidates =[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
// target = 30

熟悉递归流程的应该很容易理解为什么超时:当我们递归到第30层的时候,此时的sum就刚好等于target,然后return到29层,29层的for循环+1,递归到31层,31层又return···直到第29层的for循环耗尽,再return到第28层,28层的for循环+1···

所以我们需要想一个办法让后面无效的1都pass

class Solution {
public:
    vector<vector<int>> res;
    vector<int> vec; // 我们重新将set换成vector,因为不需要set去重了
    void recursion(vector<int>& candidates,int tar,int sum,int step)
    {
        if(sum > tar) return;
        if(sum == tar) 
        {
            res.push_back(vec);
            return;
        }
        for(int i=step;i<candidates.size();i++)
        {
            vec.push_back(candidates[i]); 
            recursion(candidates,tar,sum + candidates[i],i + 1);
            while((i + 1)< candidates.size() && candidates[i+1] == vec[vec.size()-1]) i++;
            // 如果pop之前发现下一个要加入的元素和即将pop的元素相同,我们就一直让i++
            vec.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(),candidates.end());
        recursion(candidates,target,0,0);
        return res;
    }
};

实际上,上面加入的这段代码也能处理初代版本中两个[1,2,5]的情况:

加入第一个1之后我们通过for循环遍历了整个数组,得到了三种符合题意的情况[[1,1,6],[1,2,5],[1,7]],那么此时以元素1为开头的数组就再没有符合题意的情况了,for循环遍历完毕之后就要把第一个元素1给pop出去

注意到了吗,此时sort后的target = [1,1,2,5,6,7,10]中第二个元素也是1,我们利用上面加入的while语句让i直接变成3从而跳过了第2个1,从而避免了重复···

Leecode131. 分割回文串

链接:https://leetcode.cn/problems/palindrome-partitioning/

还算是简单,若是回文串就切分,若不是就接着continue

除此之外我们还要写一个判断是否是回文的函数

// 总是有这种比较抽象的题目
// 先写一个判断回文的函数吧
class Solution {
public:
    vector<vector<string>> res;
    vector<string> vec;
    bool isPalindrome(const string &s,int l,int r)
    {
        if(!s.size()) return false;
        if(s.size() == 1) return true;
        while(l < r)
        {
            if(s[l]!=s[r]) return false;
            l++;
            r--;
        }
        return true;
    }
    void recursion(const string &s,int step)
    {
        if(step >= s.size())
        {
            res.push_back(vec);
            return;
        }
        // 我们不是枚举每次子串的长度,而是枚举位置看当前串是不是回文串
        for(int i = step;i < s.size();i++)
        {
            if(isPalindrome(s,step,i))
            {
                string str = s.substr(step,i - step + 1); // 因为substr函数右侧是开区间
                vec.push_back(str);
            }
            else continue;
            // 此时我们让step = i + 1
            recursion(s,i + 1);
            vec.pop_back();
        }
    }
    vector<vector<string>> partition(string s) {
        // 然后我们在这个函数中调用递归函数
        recursion(s,0); // 我们首先从第0个元素开始
        return res;
    }
};
Leecode 93. 复原 IP 地址

一道蛮艰辛的题目,从耗时8ms一直优化到耗时0ms

先来看初代方案:

class Solution {
public:
    // 不能有前导0那么遇见0就只能输出
    // 如果子串没有结束或者是目前的数字不够四个,就不记录
    // 结束条件是什么?子串结束了,或者是vector中已经有超过四个元素了,我们直接return
    vector<string> res;
    vector<string> vec;
    void recursion(string &s,int step)
    {
        if(step > s.size() || vec.size() > 4) return;
        // 如果当前索引已经走到子串末尾了
        if(step == s.size()) 
        {
            // 并且次数子串中刚好存储了4个字符串
            if(vec.size() == 4) 
            {
                // 用res记录当前结果
                string str;
                for(int i=0;i<3;i++) {str += vec[i];str += '.';}
                str += vec[3];
                res.push_back(str);
            }
            return;
        }
        // 接下来我们要去搜索合法的数字
        for(int i=step;i<s.size();i++)
        {
            // 一共定义了两个string
            // start_char表示这层递归循环刚开始时的首字符
            // is_zero表示此次push进vector中的串的首字符
            // 一般而言我们判断is_zero是不是0就OK
            // 但是有一种特殊情况是start_char是1,但是回溯回来由于下一个元素是0,此时is_zero是0,因此下一次回溯会push进0而不是10
            // 所以只有同时满足二者都为0的情况下才push进去0
            string is_zero = s.substr(i,i + 1);
            string start_char; if(step + 1 < s.size()) string start_char = s.substr(step,step+1);

            if(is_zero == "0" && start_char == "0") 
            {
                vec.push_back(is_zero);
                // 我们把0给push进去之后
                recursion(s,i + 1);
                vec.pop_back();
            }
            else 
            {
                string str = s.substr(step,i - step + 1);
                if(str[0] == '0' && str!="0") return; 
                int number = atoi(str.c_str());
                if(number <= 255) 
                {
                    vec.push_back(str);
                    recursion(s,i + 1);
                    vec.pop_back();
                }
                else return;
            }
        }
    }
    vector<string> restoreIpAddresses(string s) 
    {
        recursion(s,0);
        return res;
    }
};

第一种方案我在遇到0之后会直接push进去,但是由于回溯这里的处理逻辑就会比较复杂,必须同时满足“当前step处的值是0”和“当前选定子串的第一个字符是0”才会把单独的0给push进去

这里举一个反例,为什么不能只满足“当前选定子串的第一个字符是0”就把单独的0给push进去

"010010"
输出
["0.10.0.0","0.100.1.0"]
预期结果
["0.10.0.10","0.100.1.0"]

显然输出结果中的子串并不完整。奇怪,我们不是判断了只有在当前step等于子串末尾的时候才会push进去元素吗?其实是在回溯的时候出现了问题:元素1push进去后又把0给push进去,此时vec中的元素个数大于4了,所以pop出去0,然后return到元素1所在的第五层递归中,for 中i + 1,此时的元素是6,如果我们仅仅判断了s.substr(i,i + 1),这是0,所以就会把0给单独push进去,而不是10,错就错在这了

所以我们需要同时满足is_zero和start_char同时为0,才会把单独的0给push到vec中去

然后我们来看第二种写法:

第二种写法就是把判断都集中到一个函数中去,因为边写递归函数边判断实在是有些冗余并且复杂

class Solution {
public:
    vector<string> vec;
    vector<string> res;
    // 换一种写法吧,专门设计一个函数用来判断当前值是不是合法
    bool is_legitimate(string s)
    {
        int l = 0,r = s.size() - 1;
        // 其实不需要只要遇见 0 就将其push进去,只要判断当前切分的子串合不合法就完事了
        // 首先判断当前子串的第一个字符是不是0并且只有0
        if(s[l] == '0' && r > l) return false;
        // 然后判断当前值转化为数字后是不是位于0 - 255之间
        // 注意atoi函数遇到非数字字符会直接停止,所以若是遇见非法字符,是无法判断当前切割是不是合法的,幸好s仅由数字构成
        int num = atoi(s.c_str()); // 错误数据直接爆int了
        if(num > 255 || num < 0) return false;

        return true;
    }
    void recursion(string &s,int step)
    {
        // 若是当前扫描范围大于等于s.size(),那直接就是不合法的情况
        if(step > s.size()) return;
        if(step == s.size())
        {
            if(vec.size() != 4) return;
            string str;
            for(int i=0;i<3;i++) {str += vec[i];str += '.';}
            str += vec[3];
            res.push_back(str);
        }
        for(int i=step;i<s.size();i++)
        {
            string str = s.substr(step,i - step + 1);
            if(is_legitimate(str))
            {
                vec.push_back(str);
                recursion(s,i + 1);
                vec.pop_back();
            }
            // 考虑一下,当前不合法,后序还会有合法的可能性吗
        }
    }
    vector<string> restoreIpAddresses(string s) 
    {
        recursion(s,0);
        return res;
    }
};

有个细节,我们在is_legitimate函数中判断当前字符串是不是合法的时候,直接用atoi将字符串转化为int,但是有可能需要转化的串远远超过int范围,而溢出之后再判断是不是比255大就不合适了,所以我们开始的时候就判断这个串的长度是不是大于等于4,若是,那么一定非法,直接return

但是还是不够快,所以后续我们加上了三个剪枝操作

class Solution {
public:
    vector<string> vec;
    vector<string> res;
    bool is_legitimate(string s)
    {
        if(s.size() > 3) return false; 
        int l = 0,r = s.size() - 1;
        if(s[l] == '0' && r > l) return false;
        long long num = atoi(s.c_str());
        if(num > 255) return false;

        return true;
    }
    void recursion(string &s,int step)
    {
        if(step > s.size()) return;
        // 再加上一个判断来剪枝,因为正确结果肯定是包含全部数字的 -- 剪枝1
        if(vec.size() == 3) if(!is_legitimate(s.substr(step,s.size()))) return; // 判断后面的子串是不是合法,不合法直接return

        if(step == s.size())
        {
            if(vec.size() != 4) return;
            string str;
            for(int i=0;i<3;i++) {str += vec[i];str += '.';}
            str += vec[3];
            res.push_back(str);
        }

        for(int i=step;i<s.size();i++)
        {
            string str = s.substr(step,i - step + 1);
            if(is_legitimate(str))
            {
                vec.push_back(str);
                recursion(s,i + 1);
                vec.pop_back();
            }
            else return; // 剪枝2
            // 考虑一下,当前不合法,后序还会有合法的可能性吗
        }
    }
    vector<string> restoreIpAddresses(string s) 
    {
        if(s.size() < 4 || s.size() > 12) return {}; // 剪枝3
        recursion(s,0);
        return res;
    }
};

剪枝操作解释:

  1. 第一个剪枝是最有效的:因为符合题意的结果一定是包含所有字符的四个串,因此我们在push进去三个子串后直接判断后序的字符组成的串它是不是合法的,若不是合法的直接return
  2. 第二个剪枝是在push的时候发生的,若当前子串不合法,那么一定是有前导0或者是值大于255了,那么接着continue让子串边长还会有合法的可能性吗?显然没有,所以else return。这里一定不要直接写return,因为会有回溯,回溯回来肯定不能再return了吧
  3. 第三个剪枝就是在restoreIpAddresses函数中输入调用递归函数之前,若是判断当前的给的s一定不能组成一个合法的ip地址,就直接return false
Leecode 78. 子集

链接:https://leetcode.cn/problems/subsets/

第一种方法就是枚举当前子串的长度

class Solution {
public:
    // 我的理解其实是for循环中嵌套递归
    // 就是从1一直枚举到字符串的长度,分别去搜索答案
    // 稍微简化一点的写法就是先把空集和全集给push进去,然后push其他长度的子集
    vector<vector<int>> res;
    vector<int> vec;
    void recursion(vector<int>& nums,int k,int step)
    {
        // 元素收集满了,记录当前vec然后return
        if(vec.size() == k)
        {
            res.push_back(vec);
            return;
        }
        for(int i = step;i<nums.size();i++)
        {
            vec.push_back(nums[i]);
            recursion(nums,k,i + 1); // 是取搜下一个元素了
            vec.pop_back();
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) 
    {
        // 首先吧空集和全集都给push进去
        res.push_back({});
        res.push_back(nums);
        // 依次枚举子串的长度
        for(int i = 1; i< nums.size() ;i++) 
        {
            vec.clear();
            recursion(nums,i,0); // 搜i个元素,从第0位开始搜
        }
        return res;
    }
};

其实我们在搜索的过程中,就出现了全部的子集···

class Solution {
private:
    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& nums, int startIndex) {
        result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己
        if (startIndex >= nums.size()) { // 终止条件可以不加
            return;
        }
        for (int i = startIndex; i < nums.size(); i++) {
            path.push_back(nums[i]);
            backtracking(nums, i + 1);
            path.pop_back();
        }
    }
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }
};

真的是需要好好总结的啊

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
第二十二天的算法训练营主要涵盖了Leetcode题目中的三道题目,分别是Leetcode 28 "Find the Index of the First Occurrence in a String",Leetcode 977 "有序数组的平方",和Leetcode 209 "长度最小的子数组"。 首先是Leetcode 28题,题目要求在给定的字符串中找到第一个出现的字符的索引。思路是使用双指针来遍历字符串,一个指向字符串的开头,另一个指向字符串的结尾。通过比较两个指针所指向的字符是否相等来判断是否找到了第一个出现的字符。具体实现的代码如下: ```python def findIndex(self, s: str) -> int: left = 0 right = len(s) - 1 while left <= right: if s[left == s[right]: return left left += 1 right -= 1 return -1 ``` 接下来是Leetcode 977题,题目要求对给定的有序数组中的元素进行平方,并按照非递减的顺序返回结果。这里由于数组已经是有序的,所以可以使用双指针的方法来解决问题。一个指针指向数组的开头,另一个指针指向数组的末尾。通过比较两个指针所指向的元素的绝对值的大小来确定哪个元素的平方应该放在结果数组的末尾。具体实现的代码如下: ```python def sortedSquares(self, nums: List[int]) -> List[int]: left = 0 right = len(nums) - 1 ans = [] while left <= right: if abs(nums[left]) >= abs(nums[right]): ans.append(nums[left ** 2) left += 1 else: ans.append(nums[right ** 2) right -= 1 return ans[::-1] ``` 最后是Leetcode 209题,题目要求在给定的数组中找到长度最小的子数组,使得子数组的和大于等于给定的目标值。这里可以使用滑动窗口的方法来解决问题。使用两个指针来表示滑动窗口的左边界和右边界,通过移动指针来调整滑动窗口的大小,使得滑动窗口中的元素的和满足题目要求。具体实现的代码如下: ```python def minSubArrayLen(self, target: int, nums: List[int]) -> int: left = 0 right = 0 ans = float('inf') total = 0 while right < len(nums): total += nums[right] while total >= target: ans = min(ans, right - left + 1) total -= nums[left] left += 1 right += 1 return ans if ans != float('inf') else 0 ``` 以上就是第二十二天的算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值