代码随想录算法训练营day27 | 39. 组合总和、40.组合总和II、131.分割回文串

本文介绍了如何通过递归和回溯算法解决组合总和问题(包括无重复和单次使用条件),以及组合总和II中去除重复元素的要求。同时讨论了分割回文串问题的解决方案,涉及时间复杂度和空间复杂度分析。
摘要由CSDN通过智能技术生成

组合总和

链接: 组合总和

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

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

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

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。
示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]
示例 3:

输入: candidates = [2], target = 1
输出: []

思路

递归函数参数
这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入)

首先是题目中给出的参数,集合candidates, 和目标值target。

此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。

本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?

我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:77.组合 (opens new window),216.组合总和III (opens new window)。

如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex

最后再剪枝优化。

解题方法

看代码即可

复杂度

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

Code

class Solution {
private:
    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;
        }

        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        result.clear();
        path.clear();
        sort(candidates.begin(), candidates.end()); // 需要排序
        backtracking(candidates, target, 0, 0);
        return result;
    }
};

组合总和II

链接: 组合总和II

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

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

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

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

思路

按顺序搜索,设置合理的变量,在搜索的过程中判断是否会出现重复集结果。重点理解对输入数组排序的作用和 参考代码 中大剪枝和小剪枝 的意思。

解题方法

由第 39 题我们知道,数组 candidates 有序,也是 深度优先遍历 过程中实现「剪枝」的前提。
将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组 升序 排序,重复的元素一定不是排好序以后相同的连续数组区域的第 111 个元素。也就是说,剪枝发生在:同一层数值相同的结点第 222、333 … 个结点,因为数值相同的第 111 个结点已经搜索出了包含了这个数值的全部结果,同一层的其它结点,候选数的个数更少,搜索出的结果一定不会比第 111 个结点更多,并且是第 111 个结点的子集。(说明:这段文字很拗口,大家可以结合具体例子,在纸上写写画画进行理解。)

复杂度

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

Code

class Solution {

private:
    vector<int> candidates;
    vector<vector<int>> res;
    vector<int> path;
public:
    void DFS(int start, int target) {
        if (target == 0) {
            res.push_back(path);
            return;
        }

        for (int i = start; i < candidates.size() && target - candidates[i] >= 0; i++) {
            if (i > start && candidates[i] == candidates[i - 1])
                continue;
            path.push_back(candidates[i]);
            // 元素不可重复利用,使用下一个即i+1
            DFS(i + 1, target - candidates[i]);
            path.pop_back();
        }
    }

    vector<vector<int>> combinationSum2(vector<int> &candidates, int target) {
        sort(candidates.begin(), candidates.end());
        this->candidates = candidates;
        DFS(0, target);
        return res;
    }
};

分割回文串

链接: 分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是
回文串。返回 s 所有可能的分割方案。

示例 1:

输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:

输入:s = “a”
输出:[[“a”]]

思路

这道题是经典的回溯算法,完全遵循 回溯算法详解 中讲到的算法框架,和 一文秒杀所有排列组合子集问题 有异曲同工之妙。

我们就按照最直接粗暴的方式思考就行了:

从 s 的头部开始暴力穷举,如果发现 s[0…i] 是一个回文子串,那么我们就可以把 s 切分为 s[0…i] 和 s[i+1…],然后我们去尝试把 s[i+1…] 去暴力切分成多个回文子串即可。

PS: 至于如何判断一个字符串是否是回文串,我在 数组双指针技巧汇总 中的左右指针部分有讲解,很简单。

把这个思路抽象成回溯树,树枝上是每次从头部穷举切分出的子串,节点上是待切分的剩余字符串

只有树枝上的子串是回文串时才能继续往下走,最后如果能够走到空串节点,就说明整个 s 完成了切分,也就是得到了一个合法的答案。

只要套用回溯算法框架,按照上述规则遍历整棵回溯树即可找到所有合法切分,直接看代码吧。

解题方法

复杂度

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

Code

class Solution {
public:
    vector<vector<string>> res;
    deque<string> track;

    vector<vector<string>> partition(string s) {
        backtrack(s, 0);
        return res;
    }

    // 回溯算法框架
    void backtrack(string &s, int start) {
        if (start == s.length()) {
            // base case,走到叶子节点
            // 即整个 s 被成功分割为若干个回文子串,记下答案
            res.push_back(vector<string>(track.begin(), track.end()));
        }
        for (int i = start; i < s.length(); i++) {
            if (!isPalindrome(s, start, i)) {
                // s[start..i] 不是回文串,不能分割
                continue;
            }
            // s[start..i] 是一个回文串,可以进行分割
            // 做选择,把 s[start..i] 放入路径列表中
            track.push_back(s.substr(start, i - start + 1));
            // 进入回溯树的下一层,继续切分 s[i+1..]
            backtrack(s, i + 1);
            // 撤销选择
            track.pop_back();
        }
    }

    // 用双指针技巧判断 s[lo..hi] 是否是一个回文串
    bool isPalindrome(string &s, int lo, int hi) {
        while (lo < hi) {
            if (s[lo] != s[hi]) {
                return false;
            }
            lo++;
            hi--;
        }
        return true;
    }
};
  • 8
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值