代码随想录算法训练营第23天 | LeetCode39.组合总和、LeetCode40.组合总和II、LeetCode131.分割回文串

目录

LeetCode39.组合总和

LeetCode40.组合总和II

LeetCode131.分割回文串


 

LeetCode39.组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

思路:之前也做了一个求组合之和,无重复元素,限定目标之和以及元素个数,采用的是回溯。

同样,这道题同样可以采用回溯方法,这道题没有限制个数,同时需要注意后面元素的起始位置可以从自身开始,因为题目中说同一个数字可以无限制重复被选取。

其实相对来说还是比较简单的。

    vector<vector<int>> result;//记录最终结果
    vector<int> path; //记录每次结果
    int sum = 0;//记录每次求和
    void backtracking(vector<int>& candidates, int target, int startIndex){
        if(sum > target) return;//sum的值大于了target,直接返回
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size(); i ++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, startIndex);
            path.pop_back();//开始回溯
            sum -= candidates[i];
            startIndex ++; //准备从下一个元素开始遍历
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        backtracking(candidates, target, 0);
        return result;     
    }

 当然,可以对上面的代码进行优化。

先对要处理的数组排序,然后当sum+candidates[i]>target是,就别进入循环了,因为就算进入递归了也会立即返回。所以优化在于当sum加上本次循环的元素值已经大于target时,就继续下一次循环,这次循环就不进行了。

    vector<vector<int>> result;//记录最终结果
    vector<int> path; //记录每次结果
    int sum = 0;//记录每次求和
    void backtracking(vector<int>& candidates, int target, int startIndex){
        if(sum == target){
            result.push_back(path);
            return;
        }
        //当sum+candidates[i]<=target时可以继续循环,否则进入下一个循环
        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, startIndex);
            path.pop_back();//开始回溯
            sum -= candidates[i];
            startIndex ++; //准备从下一个元素开始遍历
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(),candidates.end());//需要排序
        backtracking(candidates, target, 0);
        return result;     
    }

时间复杂度:O(n*2^n)

空间复杂度:O(target)

LeetCode40.组合总和II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次 。

注意:解集不能包含重复的组合。

思路:前面做过好几道这样的题目了。大体思路一致,但是仍然存在难点。

首先是每个元素只能使用一次,且不能有重复的解。

这里难主要是难在不能有重复的解,但是每个解里面能够存在重复的元素

如何能够在同一组解里面允许相同元素进入,但是在不同解里面碰到重复元素时能够continue,就是解决本题的关键。

这里提供两种方法。

第一种是设置一个used数组,记录各元素的使用情况,当两值相等时,如果used[i]==true,说明它们是在同一组解里面,只要不超过target,就能加入试试;而两值相等,used[i]==false时,说明前面和它相等的元素的解已经找完了,递归跳出来了,开始尝试递归它了,此时就不能进入循环,而是应该continue,否则递归下去必然就会出现重复解。

    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex, vector<bool>& used){
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
            //当candidates[i]==candidates[i-1]&&used[i-1]==false时,说明是在另一个可能是答案的数组中进行(同树层),
            //这时就不允许循环了,因为前面一个相等的元素已经将该元素可能的情况遍历完全了,再遍历的话必然重复
            //当candidates[i]==candidates[i-1]&&used[i-1]==true时,说明是在同一个可能是答案的数组中(同树枝),
            //这时是允许继续循环的,因为相当于使用前一个相等元素尝试和后面的元素组合起来,尝试找到解 
            if(i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) continue;
            path.push_back(candidates[i]);
            sum += candidates[i];
            used[i] = true;
            backtracking(candidates, target, sum, i + 1, used);
            path.pop_back();//开始回溯
            sum -= candidates[i];
            used[i] = false;
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        vector<bool> used(candidates.size(), false);//记录元素使用情况
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end());//去重需要进行排序
        backtracking(candidates, target, 0, 0, used);
        return result;
    }

时间复杂度:O(n*2^n)

空间复杂度:O(n)

第二种方法是不设置used数组,而是使用startIndex与下标索引i进行比较,当i>startIndex且前后两值相等时,continue,原因上面也说过了,就是避免出现重复解,同时也保证能够在不超过target的情况下能够将重复元素加入同一组解中尝试构造一组解。

