多维背包
有N件物品和一个容量为V,承重量为M的背包。第i件物品的体积是c[i]
,重量是z[i]
,价值是w[i]
。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,并且重量在背包的承重范围之内,且价值总和最大。
多维背包:前面的背包问题一般只有一个限制(背包体积),而多维背包问题的限制条件不止一个,可能两个,甚至多个,要求必须要同时满足多个条件。
多维背包问题的分析方法和普通背包相同,还是以物品为阶段,考虑第i种物品选还是不选,状态在原有基础上多一维。
f[i][v][m]
表示当背包体积为v,承重量为m,选择前i件物品放入时的最大价值。
按照普通01背包的推导方法。第i件物品可以取可以不取。
如果取第i件物品
f
[
i
]
[
v
]
[
m
]
=
f
[
i
−
1
]
[
v
−
c
[
i
]
]
[
m
−
z
[
i
]
]
;
f[i][v][m]=f[i-1][v-c[i]][m-z[i]];
f[i][v][m]=f[i−1][v−c[i]][m−z[i]];
如果不取第i件物品
f
[
i
]
[
v
]
[
m
]
=
f
[
i
−
1
]
[
v
]
[
m
]
;
f[i][v][m]=f[i-1][v][m];
f[i][v][m]=f[i−1][v][m];
然后取最大值即可。
例题
「一本通入门 2.9.2」 潜水员
【题目描述】
潜水员为了潜水要使用特殊的装备。他有一个带2种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?
例如:潜水员有5个气缸。每行三个数字为:氧,氮的(升)量和气缸的重量:
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
如果潜水员需要5升的氧和60升的氮则总重最小为249(1,2或者4,5号气缸)。
你的任务就是计算潜水员为了完成他的工作需要的气缸的重量的最低值。
【输入】
第一行有2整数m,n(1≤m≤21,1≤n≤79)。它们表示氧,氮各自需要的量。
第二行为整数k(1≤n≤1000)表示气缸的个数。
此后的k行,每行包括
a
i
a_i
ai,
b
i
b_i
bi,
c
i
c_i
ci(
1
≤
a
i
≤
21
1\le a_i \le 21
1≤ai≤21,
1
≤
b
i
≤
79
1≤b_i≤79
1≤bi≤79,
1
≤
c
i
≤
800
1≤c_i≤800
1≤ci≤800)3个整数。这些各自是:第i个气缸里的氧和氮的容量及汽缸重量。
【输出】
仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。
【输入样例】
5 60
5
3 36 120
10 25 129
5 50 250
1 45 130
4 20 119
【输出样例】
249
【来源】
一本通在线评测
思路
这道题就是典型的多维背包问题,不难发现,我们只需要在普通的情况下增加一维即可,只不过我们需要注意一下,如果氧气和氮气超过需求时,直接等于需求
#include<bits/stdc++.h>
using namespace std;
int w[1010],v[1010],M[1010];
int dp[25][100];
int main(){
int m,n,k;//m为氧气,n为氮气
cin>>m>>n>>k;
for (int i=1;i<=k;i++){
cin>>w[i]>>v[i]>>M[i];//w为氧气,v为氮气,m为称重
}
memset(dp,0x7f7f7f,sizeof(dp));
dp[0][0]=0;
for (int i=1;i<=k;i++){
for (int j=m;j>=0;j--){
for (int l=n;l>=0;l--){
dp[j][l]=min(dp[j][l],dp[max(0,j-w[i])][max(0,l-v[i])]+M[i]);
}
}
}
cout<<dp[m][n];
return 0;
}
分组背包
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
输入:
10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3
输出:
20
第一行输入依次是背包容量,物品种类以及物品组数。接下来每行依次是体积,价值和所属组数。
对于任何一种背包问题,分析过程都是按照01背包的方式来分解,所以分组背包还是可以看做01背包来求解,01背包是对于每种物品,有取和不取两种状态,而对于分组背包而言,是对于每一组物品,也只有两种状态,取走该组内其中一个和一个都不取(每组物品相互冲突)。所以分组背包的每一组仍然可以看做01背包求解。接着再考虑每一组,这一组内取哪一个数是最优解是不确定的,所以我们还需要枚举组内的每一个数。
分组背包有三层循环,一层是枚举组数,一层是枚举组内选择哪一个物品,一层是背包的体积。具体的枚举顺序是怎么样的?
首先,阶段为第几组物品,所以最外层循环肯定是组数
如果第二层循环是组内第几个物品,最后枚举背包体积,那么就类似于多重背包的错误写法了,(详情请参考浅说背包问题(中))
最外层为第几种物品,第二层循环为每种物品有几个,表示的含义就不再是每组里面只能选择一个,而是这一组内的物品都可以选择
所以第二层循环枚举背包体积,第三层循环枚举组内第几个物品。
例题
【题目描述】
一个旅行者有一个最多能装V公斤的背包,现在有n件物品,它们的重量分别是
W
1
W_1
W1,
W
2
W_2
W2,…,
W
n
W_n
Wn,它们的价值分别为
C
1
C_1
C1,
C
2
C_2
C2,…,
C
n
C_n
Cn。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
【输入】
第一行:三个整数,V(背包容量,V≤200),N(物品数量,N≤30)和T(最大组号,T≤10);
第2…N+1行:每行三个整数 W i W_i Wi, C i C_i Ci, P P P,表示每个物品的重量,价值,所属组号。
【输出】
仅一行,一个数,表示最大总价值。
【输入样例】
10 6 3
2 1 1
3 3 1
4 8 2
6 9 2
2 8 3
3 9 3
【输出样例】
20
【来源】
一本通在线评测
思路
这道题就是典型的模板题
#include<bits/stdc++.h>
using namespace std;
int W[30][30],C[30][30],tot[30],dp[300];
int main(){
int V,n,T;
cin>>V>>n>>T;
for (int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
tot[c]++;
W[c][tot[c]]=a;
C[c][tot[c]]=b;
}
for (int i=1;i<=T;i++){
for (int j=V;j>=0;j--){
for (int k=1;k<=tot[i];k++){
if (j>=W[i][k]){
dp[j]=max(dp[j],dp[j-W[i][k]]+C[i][k]);
}
}
}
}
cout<<dp[V];
return 0;
}
泛化背包
对于某些物品而言,他没有固定的费用和价值,他的价值是随着你分配的费用而发生着变化。比如你分配的费用较多,他的单个价值就变大。你分配的费用较少,他的单个价值就变小。他的价值不是固定的。这类背包问题也可以看做完全背包来求解。即该类物品理论上有无限种,但是考虑到背包的体积他最多只可以放V/c[i]个。解题办法和不优化的完全背包类似。尝试该物品所有可以放的可能性然后从中得到最优的解决办法。
物品的价值随着分配的空间变化而变化,分配任意空间都有可能是最优解,所以每一种情况都要枚举到,类似于完全背包的暴力做法,第三层循环枚举给物品i分配的空间大小。
例题
Description
有一个容量为m(m<=1000)的背包,现在有n(n<=1000)种物品,每个物品都只有一个,但是物品的价值w会随着给该物品分配的空间v的变化而变化,变化规律为w=
a
i
2
×
v
+
b
i
×
v
+
c
i
ai^2×v+bi×v+ci
ai2×v+bi×v+ci(变化规律仅在分配空间至少为1时生效),现要你装入一些物品,使得在体积小于等于m的情况下,价值尽可能大。
Format
Input
第一行输入两个正整数n,m分别表示物品数量和背包体积。
后面n行每行三个整数a[i],b[i],c[i]分别表示变化系数。
Output
一个正整数,最大价值。
Samples
输入数据 1
6 100
1 5 5
-1 1 -5
-8 9 5
2 -1 4
7 7 5
2 0 -10
输出数据 1
7305
Limitation
对于100%的数据,
1
<
=
m
,
n
<
=
1000
,
−
100
<
=
a
i
,
b
i
,
c
i
<
=
100
1<=m,n<=1000,-100<=ai,bi,ci<=100
1<=m,n<=1000,−100<=ai,bi,ci<=100
思路
这道题就是一道最为典型的泛化背包问题
#include<bits/stdc++.h>
using namespace std;
int a[1010],b[1010],c[1010],dp[1010];
int main(){
int n,m;
cin>>n>>m;
for (int i=1;i<=n;i++){
cin>>a[i]>>b[i]>>c[i];
}
for (int i=1;i<=n;i++){
for (int j=m;j>=0;j--){//上限空间
for (int k=1;k<=j;k++){//分配空间
dp[j]=max(dp[j],dp[j-k]+a[i]*a[i]*k+b[i]*k+c[i]);
}
}
}
cout<<dp[m];
return 0;
}
有依赖的背包
普通背包问题各个物品之间没有联系,而有依赖的背包是指一些物品必须依赖于另一些物品而存在,我们把他们称为主件和附件。如果要买附件,就一定要买对应的主件,反之,如果要买主件,不一定必须要买其附件。比如买音响之前需要先买电脑,不能单独买音响,但是买了电脑后不一定要买音响。有依赖的背包问题是背包问题中最难的一个部分,一个主件有多个附件,一个附件下面可能还有多个附件,这种情况下一般涉及到树形结构,目前我们能解决的一般都是一个附件只从属于一个主件,附件之下没有附件的情况。
例题
[NOIP2006 提高组] 金明的预算方案
题目描述
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 n n n 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
主件 | 附件 |
---|---|
电脑 | 打印机,扫描仪 |
书柜 | 图书 |
书桌 | 台灯,文具 |
工作椅 | 无 |
如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 0 0 个、 1 1 1 个或 2 2 2 个附件。每个附件对应一个主件,附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 n n n 元。于是,他把每件物品规定了一个重要度,分为 5 5 5 等:用整数 1 ∼ 5 1 \sim 5 1∼5 表示,第 5 5 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 10 10 元的整数倍)。他希望在不超过 n n n 元的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第 j j j 件物品的价格为 v j v_j vj,重要度为 w j w_j wj,共选中了 k k k 件物品,编号依次为 j 1 , j 2 , … , j k j_1,j_2,\dots,j_k j1,j2,…,jk,则所求的总和为:
v j 1 × w j 1 + v j 2 × w j 2 + ⋯ + v j k × w j k v_{j_1} \times w_{j_1}+v_{j_2} \times w_{j_2}+ \dots +v_{j_k} \times w_{j_k} vj1×wj1+vj2×wj2+⋯+vjk×wjk。
请你帮助金明设计一个满足要求的购物单。
输入格式
第一行有两个整数,分别表示总钱数 n n n 和希望购买的物品个数 m m m。
第 2 2 2 到第 ( m + 1 ) (m + 1) (m+1) 行,每行三个整数,第 ( i + 1 ) (i + 1) (i+1) 行的整数 v i v_i vi, p i p_i pi, q i q_i qi 分别表示第 i i i 件物品的价格、重要度以及它对应的的主件。如果 q i = 0 q_i=0 qi=0,表示该物品本身是主件。
输出格式
输出一行一个整数表示答案。
样例 #1
样例输入 #1
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
样例输出 #1
2200
提示
数据规模与约定
对于全部的测试点,保证 1 ≤ n ≤ 3.2 × 1 0 4 1 \leq n \leq 3.2 \times 10^4 1≤n≤3.2×104, 1 ≤ m ≤ 60 1 \leq m \leq 60 1≤m≤60, 0 ≤ v i ≤ 1 0 4 0 \leq v_i \leq 10^4 0≤vi≤104, 1 ≤ p i ≤ 5 1 \leq p_i \leq 5 1≤pi≤5, 0 ≤ q i ≤ m 0 \leq q_i \leq m 0≤qi≤m,答案不超过 2 × 1 0 5 2 \times 10^5 2×105。
NOIP 2006 提高组 第二题
思路
该题情况比较简单,一个附件只从属于一个主件,一个主件最多只有两个附件。我们还是可以用01背包来考虑。先根据主件做01背包。然后只有五种情况(一个都不选择,只选择主件,选择主件和附件1,选择主件和附件2,选择主件和附件1,2),然后选择五种情况中的最大值。但是一定要注意必须先满足在背包的容量范围之内。对于存储的问题,我们可以用c[i][0]表示主件,c[i][1]表示第一个附件,c[i][2]表示第二个附件。考虑的时候先考虑主件,在考虑附件,不单独考虑附件。
#include <bits/stdc++.h>
using namespace std;
int n,m;
int v[32100],w[32100],fjw[32100][3],fjv[32100][3];//v为主件的价值,w为主件的重量,fjw为附件的重量,fjv为附件的价值
int dp[33300];
int main() {
cin>>n>>m;
for (int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
if (c==0){
v[i]=a*b;
w[i]=a;
}else {
fjw[c][0]++;
fjw[c][fjw[c][0]]=a;
fjv[c][fjw[c][0]]=a*b;
}
}
for (int i=1;i<=m;i++){
for (int j=n;j>=w[i];j--){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
//情况1只要主件
if (j>=w[i]+fjw[i][1])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][1]]+v[i]+fjv[i][1]);
//情况2只要主件和附件1
if (j>=w[i]+fjw[i][2])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][2]]+v[i]+fjv[i][2]);
//情况2只要主件和附件2
if (j>=w[i]+fjw[i][1]+fjw[i][2])dp[j]=max(dp[j],dp[j-w[i]-fjw[i][1]-fjw[i][2]]+v[i]+fjv[i][1]+fjv[i][2]);
//情况3都要
}
}
cout<<dp[n];
return 0;
}
背包方案数
在01背包的前提下,求解得到最大数的方案有多少种
这个类似于一个递推,其实可以直接列出以下状态转移方程
f
[
j
]
+
=
f
[
j
−
v
]
f[j] += f[j - v]
f[j]+=f[j−v]
只不过这里要注意一下,需要对发f[0]
附上初值
例题
Description
有一个容量为m(m<=20000)的背包,现在有n(n<=2000)个物品,体积为v[i],现要你装入一些物品,使得在体积剩余体积最小,最小体积是多少,以及在最小体积的情况下,有多少种方案。
Format
Input
第一行输入两个正整数m,n分别表示背包体积和物品数量。
第二行n个正整数v[i]分别表示物品体积。
Output
第一行一个整数表示最小剩余体积。
第二行一个正整数表示方案数。
Samples
输入数据 1
99 8
3 5 8 13 21 34 55 89
输出数据 1
2
6
Limitation
对于100%的数据,1<=m<=20000,1<=n<=2000。
思路
这道题其实就是正常的01背包加上一个求方案数,代码如下
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long f[21000],dp[21000],v[2100];
int main() {
cin>>m>>n;
for (int i=1;i<=n;i++){
cin>>v[i];
}
for (int i=1;i<=n;i++){
for (int j=m;j>=v[i];j--){
dp[j]=max(dp[j],dp[j-v[i]]+v[i]);
}
}
long long ans=dp[m];
cout<<m-ans<<endl;
f[0]=1;
for (int i=1;i<=n;i++) {
for (int j=ans;j>=v[i];j--)
f[j]+=f[j-v[i]];
}
cout<<f[ans];
return 0;
}
01背包问题输出方案
有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。
数据范围
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<vi,wi≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 6
输出样例
1 4
这道题的思路其实不难,其实就是如果我们找到了正确答案,那么在从结尾往前推的的时候,那个dp转移方程一定是成立的,而且我们想要字典序最小,也应该从后往前进行枚举,所以我们很容易得到以下代码
#include<bits/stdc++.h>
using namespace std;
const int N=1005;
int n,m;
int v[N],w[N],f[N][N];
int main() {
cin>>n>>m;
for (int i=1;i<=n;i++){
cin>>v[i]>>w[i];
}
for (int i=n;i>=1;i--){
for (int j=0;j<=m;j++) {
f[i][j]=f[i+1][j];
if (j>=v[i])f[i][j]=max(f[i][j],f[i+1][j-v[i]]+w[i]);
}
}
//注:此时的最大值为f[1][m]
cout<<f[1][m]<<endl;
int j = m;
for (int i=1;i<=n;i++){
if (j>=v[i]&&f[i][j]==f[i+1][j-v[i]]+w[i]) {
cout<<i<<' ';
j-=v[i];
}
}
return 0;
}
背包问题总结
背包问题是线性DP的一个衍生,同时也是比较复杂的动态规划问题的一个简单雏形。动态规划是信息学竞赛的一个重难点,要想学好动态规划,先打好基础是非常有必要的。同样的,背包问题远不止这九类,还有很多类型的比较复杂的背包问题,这里先不做累述了。如果遇到比较复杂的背包问题,或者没有讲解过的背包问题,先看看能否转换前面我们学习过的简单背包问题来处理。一定要仔细分析问题,想出解决该类问题的状态状态转移方程(当前状态怎么由前一状态到达)
码了8000多个字,又破记录了