最近做leetcode每日一题,连续三四天都做到一个主题,叫做“组合”,核心算法思想是 回溯法,跟子集、子集II、全排列、路径规划等思想较为相似,这里做一个记录。
77. 组合
给定两个整数 n 和 k,返回 1 … n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
39.组合总数
给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
提示:candidate 中的每个元素都是独一无二的。
40.组合总数II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
216 组合总数III
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明:
所有数字都是正整数。
解集不能包含重复的组合。
示例 1:
输入: k = 3, n = 7
输出: [[1,2,4]]
示例 2:
输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
这几道题的核心思想都是回溯+剪枝,对这几道题做一个归纳,大概如下:
数组无重复数字,规定每个组合的元素个数为k。
直接进行深搜+边界剪枝即可。
class Solution {
public:
vector<int> temp;
vector<vector<int>> ans;
void dfs(int cur, int n, int k) {
// 剪枝:temp 长度加上区间 [cur, n] 的长度小于 k,不可能构造出长度为 k 的 temp
if (temp.size() + (n - cur + 1) < k) {
return;
}
// 记录合法的答案
if (temp.size() == k) {
ans.push_back(temp);
return;
}
/* 这种写法也可以,下面采用循环,不过需要注意循环起点不能是0,因为这里不是求全排列,而就是单纯选取当前的数
// 考虑选择当前位置
temp.push_back(cur);
dfs(cur + 1, n, k);
temp.pop_back();
// 考虑不选择当前位置
dfs(cur + 1, n, k);
*/
for(int i = cur; i <= n; ++i){
temp.push_back(i);
dfs(i + 1, n, k);
temp.pop_back();
}
}
vector<vector<int>> combine(int n, int k) {
dfs(1, n, k);
return ans;
}
};
每个数字可以任意选取,数组中无重复元素。
同样由于无重复元素,故直接深搜即可。
dfs(candidates, i, target, cursum + candidates[i]);
这里不需要i + 1,因为每个数字可任意选取。
class Solution {
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
if(candidates.size() <= 0){
return res;
}
dfs(candidates, 0, target, 0);
return res;
}
void dfs(vector<int>& candidates, int begin, int target, int cursum){
if(target == cursum){
res.emplace_back(temp);
return;
}
for(int i = begin; i < candidates.size(); ++i){
if(cursum + candidates[i] <= target){
temp.emplace_back(candidates[i]);
//注意这里不是i + 1,因为每个元素可以被重复利用*
dfs(candidates, i, target, cursum + candidates[i]);
temp.pop_back();
}
}
}
private:
vector<vector<int>> res;
vector<int> temp;
};
每个数字只能使用一次,解集不能包含重复的组合。
为了避免重复,首先我们可以先对源数组排序,排序的目的就是为了避免紧挨着两个相同的元素(nums[i]和nums[i-1])被重复使用,然后当前使用过的元素下一次就不能再次使用。
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
if(candidates.size() <= 0){
return res;
}
sort(candidates.begin(), candidates.end());
dfs(candidates, 0, 0, target);
return res;
}
void dfs(vector<int>& nums, int begin, int cursum, int target){
if(cursum == target){
res.emplace_back(temp);
return;
}
for(int i = begin; i < nums.size(); ++i){
//由于事先排序好了,这里直接去重即可。
if(cursum + nums[i] <= target){
if(i > begin && nums[i] == nums[i - 1]){
continue;
}
temp.emplace_back(nums[i]);
dfs(nums, i + 1, cursum + nums[i], target);
temp.pop_back();
}
}
}
private:
vector<vector<int>> res;
vector<int> temp;
};
相加和为 n 的 k 个数,只允许包含 1~9,解集不能重复
其实也是一样的,这个更简单一些,对于每个当前数字而言,下一个数字可以选择当前数字之后的数字,也不需要排序了,并且源数组直接就是1~9,在1 - 9进行选择即可。
class Solution {
public:
vector<vector<int>> combinationSum3(int k, int n) {
if(k <= 0){
return vector<vector<int>>();
}
// 1. 不能存在重复的数
dfs(1, 0, n, k);
return res;
}
void dfs(int begin, int cursum, int n, int k){
if(cursum >= n && temp.size() == k){
if(n == cursum){
res.emplace_back(temp);
}
return;
}
//题目说元素只能是1~9, 所以可以不需要nums, 直接循环
for(int i = begin; i < 10; ++i){
//从begin开始, 一直往后搜索,直到满足退出条件。
if(cursum + i <= n){
temp.emplace_back(i);
dfs(i + 1, cursum + i, n, k);
temp.pop_back(); //回溯
}
}
}
private:
vector<vector<int>> res;
vector<int> temp;
};