简单背包问题
一、01背包
回到目录
问题描述:
一个旅行者有一个最多能装 M 公斤的背包,现在有 n 件物品,它们的重量分别是W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn,求旅行者能获得最大总价值。
输入
第一行:两个整数,M(背包容量,M ≤ 200)和N(物品数量,N ≤ 30);
第2…N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
输出
仅一行,一个数,表示最大总价值。
样例输入
10 4
2 1
3 3
4 5
7 9
样例输出
12
策略:
显然,对于每一个物品都只有两种状态,装和不装。对于一个子问题,我们可以用
d
[
i
]
[
j
]
d[i][j]
d[i][j]来表示在把前i个物品中装入容量为j的背包中所能够获得的最大的价值
那么可以很容易的得到状态转移方程为
d
[
i
]
[
j
]
=
{
d
[
i
−
1
]
[
j
]
(
j
<
w
[
i
]
)
m
a
x
(
d
[
i
−
1
]
[
j
]
,
d
[
i
−
1
]
[
j
−
w
[
i
]
]
+
v
[
i
]
)
(
j
≥
w
[
i
]
)
d[i][j]= \left\{ \begin{aligned} d[i-1][j] & &({j < w[i]})\\ max(d[i-1][j],d[i-1][j-w[i]]+v[i]) & &({j \geq w[i]}) \\ \end{aligned} \right.
d[i][j]={d[i−1][j]max(d[i−1][j],d[i−1][j−w[i]]+v[i])(j<w[i])(j≥w[i])
这样就通过双重循环去从前往后的填充这个
d
d
d这个二维数组,这样每一次填充的时候,都能够满足最优子结构,且无后效性,都是最优的这样最终的答案就是
d
[
n
]
[
m
]
d[n][m]
d[n][m]
根据样例填充的表格如下
纵轴:物品编号 横轴:当前的最大重量 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 0 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 | 1 |
2 | 0 | 0 | 1 | 3 | 3 | 4 | 4 | 4 | 4 | 4 | 4 |
3 | 0 | 0 | 1 | 3 | 5 | 5 | 6 | 8 | 8 | 9 | 9 |
4 | 0 | 0 | 1 | 3 | 4 | 5 | 6 | 9 | 9 | 10 | 12 |
代码
#include <cstdio>
#include <algorithm>
#define MAXN 205
#define MAXX 35
int d[MAXX][MAXN], w[MAXX], v[MAXX];
int main() {
int n, m;
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d %d", &w[i], &v[i]);
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= m; j++) {
if(j < w[i])
d[i][j] = d[i-1][j];
else
d[i][j] = std::max(d[i-1][j], d[i-1][j-w[i]]+v[i]);
}
}
printf("%d\n", d[n][m]);
return 0;
}
空间优化:
可以发现,对于二维的数组来讲,每更新当前一层的时候,只与上一层有关,可以从这里进行优化。
使用一维数组去表示,考虑d[i]表示背包中重量不超过i的最大价值,同样的状态转移方程如下
d
[
j
]
=
m
a
x
(
d
[
j
]
,
d
[
j
−
w
[
i
]
]
+
v
[
i
]
)
     
(
j
≥
w
[
i
]
)
d[j] = max(d[j], d[j-w[i]]+v[i]) \,\,\,\,\,(j\geq w[i])
d[j]=max(d[j],d[j−w[i]]+v[i])(j≥w[i])
对于一维数组我们的填充就要考虑从后往前填充(
j
−
>
w
[
i
]
j->w[i]
j−>w[i]),因为我们要保证随着每考虑一个物品的时候,我们都要尽可能的装入当前的能装的背包,同时要基于上一层的情况进行下去,才能找出最优情况.
至于从前往后填充为什么不行,可以这样理解,因为在每一次更新
d
[
j
]
d[j]
d[j]的值的时候,我们需要保证,
d
[
j
]
d[j]
d[j]前面的值是选择上一个物品时的情况,如果从前往后填充,随着j的增大,
d
[
j
−
w
[
i
]
]
+
v
[
i
]
d[j-w[i]]+v[i]
d[j−w[i]]+v[i]一定是大于
d
[
j
]
d[j]
d[j]本身的,而且会越来越大,这样d[j]也没有记录上一层的情况(因为此时每种物品只能使用一次),那么d[j-w[i]]+v[i]会不断的更新,d[j]的值会越来越大,因为v[i]和之前的d[j]会被反复叠加。
可以总结出,逆推的关键就是因为每种物品只能选择一次
如果要求恰好装满背包,需要将
d
d
d数组其他初始化无穷小,
d
[
0
]
=
0
d[0]=0
d[0]=0
代码
#include <cstdio>
#include <algorithm>
#define MAXN 205
#define MAXX 35
int d[MAXN], w[MAXX], v[MAXX];
int main() {
int n, m;
scanf("%d %d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d %d", &w[i], &v[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--) //逆推
d[j] = std::max(d[j], d[j-w[i]]+v[i]);
printf("%d\n", d[m]);
return 0;
}
二、完全背包
问题描述:
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。
输入
第一行:两个整数,M(背包容量,M <= 200)和N(物品数量,N <= 30);
第2…N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。
输出
仅一行,一个数,表示最大总价值。
样例输入
10 4
2 1
3 3
4 5
7 9
样例输出
12
策略分析:
现在的情况是每个物品可以选择多个,那么久不再是选择或者不选的情况了,情况变成了每个物品可以选择
0
、
1
、
2
、
3...
K
0、1、2、3...K
0、1、2、3...K件,只要装的下,就可以放进去,那么基于之前的
01
01
01背包,如果还是用
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]来表示从前i件物品中选择重量不超过j的物品的最大价值那么很容易得到状态转移方程为
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
k
∗
w
[
i
]
]
+
k
∗
v
[
i
]
  
