P1510 精卫填海 题解复盘
模块:01背包
类型:至少装满 + 最小代价
目标:总体积至少达到 V,在消耗体力不超过 C 的前提下,使剩余体力最大。
基本信息
| 项目 | 内容 |
|---|---|
| 题目编号、来源 | P1510 洛谷 |
| 训练层级 | 普及− |
| 知识版块 | 01背包、最小代价背包、至少装满 |
解题前・关键信号识别
| 维度 | 分析 |
|---|---|
| 目标、约束、底层结构 | 每块石头只能使用一次,总体积至少达到 V,要求消耗体力最小。 |
| 数据规模 | n 较大,但总体积不大,可以使用 01 背包。 |
| 候选算法和依据 | 每块石头只能选一次,因此是 01 背包;要求体积至少达到 V,并使花费最小,因此属于最小代价背包。 |
| 复杂度预判 | 时间复杂度:O(n×limit) 空间复杂度:O(limit) |
解题后・外化复盘
| 维度 | 内容 |
|---|---|
| 状态定义 | dp[j] 表示:总体积恰好为 j 时,需要消耗的最小体力。 |
| 状态转移 | dp[j]=min(dp[j],dp[j-vol[i]]+cost[i]) |
| 遍历顺序 | 每块石头只能选一次,因此体积必须倒序枚举。 |
| 实现结构 / 核心思路 | ① 初始化所有状态为 INF。 ② dp[0]=0。 ③ 枚举每块石头进行01背包。 ④ 在所有 j≥V 的状态中寻找最小体力。 ⑤ 判断是否能够完成填海。 |
| 错因回溯 | ① 一开始把它当成普通最大价值背包。 ② 忘记初始化 dp[0]=0。 ③ 数组 vol、cost 开成 105 导致 WA,应开到 10005。 ④ 忘记判断 best==INF。 |
| 边界和易错点 | 1. dp 初始化为 INF。 2. dp[0]=0。 3. 背包容量必须倒序。 4. 最后答案是在所有 j≥V 中找最小值,而不是只看 dp[V]。 5. 若 best==INF 或 best>C,输出 Impossible。 |
| 下次看到什么信号,我应该想到这个方法 | 看到: ① 每个物品只能选一次。 ② 至少达到某个容量。 ③ 求最小代价。 想到: 01背包(至少装满)+ 最小代价DP。 |
核心思想
第一步:状态定义
定义:
cpp
dp[j]
表示:
总体积恰好为 j 时,需要消耗的最小体力。
例如:
text
dp[120]=15
表示:
总体积恰好为 120,
最少需要消耗 15 点体力。
第二步:为什么初始化为 INF?
开始时:
除了体积为 0,
其它体积都无法达到。
因此:
cpp
for(int i=0;i<=limit;i++)
dp[i]=INF;
表示:
text
目前不能达到这些体积。
第三步:为什么 dp0=0?
什么石头都不拿:
text
总体积=0
消耗体力:
text
0
因此:
cpp
dp[0]=0;
这是整个 DP 的起点。
以后所有状态,
都是由:
text
dp[0]
一步一步转移出来的。
第四步:状态转移
当前石头:
text
体积=vol
体力=cost
如果以前:
text
总体积=j-vol
能够达到,
那么:
加入当前石头以后:
text
总体积=j
体力变成:
cpp
dp[j-vol]+cost
因此:
cpp
dp[j]=min(dp[j],dp[j-vol]+cost);
第五步:为什么倒序?
因为:
每块石头:
text
只能使用一次
因此:
cpp
for(int j=limit;j>=vol[i];j--)
如果正序:
text
当前石头
刚更新出的状态
又继续更新
↓
相当于
同一块石头用了两次
因此:
必须倒序。
第六步:为什么最后不是 dpV?
题目要求:
text
总体积
至少
V
不是:
text
恰好
V
例如:
text
V=100
可能:
text
95
×
但是:
text
110
√
因此:
最后:
cpp
int best=INF;
for(int j=V;j<=limit;j++){
best=min(best,dp[j]);
}
寻找:
text
所有
≥V
中的最小体力
第七步:为什么输出 C-best?
题目要求:
text
剩余体力最大
如果:
初始体力:
text
C
最少消耗:
text
best
那么:
剩余:
cpp
C-best
即可。
AC完整代码
cpp
#include<iostream>
#include<algorithm>
using namespace std;
const int INF=1e9;
int main(){
int V,n,C;
cin>>V>>n>>C;
int vol[10005],cost[10005];
int maxv=0;
for(int i=1;i<=n;i++){
cin>>vol[i]>>cost[i];
maxv=max(maxv,vol[i]);
}
int limit=V+maxv;
int dp[20005];
for(int i=0;i<=limit;i++){
dp[i]=INF;
}
dp[0]=0;
for(int i=1;i<=n;i++){
for(int j=limit;j>=vol[i];j--){
dp[j]=min(
dp[j],
dp[j-vol[i]]+cost[i]
);
}
}
int best=INF;
for(int j=V;j<=limit;j++){
best=min(best,dp[j]);
}
if(best==INF || best>C)
cout<<"Impossible";
else
cout<<C-best;
return 0;
}
模型总结
| 普通01背包 | 本题 |
|---|---|
| 求最大价值 | 求最小代价 |
| 容量不能超过 V | 容量至少达到 V |
| 最后看 dpV | 最后枚举所有 ≥V 的状态 |
max() |
min() |
记忆
text
看到:
① 每个物品只能选一次
↓
01背包
② 至少达到某个容量
↓
最后枚举:
j≥V
③ 求最小花费
↓
dp初始化INF
↓
dp[0]=0
↓
状态转移:
dp[j]=min(dp[j],dp[j-vol]+cost)
最后:
best=min(dp[j])
↓
答案:
C-best
P1910 L 国的战斗之间谍 题解复盘
模块:动态规划
类型:二维费用01背包
目标:在伪装能力和工资都不超过限制的情况下,使获得资料最多。
基本信息
| 项目 | 内容 |
|---|---|
| 题目编号、来源 | P1910 洛谷原创 |
| 训练层级 | 普及− |
| 知识版块 | 二维费用01背包、二维DP |
解题前・关键信号识别
| 维度 | 分析 |
|---|---|
| 目标、约束、底层结构 | 每个人只能选一次,同时受到伪装能力和工资两个限制,要求资料最大。 |
| 数据规模 | 两个容量都较小,可以二维DP。 |
| 候选算法和依据 | 每个人只能选择一次,因此是01背包;存在两个限制,因此是二维费用背包。 |
| 复杂度预判 | 时间复杂度:O(n×m×x) 空间复杂度:O(m×x) |
解题后・外化复盘
| 维度 | 内容 |
|---|---|
| 状态定义 | dp[j][k] 表示:伪装能力不超过 j,工资不超过 k 时,能够获得的最大资料。 |
| 状态转移 | dp[j][k]=max(dp[j][k],dp[j-b][k-c]+a) |
| 遍历顺序 | 两个容量都必须倒序,因为每个人只能选择一次。 |
| 实现结构 / 核心思路 | ① 枚举每个人。 ② 第一维容量倒序。 ③ 第二维容量倒序。 ④ 更新最大资料。 |
| 错因回溯 | ① 容易忘记两个维度都要倒序。 ② 容易把二维费用背包写成两个一维DP。 |
| 边界和易错点 | 1. 两个容量都必须倒序。 2. 每个人只能选一次。 3. dp 默认初始化为0即可。 |
| 下次看到什么信号,我应该想到这个方法 | 看到: ① 每个物品只能选一次。 ② 有两个容量限制。 ③ 求最大价值。 想到: 二维费用01背包。 |
核心思想
第一步:状态定义
定义:
cpp
dp[j][k]
表示:
伪装能力不超过 j,工资不超过 k 时,能够获得的最大资料。
例如:
text
dp[5][20]=30
表示:
text
总伪装能力≤5
总工资≤20
最多获得30份资料
第二步:状态转移
当前间谍:
text
资料=a
伪装能力=b
工资=c
如果选择当前间谍:
那么之前必须满足:
text
伪装能力:
j-b
工资:
k-c
因此:
cpp
dp[j][k]=max(dp[j][k],dp[j-b][k-c]+a);
第三步:为什么两个维度都要倒序?
因为:
每个人:
text
只能选一次
如果:
第一维正序:
当前人可能被重复使用。
如果:
第二维正序:
当前人同样可能被重复使用。
因此:
必须:
cpp
for(int j=m;j>=b;j--)
以及:
cpp
for(int k=x;k>=c;k--)
两个容量都倒序。
第四步:最后为什么输出 dpmx?
因为:
text
dp[j][k]
表示:
text
容量不超过
j、k
因此:
cpp
dp[m][x]
已经包含了:
text
所有满足限制的方案
直接输出即可。
AC完整代码
cpp
#include<iostream>
#include<map>
#include<vector>
#include<algorithm>
using namespace std;
struct node{
int a,b,c;
};
int main(){
int n,m,x;
cin>>n>>m>>x;
vector<node> v(n+1);
for(int i=1;i<=n;i++){
cin>>v[i].a>>v[i].b>>v[i].c;
}
int dp[1005][1005]={0};
for(int i=1;i<=n;i++){
for(int j=m;j>=v[i].b;j--){
for(int k=x;k>=v[i].c;k--){
dp[j][k]=max(
dp[j][k],
dp[j-v[i].b][k-v[i].c]+v[i].a
);
}
}
}
cout<<dp[m][x];
return 0;
}
模型总结
| 普通01背包 | 本题 |
|---|---|
| 一个容量 | 两个容量 |
dp[j] |
dp[j][k] |
| 一层倒序 | 两层都倒序 |
| 最大价值 | 最大资料 |
记忆
text
看到:
① 每个物品只能选一次
↓
01背包
② 有两个限制
例如:
重量
体积
时间
金钱
↓
二维费用背包
状态:
dp[j][k]
转移:
dp[j][k]
=
max(
dp[j][k],
dp[j-b][k-c]+a
)
注意:
两个容量
都要倒序