1 条题解

  • 0
    @ 2026-1-5 17:50:46

    同学你好!作为 NOI 教练,我将带你通过“渐进式”的方法解决这道题。处理含有重复元素的组合问题,其核心在于 “排序+树层剪枝”


    版本一:基础回溯 + set 暴力去重

    这是初学者最容易想到的思路:先找出所有子集,和为 TT 的记录下来。为了去重,将每个组合排序后放入 std::set<vector<int>>

    思考过程

    • 时间复杂度O(2nnlogn)O(2^n \cdot n \log n)。产生所有子集需 O(2n)O(2^n),每个子集排序并插入 set 的开销极大。
    • 空间复杂度O(结果数n)O(\text{结果数} \cdot n)。在 n=100n=100 的情况下,极易 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

    思考过程

    1. 排序:必须先排序,让相同的数相邻。
    2. 树层去重:在当前 for 循环里,如果 a[i] == a[i-1]i > start,说明“以 a[i]a[i] 为开头”的所有组合在刚才已经被处理过了,直接跳过。
    3. 可行性剪枝:如果 a[i]a[i] 已经大于剩余目标值,由于后面更重,直接 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. 时间复杂度分析

    • 计算量:最坏情况下是 O(2n)O(2^n)
    • 实际表现:虽然 n=100n=100,但因为目标和 TT 极小(T=30T=30)且数字都是正整数,搜索深度受到强烈约束(最多 30 层)。
    • 剪枝效果:树层去重和 break 剪枝将搜索空间压缩了 90% 以上。在 NOI 环境下,此算法通常在 10ms 内即可出解。

    2. 空间复杂度分析

    • 栈空间:递归深度 O(n)O(n)
    • 辅助空间O(n)O(n) 存储路径。
    • 注意:边搜边输出不仅节省内存,还能避免结果集过大导致的 MLE。

    3. 优化建议

    1. I/O 优化:如果输出行数很多(如 TT 范围变大),建议使用 printf 或手写 Fast I/O
    2. 位运算:如果本题只要求输出“是否能凑成”,可以使用 std::bitset<target_max> 进行动态规划,复杂度仅为 O(ntarget/64)O(n \cdot target / 64)。但本题要求输出所有路径,DFS 是唯一选择。

    八、 读题理解关键词总结

    做 NOI 题目时,看到以下描述要立刻反应:

    • “重复数字” \rightarrow sort(a, a + n) 是标配。
    • “每个数字只能用一次” \rightarrow dfs 递归参数传 i + 1
    • “不能包含重复的组合” \rightarrow 搜索树中加入 i > start && a[i] == a[i-1]
    • “正整数集合” \rightarrow 可以使用 if (remain < a[i]) break; 快速剪枝。

    教练点评:版本二中的 i > start 是区分普通选手和竞赛选手的“金准则”。它能确保在搜索树的横向维度上去重,而不影响纵向维度的正常选取。加油!

    • 1

    信息

    ID
    19435
    时间
    1000ms
    内存
    128MiB
    难度
    10
    标签
    (无)
    递交数
    3
    已通过
    1
    上传者