回溯枚举每个候选数选或不选、选几次,允许重复使用当前数后再推进到下一个候选数。
OJ: leetcodecn
题目 ID: combination-sum
难度:普及+/提高
标签:回溯枚举递归
日期: 2026-07-29 11:20
题意
给定无重复元素的整数数组 candidates 和目标整数 target,找出所有和为 target 的组合。同一个候选数可以无限重复选取。不同组合以至少一个数字的选取数量不同来区分。
思路
最直接的思路是枚举每个候选数的使用次数,形成一条选择序列后检查总和是否等于 target:
cpp
// brute.cpp:小数据暴力解,递归枚举每个候选数使用 0、1、2……次。
#include <bits/stdc++.h>
using namespace std;
int n, target;
int c[15];
int cnt[15]; // cnt[i] 表示候选 c[i] 被选了几次
vector<vector<int>> ans;
void dfs(int i, int sum) {
if (sum > target)
return;
if (i == n) {
if (sum == target) {
vector<int> cur;
for (int j = 0; j < n; j++)
for (int k = 0; k < cnt[j]; k++)
cur.push_back(c[j]);
ans.push_back(cur);
}
return;
}
// 枚举 c[i] 选 0, 1, 2, ... 次,直到超出 target
for (cnt[i] = 0; sum + cnt[i] * c[i] <= target; cnt[i]++) {
dfs(i + 1, sum + cnt[i] * c[i]);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> target;
for (int i = 0; i < n; i++)
cin >> c[i];
dfs(0, 0);
for (auto &v : ans) {
for (int x : v)
cout << x << ' ';
cout << '\n';
}
return 0;
}这个暴力用 cnt[i] 记录每个候选数的选取次数,递归到所有候选数决定完毕后再检查总和。它在小数据上可靠,但当候选数多或目标值大时分支爆炸。
优化的关键是:在递归过程中直接维护剩余值 left,若 left < 0 立即剪枝返回;同时用"选/不选"的二元分支结构——dfs(i+1, left) 表示跳过 candidates[i],dfs(i, left - candidates[i]) 表示使用一次 candidates[i] 且不推进下标(允许重复使用)。
用起始下标而非从 0 开始,保证组合内部有序,避免产生重复组合(如 [2,3] 和 [3,2])。排序候选数后还可以进一步剪枝:若 left < candidates[i],后续更大的候选数也无法命中,直接停止。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
vector<vector<int>> ans;
vector<int> cur;
function<void(int, int)> dfs = [&](int i, int left) {
if (left == 0) {
ans.push_back(cur);
return;
}
if (i == (int)candidates.size() || left < 0)
return;
dfs(i + 1, left);
cur.push_back(candidates[i]);
dfs(i, left - candidates[i]);
cur.pop_back();
};
dfs(0, target);
return ans;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, t;
cin >> n >> t;
vector<int> a(n);
for (int &x : a)
cin >> x;
for (auto &v : Solution().combinationSum(a, t)) {
for (int x : v)
cout << x << ' ';
cout << '\n';
}
return 0;
}python
#!/usr/bin/env python3
from typing import List
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
ans, cur = [], []
def dfs(i, left):
if left == 0:
ans.append(cur[:])
return
if i == len(candidates) or left < 0:
return
dfs(i + 1, left)
cur.append(candidates[i])
dfs(i, left - candidates[i])
cur.pop()
dfs(0, target)
return ans
def main():
n, t = map(int, input().split())
a = list(map(int, input().split()))
for v in Solution().combinationSum(a, t):
print(*v)
if __name__ == "__main__":
main()复杂度
- 时间复杂度:取决于方案数,最坏情况所有组合都合法时为
,实际远小于此。 - 空间复杂度:
,递归栈深度最多等于候选数个数, cur长度最多等于组合长度。
总结
组合枚举题的关键是避免重复组合。用起始下标 i 控制"只往后选",同一候选数可以重复使用时不推进下标(dfs(i, ...)),不再使用时推进(dfs(i+1, ...))。剪枝条件 left < 0 和排序后的 left < candidates[i] 能显著减少无效递归。