核心是吃透DP中的重要分支——背包问题。
昨天发的那个版本排版不行,所以今天重新发一个,然后把01背包题解配上了
下午 (4小时): 动态规划专题——背包问题
背包问题是动态规划中的一个庞大家族,它通过“选择”与“不选择”的决策来求解在有限“容量”下的最优解。掌握背包模型,不仅能解决模板题,更能帮助您识别出许多实际问题背后的DP本质。
练习计划概览
-
总时长: 约 4 小时
-
核心目标:
-
掌握 0-1背包 的状态定义、状态转移方程以及空间优化(滚动数组)的实现。
-
掌握 完全背包 的模型,并深刻理解其与0-1背包在空间优化实现上的核心差异(循环顺序)。
-
学习识别背包问题的变种,将实际问题抽象为背包模型。
-
通过CCF真题,感受背包问题在实际竞赛中的应用。
-
第一部分:0-1 背包 —— 每种物品仅此一件 (约 1.5 小时)
这是最基础的背包模型:有N件物品和一个容量为V的背包,每件物品有自己的重量(或成本)和价值,每件物品最多只能选择一次。求解将哪些物品装入背包,可使这些物品的价值总和最大。
| 题目编号 | 题目名称 | 核心知识点 | 练习目标 |
|---|---|---|---|
| Luogu P1048 | [NOIP2005 普及组] 采药 | 0-1背包模板 | DP入门必做。这是最纯粹的0-1背包问题。请务必亲手实现二维 dp[i][j] 版本以理解原理,然后掌握一维 dp[j] 的空间优化写法,并理解为何容量 j 必须**从大到小(逆序)**遍历。 |
| CCF202209-2 | 何以包邮? | 0-1背包变种 | 将0-1背包思想应用于不同的求解目标。问题是在满足总价不低于x 的前提下求最小花费。可以定义 dp[j] 为“能否凑成总价为 j”,也可以定义为“凑成总价 j 的最小件数/最大价值”,是一种典型的变种练习。(来自您提供的文件) 1 |
题解
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main(){
int time,n;
cin >> time >> n;
vector<pair<int,int>> v(n);
vector<vector<int>> dp(n,vector<int>(time+1,0));
for(int i=0;i<n;i++){
cin >> v[i].first >> v[i].second;
}
// 初始化第一行:只考虑第一个物品
for(int j=0;j<=time;j++){
if(j>=v[0].first){
dp[0][j] = v[0].second;
}
}
// 填充dp表,从第二个物品开始
for(int i=1;i<n;i++){
for(int j=0;j<=time;j++){
if(j>=v[i].first){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-v[i].first]+v[i].second);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
cout << dp[n-1][time] << endl;
return 0;
}
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
int main(){
int n, m;
cin >> n >> m;
// 存储每本书的价格
vector<int> price(n);
int totalSum = 0;
for(int i = 0; i < n; i++){
cin >> price[i];
totalSum += price[i];
}
// 如果所有书的总价都小于m,无法满足包邮条件
if(totalSum < m){
cout << 0 << endl;
return 0;
}
// dp[i][j] 表示前i本书能否组成价格为j的组合
// 由于价格可能很大,我们需要限制dp数组的大小
// 最大价格不会超过所有书价格的总和
vector<vector<bool>> dp(n + 1, vector<bool>(totalSum + 1, false));
// 初始化:不选任何书,价格为0
dp[0][0] = true;
// 动态规划
for(int i = 1; i <= n; i++){
for(int j = 0; j <= totalSum; j++){
// 不选第i本书
dp[i][j] = dp[i-1][j];
// 选第i本书(如果价格允许)
if(j >= price[i-1]){
dp[i][j] = dp[i][j] || dp[i-1][j - price[i-1]];
}
}
}
// 找到满足条件(>=m)的最小价格
int minCost = INT_MAX;
for(int j = m; j <= totalSum; j++){
if(dp[n][j]){
minCost = j;
break;
}
}
if(minCost == INT_MAX){
cout << 0 << endl;
} else {
cout << minCost << endl;
}
return 0;
}
练习建议:
-
理解核心状态:
dp[j]的含义是“在容量限制为j的情况下,所能获得的最大价值”。 -
理解逆序遍历:在一维优化中,对容量
j的逆序遍历是为了保证在计算dp[j]时,所依赖的dp[j - weight[i]]还是上一轮(即不包含物品i)的状态。这样就保证了每个物品i最多只被放入一次。
第二部分:完全背包 —— 物品无限供应 (约 2 小时)
完全背包与0-1背包唯一的区别在于:每种物品可以被选择无限次。这个小小的改动,在代码实现上带来了奇妙的变化。
| 题目编号 | 题目名称 | 核心知识点 | 练习目标 |
|---|---|---|---|
| Luogu P1616 | 疯狂的采药 | 完全背包模板 | 完全背包的模板题。请在一维空间优化的实现中,对比它与0-1背包的代码。你会发现,只需将容量 j 的遍历顺序改为**从小到大(正序)**即可。思考并理解为什么这样做是正确的。 |
| CCF202406-4 | 货物调度 | 分组背包, 0-1背包 | 这是一个非常好的进阶题。问题可以转化为分组背包模型:每个仓库是一个“物品组”,组内有多个“物品”(即选择从该仓库卖1件、2件、3件…货物)。解决这个问题需要您对背包DP有更深层次的理解。 |
练习建议:
-
理解正序遍历:在完全背包的一维优化中,对容量
j的正序遍历,使得我们在计算dp[j]时,所依赖的dp[j - weight[i]]可能已经是本轮(即已考虑物品i)更新过的状态。这相当于允许物品i被重复选择,从而正确地实现了完全背包的逻辑。 -
化归思想:在做
货物调度时,尝试将问题分解。对于每个仓库,先预处理出“从这个仓库卖出k件商品能获得的最大净利润”,然后问题就转化为了“有N个仓库(物品组),每个仓库有多种选择方案,求最大总利润”的分组背包问题。
目标达成自查 (约 0.5 小时)
完成以上练习后,进行复盘和总结:
-
0-1背包 vs 完全背包:
-
两种背包问题的核心区别是什么?(物品能否重复选取)。
-
这个区别如何体现在一维空间优化的代码上?(0-1背包容量逆序遍历,完全背包容量正序遍历)。
-
-
如何识别背包问题?
-
问题是否可以描述为:从一堆“物品”中进行选择,放入一个有“容量”限制的“背包”里,以期达到“价值”最优?
-
“重量/成本”、“价值”、“容量”这些概念在题目中可能是以什么形式出现的?(例如“时间”、“花费”、“价格”、“满意度”、“资源限制”等)。
-
-
背包问题的本质:
- 背包DP的本质是一种“选择”的决策过程。对于每个物品,决策是“选”还是“不选”(0-1背包),或者是“选0件、1件、2件…”(完全背包/多重背包)。DP的过程就是记录下每个决策阶段的最优状态。
3万+

被折叠的 条评论
为什么被折叠?