(
0
≤
k
∗
w
[
i
]
≤
j
)
)
dp[i][j] = max( dp[i-1][j-k*w[i]]+k*v[i]\,\, (0\leq k*w[i] \leq j))
dp[i][j]=max(dp[i−1][j−k∗w[i]]+k∗v[i](0≤k∗w[i]≤j))
代码
#include <iostream>
#include <algorithm>
using namespace std;
#define N 35
#define M 205
int dp[N][M];
int w[N], v[N];
int main () {
int n, m;
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= m; j++) {
if(j < w[i])
dp[i][j] = dp[i-1][j];
else {
int t = j / w[i];
for(int k = 0; k <= t; k++)
dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]]+v[i]*k);
}
}
}
cout << "max=" << dp[n][m];
return 0;
}
同样进行优化
由于完全背包的每一个物品可以多次使用个。考虑
01
01
01背包中的一维数组实现方法,只需要将其中的逆推改为顺推即可,因为每件物品可以无限次的使用。那么在考虑是否装同一种物品的时候,肯定是能装下的范围内,越多越好。状态转移方程如下,和
01
01
01背包相同,不过实现的时候使用顺推
d
[
j
]
=
m
a
x
(
d
[
j
]
,
d
[
j
−
w
[
i
]
]
+
v
[
i
]
)
     
(
j
≥
w
[
i
]
)
d[j] = max(d[j], d[j-w[i]]+v[i]) \,\,\,\,\,(j\geq w[i])
d[j]=max(d[j],d[j−w[i]]+v[i])(j≥w[i])
代码
#include <iostream>
#include <algorithm>
using namespace std;
#define N 35
#define M 205
int dp[M];
int w[N], v[N];
int main () {
int n, m;
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i++) {
for(int j = w[i]; j <= m; j++) //顺推
dp[j] = max(dp[j], dp[j-w[i]]+v[i]);
}
cout << "max=" <<dp[m];
return 0;
}
三、多重背包
问题描述:
为了庆贺班级在校运动会上取得全校第一名成绩,班主任决定开一场庆功会,为此拨款购买奖品犒劳运动员。期望拨款金额能购买最大价值的奖品,可以补充他们的精力和体力
输入
第一行二个数n(n ≤ 500),m(m ≤ 6000),其中n代表希望购买的奖品的种数,m表示拨款金额。
接下来n行,每行3个数,v、w、s,分别表示第I种奖品的价格、价值(价格与价值是不同的概念)和能购买的最大数量(买0件到s件均可),其中v ≤ 100,w ≤ 1000,s ≤ 10。
输出
一行:一个数,表示此次购买能获得的最大的价值(注意!不是价格)。
样例输入
5 1000
80 20 4
40 50 9
30 50 7
40 30 6
20 20 1
样例输出
1040
策略分析:
多重背包,类似于完全背包,不过有限制,每种物品规定的使用的数量不超过
s
s
s,只需改一下状态转移方程即可
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
k
∗
w
[
i
]
]
+
k
∗
v
[
i
]
  
(
0
≤
k
≤
n
u
m
[
i
]
)
)
dp[i][j] = max( dp[i-1][j-k*w[i]]+k*v[i]\,\, (0\leq k \leq num[i]))
dp[i][j]=max(dp[i−1][j−k∗w[i]]+k∗v[i](0≤k≤num[i]))
代码
#include <iostream>
#include <algorithm>
#define N 505
#define M 6005
using namespace std;
int dp[N][M];
int w[N], v[N], num[N];
int main () {
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++)
cin >> w[i] >> v[i] >> num[i];
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= m; j++) {
if(j < w[i])
dp[i][j] = dp[i-1][j];
else {
int t = j / w[i];
for(int k = 0; k <= t && k <= num[i]; k++)
dp[i][j] = max(dp[i][j], dp[i-1][j-k*w[i]] + k * v[i]);
}
}
}
cout << dp[n][m];
return 0;
}
四、混合背包
问题描述:
一个旅行者有一个最多能装V公斤的背包,现在有n件物品,W1,W2,…,Wn,它们的价值分别为C1,C2,…,Cn。有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
输入
第一行:二个整数,M(背包容量,M ≤ 200),N(物品数量,N ≤ 30);
第2…N+1行:每行三个整数Wi,Ci,Pi,前两个整数分别表示每个物品的重量,价值,第三个整数若为0,则说明此物品可以购买无数件,若为其他数字,则为此物品可购买的最多件数(Pi)。。
输出
仅一行,一个数,表示最大总价值。
样例输入
10 3
2 1 0
3 3 1
4 5 4
样例输出
11
提示
选第一件物品1件和第三件物品2件。
策略分析:
混合背包就是将前三种背包结合起来使用,那么显然对于不同的背包使用不同的状态转移方程即可。
代码
#include <iostream>
#include <algorithm>
#define N 35
#define M 205
using namespace std;
int dp[M];
int w[N], v[N], num[N];
int main () {
int n, m;
cin >> m >> n;
for(int i = 1; i <= n; i++)
cin >> w[i] >> v[i] >> num[i];
for(int i = 1; i <= n; i++) {
if(num[i] == 1) { //01背包
for(int j = m; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}else if(num[i] == 0) { //完全背包
for(int j = w[i]; j <= m; j++)
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}else { //多重背包
for(int j = m; j >= 0; j--) {
for(int s = 0; s <= num[i]; s++)
if(j >= s*w[i])
dp[j] = max(dp[j], dp[j-s*w[i]] + s * v[i]);
}
}
}
cout << dp[m];
return 0;
}