
一、先抓准题目核心特点
先明确三个关键条件,决定了我们的解法方向:
- 数组元素无重复:不用处理 "相同元素选多次导致重复组合" 的问题
- 每个数字可以无限次选取:选完一个数之后,还能继续选它自己
- 组合不重复 :
[2,3]和[3,2]算同一种组合,必须去重
这道题的本质是带无限选取的组合枚举,属于典型的「回溯算法」题。
二、核心难点:怎么保证组合不重复?
这是这道题最关键的问题:如果随便选,很容易出现 [2,3] 和 [3,2] 这种顺序不同、但元素完全一样的重复组合。
通用去重思想:强制组合按数组顺序递增 我们规定:选数字的时候,只能从左往右选,不能回头选左边的数字 。 比如数组是 [2,3,6,7]:
- 选了 2 之后,下一个可以继续选 2,或者选 3、6、7
- 但选了 3 之后,下一个不能回头选 2 这样组合里的数字永远是按数组顺序出现的,天然就不会有顺序颠倒的重复组合。
方法一:「选或不选」决策法(最易理解)
1. 思路原理
对数组里的每一个数字,我们只做两种决策:
- 不选它:跳过这个数字,去考虑下一个数字
- 选它 :把它加入当前路径,因为可以重复选,所以下一层依然考虑这个数字(不往后跳)
对应完全背包的思路:每个物品可以选无限次,决策 "拿还是不拿"。
2. 递归三要素
- 递归参数 :
i(当前考虑到第 i 个数字)、left(距离 target 还差多少) - 递归边界 :
left == 0:凑出目标和了,把当前路径加入答案,返回i == 数组长度或left < 0:所有数字考虑完了,或者凑超了,直接返回
- 递归决策 :
- 不选当前数:递归
dfs(i+1, left) - 选当前数:把数字加入路径 → 递归
dfs(i, left - candidates[i])(i 不变,下次还能选) → 递归回来后弹出数字(恢复现场)
- 不选当前数:递归
3. 手算走一遍(示例 1:candidates=2,3, target=5)
plaintext
dfs(i=0, left=5) // 考虑数字2
├─ 不选2 → dfs(i=1, left=5) // 考虑数字3
│ ├─ 不选3 → i=2越界,返回
│ └─ 选3 → left=2,继续dfs(i=1, left=2),3>2,最后返回
└─ 选2 → path=[2], left=3,dfs(i=0, left=3)
├─ 不选2 → dfs(i=1, left=3)
│ └─ 选3 → left=0,path=[2,3],加入答案
└─ 选2 → path=[2,2], left=1,继续递归后返回
最终只得到 [2,3],没有重复组合。
cpp
class Solution {
vector<vector<int>> ans;
vector<int> path;
vector<int> candidates;
void dfs(int i,int left){ //i代表当前数的索引
//left 表示和target的距离
//如果left为0 就是目标值了 直接加入答案
if(left == 0){
ans.push_back(path);
return;
}
//边界2 如果数字用完了 或 凑超了,返回
if(i == candidates.size() || left<0){
return;
}
//决策1 不选择当前的数 去下一个
dfs(i+1,left);
//决策2 选当前的数字
path.push_back(candidates[i]);
dfs(i,left-candidates[i]);//i不变 因为是选当前的数 然后距离变小了
path.pop_back();//恢复现场
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
this->candidates = candidates;
dfs(0,target);
return ans;
}
};