当然,这样避免重复元素,需要首先对待处理数组进行排序,两种方法都是这样的。

    vector<vector<int>> result;
    vector<int> path;
    void backtracking(vector<int>& candidates, int target, int sum, int startIndex){
        if(sum == target){
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i ++){
            //这里可以使用startIndex来和i进行比较,判断是否能进入循环,此时不用再设置used数组
            if(i > startIndex && candidates[i] == candidates[i - 1]) continue;
            path.push_back(candidates[i]);
            sum += candidates[i];
            backtracking(candidates, target, sum, i + 1);
            path.pop_back();//开始回溯
            sum -= candidates[i];
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end());//去重需要进行排序
        backtracking(candidates, target, 0, 0);
        return result;
    }

LeetCode131.分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是

回文串。返回 s 所有可能的分割方案。

思路:分割回文串,需要解决的主要有两个问题

如何确定分割以及如何判断是否为回文串

从第一个元素分割开始,当是回文串的时候,就加入path,当进入递归时,下标要加1,分割位置也从找到第一个回文串的地方开始设置,后面只要找到了回文串,就将它加入path,并且继续递归,那什么时候停止呢?就在索引到达最后位置时,前面的也就已经划分完成,加入result,开始返回。所以如何确定分割也就完成了

那么如何判断是否为回文串呢?

这里有两种方法。

第一种是传入字符串以及其实位置,使用双指针,一个从头遍历,一个从尾遍历,判断元素值是否相同。整体代码如下所示。

    vector<vector<string>> result;//存放最终结果
    vector<string> path;//存放单次结果
    void backtracking(string& s, int startIndex){
        if(startIndex == s.size()){//当startIndex的大小等于s的大小时,说明已经切割完成
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < s.size(); i ++){
            if(isPalindrome(s, startIndex, i)){//判断是否为回文
                string str = s.substr(startIndex, i - startIndex + 1);//提取子串
                path.push_back(str);
            }else{
                continue;
            }
            backtracking(s, i + 1);//这里是i+1,避免分割位置重合
            path.pop_back();//回溯
        }
    }
    bool isPalindrome(string& s, int begin, int end){
        //使用双指针前后比较元素,若存在不同的元素,则直接返回false
        for(int i = begin, j = end; i <= j; i ++, j --){
            if(s[i] != s[j]){
                return false;
            }
        }
        return true;
    }
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        backtracking(s, 0);
        return result;
    }

时间复杂度:O(n*2^n)

空间复杂度:O(n^2)

第二种是使用动态规划,提前将字符串中的回文串情况统计出来,然后在判断时直接使用下标查询即可。整体代码如下所示。

    vector<vector<string>> result;//存放最终结果
    vector<string> path;//存放单次结果
    vector<vector<bool>> palindrome;//用于存放字符串的回文情况
    void backtracking(string& s, int startIndex){
        if(startIndex == s.size()){//当startIndex的大小等于s的大小时,说明已经切割完成
            result.push_back(path);
            return;
        }
        for(int i = startIndex; i < s.size(); i ++){
            if(palindrome[startIndex][i]){//判断是否为回文
                string str = s.substr(startIndex, i - startIndex + 1);//提取子串
                path.push_back(str);
            }else{
                continue;
            }
            backtracking(s, i + 1);//这里是i+1,避免分割位置重合
            path.pop_back();//回溯
        }
    }
    void computePalindrome(string& s, vector<vector<bool>>& palindrome){
        //palindrome[i][j]表示s[i]到s[j]是否为回文串
        palindrome.resize(s.size(), vector<bool>(s.size(), false));
        //将palindrome的大小更新为s的大小,使之成为一个s大小的行、列的二维数组,初始值为false
        for(int i = s.size() - 1; i >= 0; i --){
            //这里需要倒着求是否为回文数,这样能够保证求i行的情况时,能够用到i+1行的内容
            for(int j = i; j < s.size(); j ++){
                if(i == j){palindrome[i][j] = true;}//当为单个字符的情况
                else if(j - i == 1){palindrome[i][j] = (s[i] == s[j]);}//当为两个字符的情况
                else palindrome[i][j] = (s[i] == s[j] && palindrome[i + 1][j - 1]);//当为多个字符的情况
            }
        }
    }
    vector<vector<string>> partition(string s) {
        result.clear();
        path.clear();
        palindrome.clear();
        computePalindrome(s, palindrome);
        backtracking(s, 0);
        return result;
    }

感谢你的阅读,希望我的文章能够给你帮助,如果有帮助,麻烦点赞加收藏,或者点点关注,非常感谢。

如果有什么问题欢迎评论区讨论!

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
第二十二算法训练营主要涵盖了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(&#39;inf&#39;) 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(&#39;inf&#39;) else 0 ``` 以上就是第二十二算法训练营的内容。通过这些题目的练习,可以提升对双指针和滑动窗口等算法的理解和应用能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值