组合总和
链接: 组合总和
给你一个 无重复元素 的整数数组 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;
}
};