1 条题解

  • 0
    @ 2025-12-11 21:59:16

    2680. 「BalticOI 2010」糖果 candies 题解

    问题分析

    我们有 NN 个包装,每个包装有 BiB_i 颗糖果。我们可以选择任意一些包装给顾客,糖果总数就是这些包装的和。

    目标

    1. 初始状态:可以构成多少种不同的糖果总数(不同选项数)
    2. 可以修改一个包装的糖果数(从 BiB_i 改为某个值 QQ
    3. 要求最大化可以构成的不同糖果总数
    4. 输出方案:要修改哪个包装(PP),改成什么值(QQ
      • 如果有多个最优解,输出 PP 最小的
      • 如果 PP 相同,输出 QQ 最小的

    关键观察

    1. 问题转化

    这是一个子集和问题:给定 NN 个数,能组成哪些不同的和?

    初始的不同选项数 = 能组成的不同正整数的个数(因为不能给 0 个糖果)。

    2. 修改策略

    我们只能修改一个数,将其值 PP 改为 QQ

    对于每个可能的 PP(即每个 BiB_i),我们枚举所有可能的 QQ(从 1 到某个上限),计算修改后的不同选项数。

    3. 计算不同选项数

    使用动态规划 / 背包

    • 初始:dp[s] = true 表示能组成和 ss
    • 修改一个数 PQP→Q:相当于从集合中删除 PP,加入 QQ

    算法思路

    1. 初始状态计算

    bitset 优化:

    bitset<MAX_SUM> dp;
    dp[0] = 1;  // 可以组成0
    for (int i = 0; i < N; i++) {
        dp |= dp << B[i];
    }
    // 能组成的不同正整数的个数 = dp中1的个数 - 1(去掉0)
    int initial_cnt = dp.count() - 1;
    

    2. 修改一个数的计算

    对于每个 PP(每个包装值):

    1. 先去掉 PP:相当于反向操作
    2. 然后加入 QQ:枚举可能的 QQ

    关键优化:不需要对每个 QQ 都重新计算整个背包。

    我们可以先预处理:

    • dp_without_P:去掉 PP 后能组成的和
    • 然后对于每个 QQ,在 dp_without_P 基础上加入 QQ

    3. 计算 dp_without_P

    使用逆背包思想:

    • 正常背包:dp_new = dp_old | (dp_old << x)
    • 逆背包:dp_without = dp_all & ~(dp_without << x) >> x(需要仔细推导)

    更简单的方法:对于每个 PP,重新计算去掉 PP 的背包,复杂度 O(NMAXSUM)O(N \cdot MAX_SUM),对于 N=100,MAXSUM=7000100=7×105N=100, MAX_SUM=7000*100=7\times10^5 可能较大。

    但我们可以优化:由于 NN 不大(100),可以接受 O(N2MAXSUM/N)O(N^2 \cdot MAX_SUM/N) 的复杂度。

    详细算法

    方法1:暴力枚举(可行)

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAX_SUM = 7000 * 100 + 10;  // 最大可能和
    
    int main() {
        int N;
        cin >> N;
        vector<int> B(N);
        for (int i = 0; i < N; i++) {
            cin >> B[i];
        }
        
        // 初始背包
        bitset<MAX_SUM> dp_init;
        dp_init[0] = 1;
        for (int x : B) {
            dp_init |= dp_init << x;
        }
        int initial_cnt = dp_init.count() - 1;
        
        // 尝试修改每个数
        int best_P = 0, best_Q = 0, best_cnt = initial_cnt;
        
        // 枚举要修改哪个包装
        for (int idx = 0; idx < N; idx++) {
            int P = B[idx];
            
            // 计算去掉P后的背包
            bitset<MAX_SUM> dp_without;
            dp_without[0] = 1;
            for (int i = 0; i < N; i++) {
                if (i != idx) {
                    dp_without |= dp_without << B[i];
                }
            }
            
            // 枚举新的值Q
            // Q的范围:1到某个上限,比如最大和+1
            int max_Q = MAX_SUM - 1;
            for (int Q = 1; Q <= max_Q; Q++) {
                if (Q == P) continue;  // 修改为相同值无意义
                
                // 在dp_without基础上加入Q
                bitset<MAX_SUM> dp_new = dp_without | (dp_without << Q);
                int cnt = dp_new.count() - 1;
                
                if (cnt > best_cnt || (cnt == best_cnt && (P < best_P || (P == best_P && Q < best_Q)))) {
                    best_cnt = cnt;
                    best_P = P;
                    best_Q = Q;
                }
            }
        }
        
        cout << best_P << " " << best_Q << endl;
        return 0;
    }
    

    问题:复杂度太高

    • 外层:枚举 NNPP(100)
    • 内层:枚举 QQ(最多 7×1057\times10^5
    • 计算每个 QQ 的背包:O(MAXSUM/word)O(MAX_SUM/word)7×105/641047\times10^5/64 ≈ 10^4
    • 总复杂度:$100 \times 7\times10^5 \times 10^4 ≈ 7\times10^{11}$ 太大

    优化方法

    1. 减少 QQ 的枚举范围

    QQ 不需要枚举到 MAXSUMMAX_SUM,只需枚举到当前最大和 + 最大 BiB_i 即可。

    2. 预处理去掉每个 PP 的背包

    我们可以先计算完整的背包 dp_all,然后对于每个 PP,用逆背包快速计算 dp_without_P

    逆背包公式: 如果 dp_all 是包含所有数的背包,那么去掉 xx 后的背包可以通过:

    dp_without = dp_all;
    for (int s = x; s <= max_sum; s++) {
        if (dp_without[s] && dp_without[s - x]) {
            dp_without[s] = 0;
        }
    }
    

    但这样可能会错误地去掉一些组合。

    更可靠的方法:使用生成函数思想,但实现复杂。

    3. 更好的方法:重新思考问题

    我们其实不需要枚举所有 QQ!可以观察到:

    • 修改一个数 PQP→Q 后,新的能组成的集合 = 旧集合 ∪ (旧集合 + (Q-P))
    • 因为只是把 PP 换成了 QQ,相当于所有包含原来 PP 的组合,现在 PP 变成 QQ,所以和增加了 QPQ-P

    SS 是原集合(能组成的和),SPS_P 是包含 PP 的和组成的集合,那么: 新集合 = S(SP+(QP))S ∪ (S_P + (Q-P))

    其中 SPS_P 可以通过:SP=S(S>>P)S_P = S ∩ (S >> P)(需要处理边界)

    最终算法

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAX_SUM = 7000 * 100 + 10;
    
    int main() {
        int N;
        cin >> N;
        vector<int> B(N);
        for (int i = 0; i < N; i++) {
            cin >> B[i];
        }
        
        // 初始背包
        bitset<MAX_SUM> dp;
        dp[0] = 1;
        for (int x : B) {
            dp |= dp << x;
        }
        
        // 计算包含每个数P的集合
        vector<bitset<MAX_SUM>> contain_P(N);
        for (int i = 0; i < N; i++) {
            int P = B[i];
            // 计算不包含P的背包
            bitset<MAX_SUM> dp_without;
            dp_without[0] = 1;
            for (int j = 0; j < N; j++) {
                if (j != i) {
                    dp_without |= dp_without << B[j];
                }
            }
            // 包含P的集合 = 全集 - 不包含P的集合
            contain_P[i] = dp ^ dp_without;
        }
        
        // 初始计数
        int best_cnt = dp.count() - 1;
        int best_P = 0, best_Q = 0;
        
        // 枚举每个P和可能的Q
        for (int i = 0; i < N; i++) {
            int P = B[i];
            
            // Q的范围:我们只需要考虑能增加新和的Q
            // 新的和 = 旧和 ∪ (包含P的集合 + d),其中d=Q-P
            
            // 枚举d的变化范围
            int max_d = 20000;  // 合理范围,因为最大和约70000
            for (int d = -P+1; d <= max_d; d++) {
                int Q = P + d;
                if (Q <= 0) continue;
                
                // 计算新集合
                bitset<MAX_SUM> new_dp = dp;
                
                // 将包含P的集合平移d
                if (d > 0) {
                    new_dp |= contain_P[i] << d;
                } else {
                    new_dp |= contain_P[i] >> (-d);
                }
                
                int cnt = new_dp.count() - 1;
                if (cnt > best_cnt || (cnt == best_cnt && (P < best_P || (P == best_P && Q < best_Q)))) {
                    best_cnt = cnt;
                    best_P = P;
                    best_Q = Q;
                }
            }
        }
        
        cout << best_P << " " << best_Q << endl;
        return 0;
    }
    

    复杂度分析

    预处理

    • 计算初始背包:O(NMAXSUM/word)O(N \cdot MAX_SUM / word)
    • 计算每个 contain_P[i]O(N2MAXSUM/word)O(N^2 \cdot MAX_SUM / word) 对于 N=100,MAXSUM=70000N=100, MAX_SUM=70000,大约 1002×70000/64107100^2 \times 70000/64 ≈ 10^7 次操作,可行。

    枚举

    • 外层:NN 次(100)
    • 内层:枚举 dd,假设范围 20000
    • 每次操作:bitset 移位和或运算,O(MAXSUM/word)70000/641000O(MAX_SUM/word) ≈ 70000/64≈1000
    • 总复杂度:100×20000×10002×109100 \times 20000 \times 1000 ≈ 2\times10^9 稍大,但 bitset 操作很快,可能能过

    进一步优化

    1. 减少 dd 的枚举范围:实际上 dd 不需要太大,因为和超出最大范围无意义
    2. 只枚举有意义的 dd:观察哪些 dd 能产生新和

    更优的 dd 枚举方法

    我们不需要枚举所有 dd,只需要枚举那些能让 contain_P[i] 产生新和的 dd

    对于每个缺失的和 tt(即 dp[t]=0dp[t]=0t>0t>0),我们需要检查是否存在某个 ss 使得:

    • sscontain_P[i] 中(即 ss 包含 PP
    • s+d=ts + d = t,其中 d=QPd = Q - P

    所以 d=tsd = t - s

    因此,对于每个缺失的和 tt,我们检查是否存在 s \in \text{contain_P[i]} 使得 tst-s 是合法的 dd

    枚举所有缺失的和 tt(最多 MAXSUMMAX_SUM 个),对于每个 tt,检查是否存在 ss

    // 更高效的枚举d
    bitset<MAX_SUM> missing = ~dp;  // 不能组成的和
    missing[0] = 0;  // 0不算
    
    // 对于每个P
    for (int i = 0; i < N; i++) {
        int P = B[i];
        
        // 收集所有可能的d
        unordered_set<int> candidate_d;
        
        // 对于每个缺失的和t
        for (int t = 1; t < MAX_SUM; t++) {
            if (!missing[t]) continue;
            
            // 我们需要检查是否存在s使得s在contain_P[i]中且t-s是合理的d
            // 更高效:遍历contain_P[i]中的s
            // 我们可以用bitset快速检查
            // 但这里简化:只检查几个值
            
            // 限制范围:s应该在[P, max_sum]之间
            // 实际上,contain_P[i]中的最小和是P
            int min_s = P;
            int max_s = min(t + P, MAX_SUM-1);  // 因为d不能太大
            
            for (int s = min_s; s <= max_s; s++) {
                if (contain_P[i][s]) {
                    int d = t - s;
                    if (d != 0) {  // d=0无意义
                        candidate_d.insert(d);
                    }
                }
            }
        }
        
        // 枚举候选的d
        for (int d : candidate_d) {
            int Q = P + d;
            if (Q <= 0) continue;
            
            bitset<MAX_SUM> new_dp = dp;
            if (d > 0) {
                new_dp |= contain_P[i] << d;
            } else {
                new_dp |= contain_P[i] >> (-d);
            }
            
            int cnt = new_dp.count() - 1;
            if (cnt > best_cnt || (cnt == best_cnt && (P < best_P || (P == best_P && Q < best_Q)))) {
                best_cnt = cnt;
                best_P = P;
                best_Q = Q;
            }
        }
    }
    

    总结

    本题的关键:

    1. 使用 bitset 高效处理子集和问题
    2. 理解修改一个数的影响:新集合 = 旧集合 ∪ (包含该数的集合平移 dd)
    3. 通过预处理 contain_P[i] 加速计算
    4. 合理枚举 dd 值,而不是枚举所有 QQ
    • 1

    信息

    ID
    6155
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者