LeetCode 《组合总和》系列题目总结
- 一、[39.组合总和 I](https://leetcode.cn/problems/combination-sum/)
- 二、 [40.组合总和ll ](https://leetcode.cn/problems/combination-sum-ii/)
- 三、[216.组合总和III](https://leetcode.cn/problems/combination-sum-iii/description/)
- 四、 [377.组合总和IV ](https://leetcode.cn/problems/combination-sum-iv/description/?envType=daily-question&envId=2024-04-22)
一、39.组合总和 I
思路:dfs,但是需要注意进行剪枝操作。
class Solution {
public:
vector<vector<int>> v;
vector<int > tmp,copy_candidates;
int tg;
void dfs(int u,int sum){
if(u<0||sum > tg) return;
if(sum == tg){
v.push_back(tmp);
return ;
}
tmp.push_back(copy_candidates[u]);
dfs(u,sum+copy_candidates[u]);//可多次选取
tmp.pop_back();
dfs(u-1,sum);//不选了
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target){
copy_candidates=candidates;
tg=target;
dfs(candidates.size()-1,0);//第一个数是正在遍历的数组下标,第二个数是总和
return v;
}
};
思考:这道题找的是组合数。
1.如果只返回组合数的个数,相当于完全背包问题,直接用dp做就可以。
2.这里需要返回所有可能的组合,那么我们就不能用dp去做,而是通过dfs,这样就可以保存所选的元素。
3.这里使用的dfs里,解决“同一个数字”可使用多次的地方是通过dfs(u,sum+copy_candidates[u])函数实现的,因为这里无重复元素的出现,所以可以放心大胆的用。但如果有重复的数字,那么我们就先对candidates数组进行一下去重操作即可。这样就避免了重复的组合。
二、 40.组合总和ll
思路:方法一,对数组candidates进行排序,然后对重复的数进行操作,把dfs里“重复的数”带来的“深度”变成“宽度”
class Solution {
public:
vector<vector<int>> v;
set<vector<int> > st;//解决重复的组合
vector<int> tmp;
vector<int> copy_candidates[105];//把重复的数,放进一个向量里,该向量只能选取其中一个数,向下遍历的深度更深
int copy_target;
int lens=0;
void dfs(int u,int sum){
if(sum>copy_target) return;
if(sum==copy_target){
vector<int> t=tmp;
sort(t.begin(),t.end());
if(st.find(t)==st.end()){//去重操作
v.push_back(t);
st.insert(t);
}
return;
}
if(u==lens) return ;
if(copy_candidates[u][0]+sum>copy_target) return;
for(int i=0;i<copy_candidates[u].size();i++){//copy_candidates[u]向量里只能选一个
for(int j=0;j<=i;j++)//这里是选的数
tmp.push_back(copy_candidates[u][0]);
dfs(u+1,sum+copy_candidates[u][i]);
for(int j=0;j<=i;j++)
tmp.pop_back();
}
dfs(u+1,sum);//不选copy_candidates[u]向量里的数
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());//先排个序,好对重复的数进行处理
int ct=1;
for(int i=0;i<candidates.size();i++){
if(i+1<candidates.size()&&candidates[i]==candidates[i+1]){
copy_candidates[lens].push_back(ct*candidates[i]);
ct++;
}else{
copy_candidates[lens++].push_back(ct*candidates[i]);
ct=1;
}
}
//sort(copy_candidates.begin(),copy_candidates.end());
copy_target=target;
dfs(0,0);
return v;
}
};
思路:方法二,对数组candidates进行排序,然后使用dfs,在dfs里面我们用for循环来遍历接下来的u~candidates.size()-1。遍历的时候,如果前一个数copy_candidates[i-1]和当前的数copy_candidates[i]相同,那么我们就跳过。原因是在dfs“前一个数copy_candidates[i-1]”的时候,我们就已经考虑到了“当前的数copy_candidates[i]”是否使用,如果我们还dfs“当前的数copy_candidates[i]”,就相当于是重复了部分操作,会造成重复并且消耗大量的时间。这也是为什么使用for循环,他带来的好处就是可以避免“重复的数字”带来的“重复遍历”,影响结果和耗时。
class Solution {
public:
vector<int> copy_candidates,tmp;
vector<vector<int>> v;
int copy_target;
void dfs(int u,int sum){
if(sum==copy_target){
v.push_back(tmp);
return ;
}
if(u==copy_candidates.size()||sum>copy_target){
return;
}
for(int i=u;i<copy_candidates.size();i++){
if(i==u||i>u&©_candidates[i]!=copy_candidates[i-1]){
tmp.push_back(copy_candidates[i]);
dfs(i+1,sum+copy_candidates[i]);
tmp.pop_back();
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
copy_target=target;
copy_candidates=candidates;
dfs(0,0);
return v;
}
};
思考:方法一,就相当于已经处理了“重复的数字”带来的不同组合,把这些组合通过只能选一次的方法,来避免重复所带来的错误。也就相当于分组背包的问题。
方法二,利用for循环来巧妙的避免“重复的数字”带来的重复组合。
三、216.组合总和III
思路:就dfs,时间复杂度0(2^9)
class Solution {
public:
vector<vector<int>> v;
vector<int> tmp;
void dfs(int u,int sum,int ct){
if(sum<0) return ;
if(sum==0&&ct==0){
v.push_back(tmp);
return ;
}
if(u>9||ct<=0) return;
tmp.push_back(u);
dfs(u+1,sum-u,ct-1);
tmp.pop_back();
dfs(u+1,sum,ct);
}
vector<vector<int>> combinationSum3(int k, int n) {
dfs(1,n,k);
return v;
}
};
思考:每个数字最多使用一次,就相当于是二分了。
四、 377.组合总和IV
思路:方法一,深度搜索dfs+保存中间状态,即记忆化搜索,时间复杂度0(nums.length*target)。有个注意点:中间状态的存储数组f的初始化一定不能默认为0,会导致记忆化失败,最后超时。原因是因为部分中间状态sum它本身就是无法到达的,如果f默认为0的话,无法区别之前是否已经到达过这个sum,这样就会导致重复在这个无法到达的值sum深搜下去。
在每一个sum下都进行一次for循环,遍历所有的元素,也就是在sum这个位置上,选一个元素,这就相当于是排了个序,并且支持重复选择元素。
这里的for循环和40.组合总和ll 里的for循环内部存在不同,最后达成的效果也不同,这里是需要思考和注意的地方。
class Solution {
public:
unsigned int f[1010]={1};
vector<int> copy_nums;
unsigned int dfs(int sum){
if(sum==0){
return 1;
}
if(f[sum]!=-1) return f[sum];
unsigned int ans=0;
for(int i=0;i<copy_nums.size();i++){
if(sum>=copy_nums[i]){
ans+=dfs(sum-copy_nums[i]);
}
}
return f[sum]=ans;
}
int combinationSum4(vector<int>& nums, int target) {
copy_nums=nums;
memset(f,-1,sizeof f);
return dfs(target);
}
};
思路:方法二,第一种方法是从上到下,通过状态数组来保存中间状态,避免大量的重复操作。现在第二种方法就是从下到上,通过动态规划dp就可以实现。所以dfs能做的题基本可以用dp。
class Solution {
public:
unsigned dp[1010];
int combinationSum4(vector<int>& nums, int target) {
dp[0]=1;
for(int i=1;i<=target;i++){
for(int j=0;j<nums.size();j++){
if(nums[j]<=i) dp[i]+=dp[i-nums[j]];
}
}
return dp[target];
}
};
思考:1.顺序不同的组合视作不同的组合,说白了就是排列问题
2.如果dfs里sum是从sum=target开始往下减的,就是便于处理重复状态的问题,但缺陷就是一般只能计组合数,不能记住组合。
3.dp能做的题,通过“记忆化搜索=dfs+状态记忆”也能做。
4.记忆化搜索方法,一定要注意状态数组的初始化,尽量不要为0,因为无法判断之前是否到达过,从而弱化为普通的dfs,造成超时。
5.如果这里数组当中有重复的数,那我们还是通过排序后进行去重操作就可以了。因为每次for循环都是遍历整个数组,相当于每个数都有重复次机会,如果数组中有重复的数,那么就会产生重复的组合。