背包动态规划问题解析
背包问题是动态规划中的经典问题系列,本文将详细解析九道背包类动态规划题目的解题思路和实现方法。
1. 基础背包问题
1.1 P1048 [NOIP 2005 普及组] 采药
问题本质:01背包问题
关键思路:
- 每株药草只能采一次(01背包)
dp[j]表示在时间j内能获得的最大价值- 状态转移:
dp[j] = max(dp[j], dp[j-w[i]] + val[i]) - 需要逆序遍历容量,确保每个物品只使用一次
cpp
for(int i = 1; i <= m; i++) {
for(int j = t; j >= w[i]; j--) { // 逆序遍历
dp[j] = max(dp[j - w[i]] + val[i], dp[j]);
}
}
1.2 P1060 [NOIP 2006 普及组] 开心的金明
问题特点:01背包的变体
注意事项:
- 价值是价格×重要度,不是直接输入
- 状态转移与标准01背包相同
- 只需要处理
j >= v[i]的情况
2. 多维背包问题
2.1 P1855 榨取kkksc03
问题类型:二维背包(双重限制)
解题要点:
- 同时受到金钱M和时间T的限制
dp[j][k]表示使用j金钱和k时间能处理的最大请求数- 状态转移需要同时满足两个维度的限制
cpp
for(int i = 1; i <= n; i++) {
for(int j = M; j >= m[i]; j--) {
for(int k = T; k >= t[i]; k--) {
dp[j][k] = max(dp[j][k], dp[j-m[i]][k-t[i]] + 1);
}
}
}
3. 完全背包与货币系统
3.1 P5020 [NOIP 2018 提高组] 货币系统
核心思想:完全背包 + 最小表示集
解题步骤:
- 对货币面额排序
- 使用完全背包判断每个面额是否能被更小的面额组合表示
- 不能被表示的面额必须保留在简化系统中
关键代码:
cpp
sort(a + 1, a + 1 + n); // 排序面额
f[0] = 1; // 0元可以被表示
for(int i = 1; i <= n; i++) {
if(f[a[i]]) { // 如果当前面额能被表示
ans--; // 不需要保留
continue;
}
// 完全背包更新
for(int j = a[i]; j <= a[n]; j++) {
f[j] = f[j] | f[j - a[i]];
}
}
4. 分组背包
4.1 P1757 通天之分组背包
问题特点:每组物品只能选一个
解题策略:
- 将物品按组分类
- 三层循环:组数 → 容量(逆序) → 组内物品
- 确保在每个容量下,每组只选择一个物品
cpp
for(int i = 1; i <= t; i++) { // 遍历组
for(int j = v; j >= 0; j--) { // 逆序遍历容量
for(int k = 1; k <= b[i]; k++) { // 遍历组内物品
if(j >= w[g[i][k]])
dp[j] = max(dp[j], dp[j - w[g[i][k]]] + z[g[i][k]]);
}
}
}
5. 依赖背包(有附属品的背包)
5.1 P1064 [NOIP 2006 提高组] 金明的预算方案
问题结构:主件 + 附件(最多2个)
处理方式:
- 只有主件
- 主件 + 附件1
- 主件 + 附件2
- 主件 + 附件1 + 附件2
四种情况分别考虑
实现技巧:
- 使用数组记录每个主件的附件
- 针对每种组合分别进行状态转移
6. 特殊背包问题
6.1 P2946 [USACO09MAR] Cow Frisbee Team S
问题特点:模数背包
核心思路:
f[i][j]表示前i头牛,能力之和模F等于j的方案数- 状态转移:
f[i][j] = f[i-1][j] + f[i-1][(j-cow[i]+F)%F] - 注意取模操作
6.2 P1156 垃圾陷阱
问题类型:时间与高度双重维度的动态规划
解题关键:
f[j]表示高度为j时的最大存活时间- 两种选择:堆放(增加高度)或吃掉(增加时间)
- 需要按时间排序垃圾
cpp
for(int i = 1; i <= g; i++) {
for(int j = d; j >= 0; j--) {
if(f[j] >= c[i].t) { // 当前时间足够处理这个垃圾
if(j + c[i].h >= d) { // 可以逃脱
cout << c[i].t;
return 0;
}
// 堆放垃圾
f[j + c[i].h] = max(f[j + c[i].h], f[j]);
// 吃掉垃圾
f[j] += c[i].l;
}
}
}
6.3 P5322 [BJOI2019] 排兵布阵
问题特点:分组背包的变形
处理步骤:
- 按城堡分组,每个城堡是一个"组"
- 对于每个城堡,按对手的兵力排序
- 用分组背包求解最优分配
背包问题总结
| 问题类型 | 特点 | 遍历顺序 | 时间复杂度 |
|---|---|---|---|
| 01背包 | 每个物品选一次 | 容量逆序 | O(n×capacity) |
| 完全背包 | 物品无限次 | 容量正序 | O(n×capacity) |
| 分组背包 | 每组选一个 | 组→容量逆序→组内物品 | O(n×capacity) |
| 多维背包 | 多重限制 | 各维度都逆序 | O(n×∏capacity_i) |
| 依赖背包 | 物品间有依赖 | 按组合情况处理 | 组合数×O(n×capacity) |
解题技巧
- 明确背包类型:01背包、完全背包还是其他变体
- 确定状态定义:dp数组的含义要清晰
- 注意遍历顺序 :
- 01背包:容量逆序
- 完全背包:容量正序
- 分组背包:先组,再逆序容量,最后组内物品
- 边界条件 :通常
dp[0] = 0或根据题目设定 - 空间优化:大多数背包问题可以优化为一维数组
通过这九道题目的练习,可以掌握背包动态规划的核心思想和常见变体的解决方法。
背包动态规划题
1 P1048 [NOIP 2005 普及组] 采药
https://www.luogu.com.cn/problem/P1048
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int w[105],val[105];
int dp[1005];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int t,m,res=-1;
cin>>t>>m;
for(int i=1;i<=m;i++){
cin>>w[i]>>val[i];
}for(int i=1;i<=m;i++){
for(int j=t;j>=0;j--){
if(j>=w[i]){
dp[j]=max(dp[j-w[i]]+val[i],dp[j]);
}
}
}cout<<dp[t];
}
2 P1060 [NOIP 2006 普及组] 开心的金明
https://www.luogu.com.cn/problem/P1060
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int w[30],v[30],f[50000];
int n,m;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>m>>n;
for(int i=1;i<=n;i++){
cin>>v[i]>>w[i];
w[i]*=v[i];
}for(int i=1;i<=n;i++){
for(int j=m;j>=v[i];j--){
if(j>v[i]){
f[j]=max(f[j],f[j-v[i]]+w[i]);
}
}
}cout<<f[m]<<'\n';
}
3 P1855 榨取kkksc03
https://www.luogu.com.cn/problem/P1855
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int n,M,T,dp[1010][1010];
int m[1010],t[1010];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>M>>T;
for(int i=1;i<=n;i++){
cin>>m[i]>>t[i];
for(int j=M;j>=m[i];j--){
for(int k=T;k>=t[i];k--){
dp[j][k]=max(dp[j][k],dp[j-m[i]][k-t[i]]+1);
}
}
}cout<<dp[M][T];
}
4 P5020 [NOIP 2018 提高组] 货币系统
https://www.luogu.com.cn/problem/P5020
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int f[25005];
int a[105];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int i,j,n,T,ans;
cin>>T;
while(T--){
memset(f,0,sizeof f);
cin>>n;//当前组的面额为n
ans=n;
for(int i=1;i<=n;i++){
cin>>a[i];
}sort(a+1,a+1+n);
f[0]=1;//0可以被表示
for(int i=1;i<=n;i++){
//能被别的面额表示
if(f[a[i]]){
ans--;continue;
}
//要么j本身能表示,j-a[i]能被表示
for(int j=a[i];j<=a[n];j++){
f[j]=f[j]|f[j-a[i]];
}
}cout<<ans<<'\n';
}
}
5 P1757 通天之分组背包
https://www.luogu.com.cn/problem/P1757
每组只能选一个
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int v,n,t;
int x;
int g[205][205];
int i,j,k;
int w[10001],z[10001];
int b[10001];
int dp[10001];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>v>>n;
for(int i=1;i<=n;i++){
cin>>w[i]>>z[i]>>x;
t=max(t,x);//小组组数
b[x]++;//小组的物品加1
g[x][b[x]]=i;//g[i][j]表示i中的j
}for(int i=1;i<=t;i++){//小组组数
for(int j=v;j>=0;j--){//容量
for(int k=1;k<=b[i];k++){//小组中的物品
if(j>=w[g[i][k]])
dp[j]=max(dp[j],dp[j-w[g[i][k]]]+z[g[i][k]]);
}//遍历顺序:遵循「组 → 逆序容量 → 组内物品」(分组背包的标准顺序)
//在某个容量中只会选一个这组的
}
}cout<<dp[v];
}
6 P1064 [NOIP 2006 提高组] 金明的预算方案
https://www.luogu.com.cn/problem/P1064
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
ll n,m,v[70],w[70],c[70],dp[32010],son[70][3],q[70];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>v[i]>>w[i]>>q[i];
c[i]=v[i]*w[i];
if(q[i])son[q[i]][++son[q[i]][0]]=i;
}for(int i=1;i<=m;i++){
for(int j=n;j>=0;j--){
if(q[i])continue;
if(j>=v[i])
dp[j]=max(dp[j],dp[j-v[i]]+c[i]);
if(son[i][1]&&j>=v[i]+v[son[i][1]])
dp[j]=max(dp[j],dp[j-v[i]-v[son[i][1]]]+c[i]+c[son[i][1]]);
if(son[i][2]&&j>=v[i]+v[son[i][2]])
dp[j]=max(dp[j],dp[j-v[i]-v[son[i][2]]]+c[i]+c[son[i][2]]);
if(son[i][0]>=2&&j>=v[i]+v[son[i][1]]+v[son[i][2]])
dp[j]=max(dp[j],dp[j-v[i]-v[son[i][1]]-v[son[i][2]]]+c[i]+c[son[i][1]]+c[son[i][2]]);
}
}cout<<dp[n];
}
7 P2946 [USACO09MAR] Cow Frisbee Team S
https://www.luogu.com.cn/problem/P2946
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
const int mod=1e8;
ll cow[2005],f[2005][2005];
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
int n,F;
cin>>n>>F;
for(int i=1;i<=n;i++){
cin>>cow[i];
cow[i]%=F;//提前取模
}for(int i=1;i<=n;i++){
f[i][cow[i]]=1;//选一次是1
}for(int i=1;i<=n;i++){
for(int j=0;j<=F-1;j++){
f[i][j]=((f[i][j]+f[i-1][j])%mod+f[i-1][(j-cow[i]+F)%F])%mod;
//选与不选
}
}cout<<f[n][0];
}
8 P1156 垃圾陷阱
https://www.luogu.com.cn/problem/P1156
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
struct p{
int t,h,l;
}c[101];
int d,g;
int ti[101];
int f[101];
bool cmp(p a,p b){
return a.t<b.t;
}
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>d>>g;//d:高度,g:数量
for(int i=1;i<=g;i++){
cin>>c[i].t>>c[i].l>>c[i].h;
}sort(c+1,c+1+g,cmp);
f[0]=10;
for(int i=1;i<=g;i++){//遍历每个垃圾(时间处理)
for(int j=d;j>=0;j--){
if(f[j]>=c[i].t){//当前高度的时间>垃圾扔下的时间
if(j+c[i].h>=d){//可跳出
cout<<c[i].t;
return 0;
}f[j+c[i].h]=max(f[j+c[i].h],f[j]);//堆放后的时间等于原来的
f[j]+=c[i].l;//吃掉当前垃圾的存活时间
}
}
}cout<<f[0];
}
9 P5322 [BJOI2019] 排兵布阵
https://www.luogu.com.cn/problem/P5322
cpp
#include <bits/stdc++.h>
using namespace std;
using ll=long long;
int s,n,m,dp[20002],a[110][110],ans;
int main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>s>>n>>m;//s名队友,n座城堡,m名士兵
for(int i=1;i<=s;i++){
for(int j=1;j<=n;j++){
cin>>a[j][i];//j座城堡的第几名士兵
}
}for(int i=1;i<=n;i++){
sort(a[i]+1,a[i]+1+s);
}for(int i=1;i<=n;i++){
for(int j=m;j>=0;j--){
for(int k=1;k<=s;k++){
if(j>a[i][k]*2)
dp[j]=max(dp[j-a[i][k]*2-1]+k*i,dp[j]);//分组背包
}
}
}for(int i=0;i<=m;i++){
ans=max(ans,dp[i]);
}cout<<ans;
}