赞
踩
小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。他需要带一些研究材料,但是他的行李箱空间有限。这些研究材料包括实验设备、文献资料和实验样本等等,它们各自占据不同的空间,并且具有不同的价值。
小明的行李空间为 N,问小明应该如何抉择,才能携带最大价值的研究材料,每种研究材料可以选择无数次,并且可以重复选择。
第一行包含两个整数,N,V,分别表示研究材料的种类和行李空间
接下来包含 N 行,每行两个整数 wi 和 vi,代表第 i 种研究材料的重量和价值
输出一个整数,表示最大价值。
4 5
1 2
2 4
3 4
4 5
10
第一种材料选择五次,可以达到最大值。
数据范围:
1 <= N <= 10000;
1 <= V <= 10000;
1 <= wi, vi <= 10^9.
经典的多重背包问题,定义一个二维的dp数组,行是物品的编号,从 0 ∼ M − 1 0\sim M-1 0∼M−1,列是背包的容量,从 0 ∼ N 0\sim N 0∼N,所以我们定义的二维dp数组行数为 M M M,列数为 N + 1 N+1 N+1。另外我定义了一个一维数组 w e i g h t weight weight存放物品的质量,再定义一个一维数组 v a l u e value value存放物品的价值,它们的维度都是 M M M。
怎么初始化dp数组呢:
首先是数组的第一行,我们把行索引固定为 0 0 0,列索引 j j j从0递增到 N N N,对应dp数组每一个元素是 d p [ 0 ] [ j ] dp[0][j] dp[0][j],如果此时的 j < w e i g h t [ 0 ] j < weight[0] j<weight[0],意味着背包放不下第一个物品(前一项是背包的容量,后一项是第一个物品的质量),这时候 d p [ 0 ] [ j ] dp[0][j] dp[0][j]赋值为 0 0 0,否则可以放下物品,赋值为 d p [ 0 ] [ i − w e i g h t [ 0 ] ] + v a l u e [ 0 ] dp[0][i - weight[0]] + value[0] dp[0][i−weight[0]]+value[0], d p [ 0 ] [ i − w e i g h t [ 0 ] ] dp[0][i - weight[0]] dp[0][i−weight[0]]是背包容量为 i − w e i g h t [ 0 ] i-weight[0] i−weight[0]的最大价值(拿掉第一种物品的个数为1),这时候再加上第一种物品的价值,就是背包容量为 i i i的最大价值。
接着是数组的第一列,列索引固定为 0 0 0,表示背包的容量为 0 0 0,这时候无论物品的质量是多少,都放不进背包,这一列的所有元素价值都为 0 0 0。
剩余的元素行索引的范围从 1 ∼ M − 1 1\sim M-1 1∼M−1,列索引从 1 ∼ N 1\sim N 1∼N,其dp数组的递推公式为:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j] = max(dp[i - 1][j], dp[i ][j - weight[i]] + value[i]) dp[i][j]=max(dp[i−1][j],dp[i][j−weight[i]]+value[i])
这里可能出现数组越界的情况:
j
−
i
t
e
m
[
i
]
[
0
]
<
0
j - item[i][0]\lt 0
j−item[i][0]<0
所以dp数组的递推公式我们改为(关于这个递推公式的详细推导,我推荐看这个视频,讲得很清楚:【超精细!】动态规划—完全背包问题全面解读!!):
d
p
[
i
]
[
j
]
=
{
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
)
i
f
j
−
w
e
i
g
h
t
[
i
]
≥
0
d
p
[
i
−
1
]
[
j
]
i
f
j
−
w
e
i
g
h
t
[
i
]
<
0
dp[i][j]=\left\{
d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]表示不放入编号为 i i i物品的最大价值(物品编号从 0 ∼ i − 1 0\sim i-1 0∼i−1),而 d p [ i ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j - weight[i]] + value[i]) dp[i][j−weight[i]]+value[i])表示考虑编号为 0 ∼ i 0\sim i 0∼i种物品的情况下背包容量 j j j的最大价值。这两者的分别在于是否放入了编号为 i i i的物品。和0-1背包相比,这个递推式的不同在于:
0-1背包
d
p
[
i
]
[
j
]
=
{
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
)
i
f
j
−
w
e
i
g
h
t
[
i
]
≥
0
d
p
[
i
−
1
]
[
j
]
i
f
j
−
w
e
i
g
h
t
[
i
]
<
0
完全背包
d
p
[
i
]
[
j
]
=
{
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
)
i
f
j
−
w
e
i
g
h
t
[
i
]
≥
0
d
p
[
i
−
1
]
[
j
]
i
f
j
−
w
e
i
g
h
t
[
i
]
<
0
我们看到这个递推公式中 d p [ i ] [ j ] ( 1 < = i < M , 1 < = j < = N ) dp[i][j](1<=i<M, 1<=j<=N) dp[i][j](1<=i<M,1<=j<=N)和它的原始初始值没有关系,我们可以初始化为任意值,为了方便起见我们先把这个dp数组全部赋值为0。然后赋值第一行就可以了。我们的初始化可以写作:
vector<vector<int>> dp(N, vector<int>(V + 1, 0));
for (int i = weight[0]; i <= V; ++i){
dp[0][i] = dp[0][i - weight[0]] + value[0];
}
完整的二维数组代码版本1(先遍历物品再遍历背包):
#include <vector> #include <iostream> using namespace std; int main(){ int N, V; cin >> N >> V; vector<int> value(N); vector<int> weight(N); for (int i = 0; i < N; ++i){ cin >> weight[i] >> value[i]; } vector<vector<int>> dp(N, vector<int>(V + 1, 0)); for (int i = weight[0]; i <= V; ++i){ dp[0][i] = dp[0][i - weight[0]] + value[0]; } for (int i = 1; i < N; ++i){ //先遍历物品,行 for (int j = 1; j <= V; ++j) //后遍历背包,列 { if (j >= weight[i]) dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); else dp[i][j] = dp[i - 1][j]; } } cout << dp[N-1][V]; return 0; }
我们也可以先遍历背包再遍历物品,完整的二维数组代码版本2:
#include <vector> #include <iostream> using namespace std; int main(){ int N, V; cin >> N >> V; vector<int> value(N); vector<int> weight(N); for (int i = 0; i < N; ++i){ cin >> weight[i] >> value[i]; } vector<vector<int>> dp(N, vector<int>(V + 1, 0)); for (int i = weight[0]; i <= V; ++i){ dp[0][i] = dp[0][i - weight[0]] + value[0]; } for (int j = 1; j <= V; ++j){ //先遍历背包,列 for (int i = 1; i < N; ++i) //后遍历物品,行 { if (j >= weight[i]) dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); else dp[i][j] = dp[i - 1][j]; } } cout << dp[N-1][V]; return 0; }
我们也可以使用滚动一维dp数组来解决这个问题。相当于把二维数组做了一个压缩,之前我们做0-1背包问题,我们需要先遍历物品,再遍历背包,并且物品是正序遍历,背包是逆序遍历,而在完全背包问题里因为我们是使用当前行的数据,所以我们需要使用正序遍历。我们的动态规划的递推公式和0-1背包的递推公式是一样的:
d
p
[
j
]
=
{
m
a
x
(
d
p
[
j
]
,
d
p
[
j
−
w
e
i
g
h
t
[
i
]
]
+
v
a
l
u
e
[
i
]
)
i
f
j
−
w
e
i
g
h
t
[
i
]
≥
0
d
p
[
j
]
i
f
j
−
w
e
i
g
h
t
[
i
]
<
0
dp[j] = \left\{
这里的i还是代表物品的编号,j代表背包的容量。
为什么要使用正序遍历背包呢?虽然是一维数组,但是性质和二维背包差不多。从每一个元素往前看,因为是正序遍历,所以前面的元素都被更新过了,看到的都是“上一层的元素”,正序遍历的前提下前面的元素都发生了改变,相当于是二维dp数组当前层的元素,元素使用了多次,符合完全背包的要求。
因为二维数组是根据左测(0-1背包是左上角)元素来求的,而一维数组自然就是靠左侧来求的。正序的时候,左边的元素刚刚刷新过,也就是左边的元素已经是本层的了,意味着什么这样会导致一个物品反复加好几次。和0-1背包不同,完全背包遍历的顺序是可以改变的。
初始化很简单,和0-1背包一样,因为我们要放置把我们的结果忽略掉(可能取 d p [ j − w e i g h t [ i ] ] + v a l u e [ i ] dp[j - weight[i]] + value[i] dp[j−weight[i]]+value[i])我们把一维数组全部初始化为0就可以了。
这里给出代码:
完整的一维滚动数组代码版本1(先遍历物品再遍历背包)
#include <vector> #include <iostream> using namespace std; int main(){ int N, V; cin >> N >> V; vector<int> value(N); vector<int> weight(N); for (int i = 0; i < N; ++i){ cin >> weight[i] >> value[i]; } vector<int> dp(V + 1, 0); for (int i = 0; i < N; ++i){ //先遍历物品,行 for (int j = weight[i]; j <= V; ++j){ //再遍历背包,列 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } cout << dp[V]; return 0; }
完整的一维滚动数组代码版本2(先遍历背包再遍历物品):
```cpp #include <vector> #include <iostream> using namespace std; int main(){ int N, V; cin >> N >> V; vector<int> value(N); vector<int> weight(N); for (int i = 0; i < N; ++i){ cin >> weight[i] >> value[i]; } vector<int> dp(V + 1, 0); for (int j = 0; j <= V; ++j){ //先遍历背包,列 for (int i = 0; i < N; ++i){ //再遍历物品,行 if (j >= weight[i]) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); else dp[j] = dp[j]; } } cout << dp[V]; return 0; }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。