1 条题解
-
0
2680. 「BalticOI 2010」糖果 candies 题解
问题分析
我们有 个包装,每个包装有 颗糖果。我们可以选择任意一些包装给顾客,糖果总数就是这些包装的和。
目标:
- 初始状态:可以构成多少种不同的糖果总数(不同选项数)
- 可以修改一个包装的糖果数(从 改为某个值 )
- 要求最大化可以构成的不同糖果总数
- 输出方案:要修改哪个包装(),改成什么值()
- 如果有多个最优解,输出 最小的
- 如果 相同,输出 最小的
关键观察
1. 问题转化
这是一个子集和问题:给定 个数,能组成哪些不同的和?
初始的不同选项数 = 能组成的不同正整数的个数(因为不能给 0 个糖果)。
2. 修改策略
我们只能修改一个数,将其值 改为 。
对于每个可能的 (即每个 ),我们枚举所有可能的 (从 1 到某个上限),计算修改后的不同选项数。
3. 计算不同选项数
使用动态规划 / 背包:
- 初始:
dp[s] = true表示能组成和 - 修改一个数 :相当于从集合中删除 ,加入
算法思路
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. 修改一个数的计算
对于每个 (每个包装值):
- 先去掉 :相当于反向操作
- 然后加入 :枚举可能的
关键优化:不需要对每个 都重新计算整个背包。
我们可以先预处理:
dp_without_P:去掉 后能组成的和- 然后对于每个 ,在
dp_without_P基础上加入
3. 计算
dp_without_P使用逆背包思想:
- 正常背包:
dp_new = dp_old | (dp_old << x) - 逆背包:
dp_without = dp_all & ~(dp_without << x) >> x(需要仔细推导)
更简单的方法:对于每个 ,重新计算去掉 的背包,复杂度 ,对于 可能较大。
但我们可以优化:由于 不大(100),可以接受 的复杂度。
详细算法
方法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; }问题:复杂度太高
- 外层:枚举 个 (100)
- 内层:枚举 (最多 )
- 计算每个 的背包: ≈
- 总复杂度:$100 \times 7\times10^5 \times 10^4 ≈ 7\times10^{11}$ 太大
优化方法
1. 减少 的枚举范围
不需要枚举到 ,只需枚举到当前最大和 + 最大 即可。
2. 预处理去掉每个 的背包
我们可以先计算完整的背包
dp_all,然后对于每个 ,用逆背包快速计算dp_without_P。逆背包公式: 如果
dp_all是包含所有数的背包,那么去掉 后的背包可以通过: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. 更好的方法:重新思考问题
我们其实不需要枚举所有 !可以观察到:
- 修改一个数 后,新的能组成的集合 = 旧集合 ∪ (旧集合 + (Q-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; }复杂度分析
预处理
- 计算初始背包:
- 计算每个
contain_P[i]: 对于 ,大约 次操作,可行。
枚举
- 外层: 次(100)
- 内层:枚举 ,假设范围 20000
- 每次操作:bitset 移位和或运算,
- 总复杂度: 稍大,但 bitset 操作很快,可能能过
进一步优化
- 减少 的枚举范围:实际上 不需要太大,因为和超出最大范围无意义
- 只枚举有意义的 :观察哪些 能产生新和
更优的 枚举方法
我们不需要枚举所有 ,只需要枚举那些能让
contain_P[i]产生新和的 。对于每个缺失的和 (即 但 ),我们需要检查是否存在某个 使得:
- 在
contain_P[i]中(即 包含 ) - ,其中
所以 。
因此,对于每个缺失的和 ,我们检查是否存在 s \in \text{contain_P[i]} 使得 是合法的 。
枚举所有缺失的和 (最多 个),对于每个 ,检查是否存在 。
// 更高效的枚举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; } } }总结
本题的关键:
- 使用
bitset高效处理子集和问题 - 理解修改一个数的影响:新集合 = 旧集合 ∪ (包含该数的集合平移 )
- 通过预处理
contain_P[i]加速计算 - 合理枚举 值,而不是枚举所有
- 1
信息
- ID
- 6155
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者