1 条题解
-
0
同学你好!作为 NOI 教练,我将带你通过“渐进式”的方法解决这道题。处理含有重复元素的组合问题,其核心在于 “排序+树层剪枝”。
版本一:基础回溯 +
set暴力去重这是初学者最容易想到的思路:先找出所有子集,和为 的记录下来。为了去重,将每个组合排序后放入
std::set<vector<int>>。思考过程:
- 时间复杂度:。产生所有子集需 ,每个子集排序并插入 set 的开销极大。
- 空间复杂度:。在 的情况下,极易 MLE(内存超限)。
#include <iostream> #include <vector> #include <algorithm> #include <set> using namespace std; int n, target; int a[105]; vector<int> path; set<vector<int>> res; // 使用 set 暴力去重 void dfs(int start, int sum) { if (sum == target) { vector<int> tmp = path; sort(tmp.begin(), tmp.end()); // 排序以保证 set 能正确去重 res.insert(tmp); return; } if (sum > target || start == n) return; for (int i = start; i < n; ++i) { path.push_back(a[i]); dfs(i + 1, sum + a[i]); // 每个数用一次,传 i + 1 path.pop_back(); // 回溯 } } int main() { ios::sync_with_stdio(false); cin.tie(0); // NOI 优化 if (!(cin >> n >> target)) return 0; for (int i = 0; i < n; i++) cin >> a[i]; dfs(0, 0); for (auto const& vec : res) { for (int i = 0; i < vec.size(); i++) cout << vec[i] << (i == vec.size() - 1 ? "" : " "); cout << "\n"; } return 0; }
版本二:最优复杂度——排序 + 树层剪枝 (NOI 推荐)
这是竞赛中的标准算法。我们在搜索过程中就通过逻辑切断重复分支,不再使用
set。思考过程:
- 排序:必须先排序,让相同的数相邻。
- 树层去重:在当前
for循环里,如果a[i] == a[i-1]且i > start,说明“以 为开头”的所有组合在刚才已经被处理过了,直接跳过。 - 可行性剪枝:如果 已经大于剩余目标值,由于后面更重,直接
break。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int n, target; int a[105]; vector<int> path; // remain: 还需要凑多少 // start: 从哪个索引开始选 void dfs(int remain, int start) { // 递归边界:目标达成 if (remain == 0) { for (int i = 0; i < path.size(); ++i) { cout << path[i] << (i == path.size() - 1 ? "" : " "); } cout << "\n"; // NOI 竞赛建议使用 "\n" 替代 endl return; } for (int i = start; i < n; ++i) { // 【关键剪枝 1:可行性剪枝】 // 如果当前数已经大于剩余目标,由于数组已排好序,后面的数肯定也太大 if (remain - a[i] < 0) break; // 【关键剪枝 2:树层去重】重点! // i > start 说明在当前这个坑位(同一层递归),我们已经尝试过前面的元素了 // 如果当前数 a[i] == a[i-1],选 a[i] 产生的解必然是选 a[i-1] 产生解的子集 if (i > start && a[i] == a[i - 1]) continue; path.push_back(a[i]); // i + 1 保证每个元素只用一次 dfs(remain - a[i], i + 1); path.pop_back(); // 回溯:清理现场,尝试本层下一个 i } } int main() { // 提高 I/O 效率 ios::sync_with_stdio(false); cin.tie(nullptr); if (!(cin >> n >> target)) return 0; for (int i = 0; i < n; i++) cin >> a[i]; // 1. 必须排序,这是去重和剪枝的基础 sort(a, a + n); // 2. 开始回溯搜索 dfs(target, 0); return 0; }
七、 复杂度分析与优化建议
1. 时间复杂度分析
- 计算量:最坏情况下是 。
- 实际表现:虽然 ,但因为目标和 极小()且数字都是正整数,搜索深度受到强烈约束(最多 30 层)。
- 剪枝效果:树层去重和
break剪枝将搜索空间压缩了 90% 以上。在 NOI 环境下,此算法通常在 10ms 内即可出解。
2. 空间复杂度分析
- 栈空间:递归深度 。
- 辅助空间: 存储路径。
- 注意:边搜边输出不仅节省内存,还能避免结果集过大导致的 MLE。
3. 优化建议
- I/O 优化:如果输出行数很多(如 范围变大),建议使用
printf或手写Fast I/O。 - 位运算:如果本题只要求输出“是否能凑成”,可以使用
std::bitset<target_max>进行动态规划,复杂度仅为 。但本题要求输出所有路径,DFS 是唯一选择。
八、 读题理解关键词总结
做 NOI 题目时,看到以下描述要立刻反应:
- “重复数字”
sort(a, a + n)是标配。 - “每个数字只能用一次”
dfs递归参数传i + 1。 - “不能包含重复的组合” 搜索树中加入
i > start && a[i] == a[i-1]。 - “正整数集合” 可以使用
if (remain < a[i]) break;快速剪枝。
教练点评:版本二中的
i > start是区分普通选手和竞赛选手的“金准则”。它能确保在搜索树的横向维度上去重,而不影响纵向维度的正常选取。加油!
- 1
信息
- ID
- 19435
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 3
- 已通过
- 1
- 上传者