1. 题目背景与需求分析
小杨的智慧购物是一道典型的算法优化题,主要考察考生对贪心算法和动态规划的理解应用能力。题目设定在一个购物场景中,小杨需要从N件商品中选择若干件放入容量为V的购物车,每件商品有对应的价值(满意度)和体积(占用空间),目标是在不超过购物车容量的前提下,使所选商品的总价值最大化。
这道题本质上是经典的0-1背包问题变种,但结合了实际购物场景,增加了问题的趣味性和现实意义。从GESP三级考试大纲来看,该题完美覆盖了"运用基本算法解决实际问题"的考核要求,特别是对条件判断、循环控制和数据结构基础的综合运用。
注意:虽然题目表面是购物场景,但解题时需要抽象为数学模型。建议先建立问题形式化描述:给定n个物品的重量w[i]和价值v[i],以及背包容量V,求最大价值总和。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解题思路与算法选择
2.1 暴力搜索法的可行性分析
最直观的解法是枚举所有可能的商品组合(每个商品选或不选),计算每种组合的总重量和总价值,然后筛选出满足重量约束的最大价值解。这种方法的时间复杂度为O(2^n),当n=30时,组合数将达到10亿量级,显然无法在考试时间限制内完成。
cpp复制// 暴力搜索伪代码示例(仅用于理解问题,实际考试不可行)
int maxValue = 0;
void dfs(int index, int currentWeight, int currentValue) {
if(index == n) {
if(currentWeight <= V && currentValue > maxValue)
maxValue = currentValue;
return;
}
dfs(index+1, currentWeight, currentValue); // 不选当前物品
dfs(index+1, currentWeight + w[index], currentValue + v[index]); // 选当前物品
}
2.2 动态规划的标准解法
对于背包问题,动态规划(DP)是最优解法。我们定义dp[i][j]表示考虑前i件商品,背包容量为j时能获得的最大价值。状态转移方程为:
code复制dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) // j >= w[i]
= dp[i-1][j] // j < w[i]
这个二维DP解法的时间复杂度为O(nV),空间复杂度也是O(nV)。在GESP三级考试范围内,n和V通常会被限制在合理范围内(如n≤100,V≤1000),这种解法完全可行。
2.3 空间优化技巧
可以进一步优化空间复杂度到O(V),使用一维数组并逆序更新:
cpp复制vector<int> dp(V+1, 0);
for(int i=1; i<=n; ++i) {
for(int j=V; j>=w[i]; --j) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
这种优化在考试中尤其重要,因为减少了内存使用和代码复杂度。需要注意的是内层循环必须逆序,否则会变成完全背包问题的解法。
3. 完整代码实现与逐行解析
3.1 输入处理与初始化
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n, V;
cin >> n >> V; // 商品数量和购物车容量
vector<int> w(n+1), v(n+1); // 重量和价值数组,从1开始索引
for(int i=1; i<=n; ++i) {
cin >> w[i] >> v[i];
}
这里使用n+1大小的数组是为了让商品编号从1开始,与日常思维一致。输入时需要注意题目给出的数据格式,可能是每行一个商品的重量和价值,也可能是其他形式。
3.2 DP数组定义与状态转移
cpp复制 vector<int> dp(V+1, 0); // 初始化DP数组
for(int i=1; i<=n; ++i) {
for(int j=V; j>=w[i]; --j) {
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
}
}
关键点说明:
- dp数组初始化为0,表示容量为j时能获得的最大价值
- 外层循环遍历所有商品
- 内层循环逆序遍历容量,确保每个商品只被考虑一次
- 状态转移方程直接实现了"选或不选"的逻辑
3.3 输出结果与边界处理
cpp复制 cout << dp[V] << endl;
return 0;
}
最终dp[V]就是问题的解。需要注意几个边界情况:
- 当所有商品重量总和≤V时,直接全选
- 当最小商品重量>V时,结果为0
- 当V=0时,结果必然为0
4. 常见错误与调试技巧
4.1 初始化错误
常见错误是忘记初始化DP数组或错误初始化。例如:
cpp复制// 错误示例1:未初始化
int dp[V+1];
// 错误示例2:错误初始值
vector<int> dp(V+1, -1); // 应该初始化为0
4.2 循环顺序错误
cpp复制// 错误示例:正序更新导致完全背包问题
for(int j=w[i]; j<=V; ++j) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
这种写法会让商品被重复选择,变成完全背包问题的解法,与题目要求的0-1背包不符。
4.3 数组越界问题
当j < w[i]时,如果内层循环没有正确处理下限,会导致数组越界:
cpp复制// 危险写法:可能访问dp[负数]
for(int j=V; j>=0; --j) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
4.4 调试建议
- 使用小规模测试数据手工计算验证
- 打印中间DP表检查状态转移是否正确
- 特别注意循环边界条件
- 对比暴力解法结果(小数据时)
5. 算法优化与扩展思考
5.1 剪枝优化
当剩余容量不足以放入任何剩余商品时,可以提前终止搜索。这在DFS解法中特别有效:
cpp复制// 在DFS中加入剪枝条件
if(currentWeight + sumRemain[i] <= V) {
maxValue = max(maxValue, currentValue + sumValue[i]);
return;
}
5.2 恰好装满问题
如果题目要求"恰好装满购物车"时的最大价值,只需修改初始化:
cpp复制vector<int> dp(V+1, -INF); // 初始为负无穷
dp[0] = 0; // 容量为0时价值为0
5.3 多维度约束扩展
实际问题可能有多个约束条件(如重量+体积),这时DP状态需要增加维度:
cpp复制// 二维约束的背包问题
vector<vector<int>> dp(V1+1, vector<int>(V2+1, 0));
for(int i=1; i<=n; ++i) {
for(int j1=V1; j1>=w1[i]; --j1) {
for(int j2=V2; j2>=w2[i]; --j2) {
dp[j1][j2] = max(dp[j1][j2],
dp[j1-w1[i]][j2-w2[i]] + v[i]);
}
}
}
5.4 输出具体方案
如果需要输出选择了哪些商品,可以增加路径记录:
cpp复制vector<vector<bool>> choice(n+1, vector<bool>(V+1, false));
// 更新DP时记录选择
if(dp[j - w[i]] + v[i] > dp[j]) {
dp[j] = dp[j - w[i]] + v[i];
choice[i][j] = true;
}
// 反向追踪方案
int j = V;
for(int i=n; i>=1; --i) {
if(choice[i][j]) {
cout << i << " ";
j -= w[i];
}
}
6. 实际应用与变种题目
背包问题在现实中有广泛应用:
- 投资组合优化(有限资金下最大化收益)
- 资源分配(如广告投放预算分配)
- 行程规划(时间有限下最大化游览体验)
常见变种题目包括:
- 完全背包问题(商品可重复选择)
- 多重背包问题(商品有数量限制)
- 分组背包问题(商品分组,每组只能选一个)
- 依赖背包问题(选择某些商品必须先选其他商品)
在GESP考试中可能出现的简单变种:
- 求方案数而非最大价值
- 需要输出字典序最小的方案
- 商品有附加限制条件(如必须选至少k件)
7. 学习建议与备考策略
-
理解优先于记忆:彻底理解状态转移方程的含义,而非死记模板
-
从简单案例入手:先用n=3,V=5这样的小数据手工计算
-
对比学习:比较0-1背包、完全背包、多重背包的异同
-
多维度练习:
- 基础:标准背包问题
- 进阶:需要记录方案的问题
- 提高:多维约束或特殊条件的问题
-
调试技巧:
- 打印DP表验证中间结果
- 编写暴力解法作为对拍程序
- 使用assert检查数组边界
-
时间管理:
- 考试时先写核心DP部分
- 输入输出和边界处理可以后补
- 留出时间检查循环方向和初始化
对于GESP三级考生,建议至少完成10道不同变种的背包问题练习,并能够在不参考模板的情况下15分钟内完成标准0-1背包的代码实现。实际考试中,这道题应该在30分钟内完成,包括读题、编码和测试。
