1. 题目来源
链接:39. 组合总和
必看,必刷,dfs 经典:
- [Mdfs] lc39. 组合总和(dfs+经典)
- [Mdfs] lc40. 组合总和 II(dfs+经典)
- [Mdfs] lc46. 全排列(dfs+经典)
- [Mdfs] lc47. 全排列 II(dfs搜索顺序+去重处理+知识理解+经典)
- [Mdfs] lc77. 组合(组合类型枚举+题目总结+经典)
- [Mdfs] lc78. 子集(二进制枚举+排列类型枚举+经典)
- [Mdfs] lc90. 子集 II(组合类型枚举+多重背包+去重经典)
2. 题目解析
需要统计所有的方案数,那么完全背包在这貌似就不能使用了。就暴力搜所有方案就行了。
暴搜顺序是很重要的,在这顺序遍历数组中的每一个数,并枚举它可能取的所有个数,就能不重不漏的搜完所有可能方案。
代码细节:
path
当前层的元素添加要放到dfs
的后面。因为一开始枚举的是一个都不选的情况,即i=0
的情况,所以不要一开始就加进去。- 回溯的时候,恢复现场。和枚举个数的时候类似,当前
dfs
添加了多少个就pop
多少个。
dfs
暴搜,一直都被认为是简单,确实,但是还是得多写点题,才能考虑到搜索顺序和代码边界、细节情况!
看看官方题解蛮不错的,图解很清楚!
时间复杂度:不会分析…
空间复杂度:不会分析…,这个貌似是
O
(
t
a
r
g
e
t
)
O(target)
O(target)?就是递归深度吧
代码:
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<vector<int>> combinationSum(vector<int>& c, int target) {
dfs(c, 0, target);
return ans;
}
void dfs(vector<int>& c, int u, int target) {
if (target == 0) {
ans.push_back(path);
return ;
}
// 如果枚举完最后一个数还没能凑出target,则直接 return
if (u == c.size()) return ;
// u表示枚举到当前的数
// c[u] * i <= target 选i个,最小单位为1,相乘则表示不能超过target这个值
for (int i = 0; c[u] * i <= target; ++i) {
dfs(c, u + 1, target - c[u] * i);
path.push_back(c[u]); // 由于第一次是选 0 个,则把这个放到 dfs 的后面。当 dfs 进行两次的时候,再添加
}
// 恢复现场
for (int i = 0; c[u] * i <= target; ++i) {
path.pop_back();
}
}
};
2021年08月24日 个人理解:
这个 dfs 走了个顿挫,就很巧妙。因为一开始 c[u]
的选取个数 i
是从 0 开始的。所以在进入下一层时,要保证 path
中没有 c[u]
存在,所以先让它进入下一层,然后再给 path
中添加一个 c[u]
进去。
这样就能保证每次进入下一层时,path
中的 c[u]
个数和 i
的值保持一致。即和定义相同。
并且如果 c[u]
一个都不能选的话,必然在 i=1
时跳出 for
循环,这个尾部加如的 1 个 c[u]
就会立马被恢复现场清掉,并不会产生错误的影响,很是巧妙。
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int ans;
void dfs(vector<int>& c, int u, int target) {
if (u == c.size() && target != 0) return ;
if (target == 0) {
res.push_back(path);
return ;
}
// 选当前数。i从0 开始,指的是不选当前数的情况
for (int i = 0; c[u] * i <= target; i ++ ) {
dfs(c, u + 1, target - c[u] * i);
path.push_back(c[u]); // 后面再添加,放到dfs后面,因为一开始是一个都不选的情况
}
/* 当i=1时,path 中才会有一个c[u]存在,并dfs到下一层,而当 i=0时,path中并没有存在的该元素,就直接dfs到下一层。
由于i=0是必然要执行一次的,因为c[u]*0<=target 必然成立。
但是,如果紧接着 i=1不成立的话,for 循环跳出,这个刚加进去的c[u]就会被立马pop_back掉。并不影响答案
所以这个写法就保证了,在dfs过程中i与path中c[u]的个数的严格对应。
即,一开始,for循环进入,是 0 个,dfs进入下一层也是0个,然后回到本层时,才在尾部立马加 1 个,紧接着 i=1 就进入下一层dfs
此时,path中就有了刚刚那个刚加入的1个c[u]。
所以用这种方式,将 i个c[u]与枚举的层数严格对应起来。保证在进入下一层时,当前的c[u]是i个。
即i=0时,进入下一层时,c[u]是0个
i=1时,进入下一层时,c[u]是1个
...
仍要多多理解
*/
for (int i = 0; c[u] * i <= target; i ++ ) path.pop_back();
}
vector<vector<int>> combinationSum(vector<int>& c, int target) {
dfs(c, 0, target);
return res;
}
};