今天我们来看CSP202209.B何以包邮?这道题目

解题思路
问题分析
本题是一个典型的0-1背包问题变形。我们需要在 n 本书中选择若干本,使得总价格不小于 x,并且尽可能接近 x。
算法选择
- 暴力枚举:对于 n≤30,直接枚举所有子集的时间复杂度为 O(2^n),会超时。
- 折半枚举:将书分成两半,分别枚举,时间复杂度 O(2^(n/2)),可以接受。
- 动态规划:使用 0-1 背包思想,时间复杂度 O(n×sum),其中 sum 为总价格。
由于每本书价格 ai≤10^4,n≤30,所以总价格 sum≤3×10^5,动态规划完全可行。
动态规划思路
状态定义:
- 设
dp[j]表示能否通过选择若干本书,使得总价格恰好为 j。
状态转移:
- 初始状态:
dp[0] = true,其余为false - 对于每本书
a[i],从大到小遍历 j,更新dp[j] = dp[j] || dp[j - a[i]]
答案查找:
- 从 x 开始向上遍历,找到第一个
dp[j] == true的 j,即为满足条件的最小花费
复杂度分析
- 时间复杂度:O(n × sum),其中 sum 为所有书价格总和
- 空间复杂度:O(sum)
C++ 代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, x;
cin >> n >> x;
vector<int> a(n);
int sum = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
sum += a[i];
}
// dp[j] 表示能否凑出总价格恰好为 j
vector<bool> dp(sum + 1, false);
dp[0] = true;
// 0-1背包:每本书只能选择一次
for (int i = 0; i < n; i++) {
// 从大到小遍历,避免重复选择同一本书
for (int j = sum; j >= a[i]; j--) {
if (dp[j - a[i]]) {
dp[j] = true;
}
}
}
// 从 x 开始向上找第一个能凑出的价格
for (int j = x; j <= sum; j++) {
if (dp[j]) {
cout << j << "\n";
break;
}
}
return 0;
}
代码详解
1. 输入处理
cpp
int n, x;
cin >> n >> x;
vector<int> a(n);
int sum = 0;
for (int i = 0; i < n; i++) {
cin >> a[i];
sum += a[i]; // 计算总价格,用于确定 dp 数组大小
}
2. 动态规划数组初始化
cpp
vector<bool> dp(sum + 1, false);
dp[0] = true; // 不选择任何书时,总价格为 0
3. 0-1背包核心代码
cpp
for (int i = 0; i < n; i++) {
for (int j = sum; j >= a[i]; j--) {
if (dp[j - a[i]]) {
dp[j] = true; // 如果 j-a[i] 可以凑出,那么 j 也可以凑出
}
}
}
注意:内层循环必须从大到小遍历,这样才能保证每本书最多被选择一次(0-1背包特性)。
4. 查找答案
cpp
for (int j = x; j <= sum; j++) {
if (dp[j]) {
cout << j << "\n";
break; // 找到第一个满足条件的就输出
}
}
示例运行
输入示例
5 15
5
7
3
8
2
运行过程
- 总价格 sum = 25
- dp 数组大小为 26
- 经过动态规划后,dp15 = true(可以选择 5+7+3 = 15)
- 从 15 开始查找,找到 dp15 = true
- 输出:15
注意事项
- 数组大小:dp 数组大小需要设为 sum+1,因为可能的最大价格是 sum
- 遍历顺序:内层循环必须从大到小,避免一本书被重复选择
- 边界条件:题目保证 sum≥x,所以一定能找到答案
- 优化:可以使用 bitset 优化空间和时间,但对于本题数据范围,bool 数组已经足够
总结
本题是经典的 0-1 背包问题变体,通过动态规划可以高效求解。关键点在于:
- 正确理解 0-1 背包的状态转移
- 掌握从大到小遍历的技巧
- 学会从 x 开始查找满足条件的最小值
这种方法时间复杂度为 O(n×sum),在给定数据范围内可以高效运行。