01背包
问题描述
有 n n n 件物品和一个容量为 V V V 的背包。第i件物品的体积是 v i v_i vi,价值是 c i c_i ci。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。
数据规模
n
≤
100
n \le 100
n≤100
1
≤
v
i
,
c
i
≤
100
1 \le v_i, c_i \le 100
1≤vi,ci≤100
V
≤
10000
V \le 10000
V≤10000
样例输入
n
=
4
n = 4
n=4
(
v
,
c
)
=
{
(
2
,
3
)
,
(
1
,
2
)
,
(
3
,
4
)
,
(
2
,
2
)
}
(v, c) = \{ (2, 3), (1, 2), (3, 4), (2, 2) \}
(v,c)={(2,3),(1,2),(3,4),(2,2)}
V
=
5
V = 5
V=5
样例输出
7 7 7
问题求解
n n n 件物品,每一件都可以选或不选,所以最直接的想法就是搜索,结束条件是体积小于等于0,这样算法的时间复杂度就是 O ( 2 n ) O(2^n) O(2n),以这题的数据规模来说肯定是不行的,所以要降复杂度。跟踪函数的每一次调用,可以发现,有很多的重复计算,在这种情况下,我们考虑动态规划求解,把已经得出的结果存下来,供后面计算使用。
现在想象有 V V V个背包,以 j ( 1 ≤ j ≤ V ) j(1 \le j \le V) j(1≤j≤V) 表示第 j j j 个背包,第 j j j 个背包的体积是 j j j ,我们将 n n n 个物品依次放入 V V V 个背包,所以需要一个二维数组,二维数组定义如下:
const int N = 5e3;
int dp[N][N];
定义了一个dp
全局数组,数组里的每一项都初始化为0。dp[i][j]
的意义是:对于第
i
i
i 件物品,第
j
j
j 个背包能获得的最大价值。
为了表示 v i v_i vi 和 c i c_i ci,我们再定义两个数组。
const int N = 5e3;
int dp[N][N];
//v[i]表示第i件物品的体积,1 <= i <= n
//c[i]表示第i件物品的价值,1 <= i <= n
int v[N];
int c[N];
现在开始放第一个物品,对于第一件物品,我们将它依次放入第
j
j
j个背包,上面的样例输入第一件物品的体积是2,价值是3,即 v[1] = 2
,c[1] = 3
,第1个背包的体积为1,1 < v[1]
,不能放在第1个背包,所以 dp[1][1]
的值是0;第2个背包的体积为2,2 = v[1]
,可以放在第2个背包,dp[1][2]
的值变成 c[1]
;从第三个背包开始都有 j > v[1]
,所以第1件物品可以放入从第三个背包开始的所有背包,所以有dp[1][j]=c[1]
i \ j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 3 | 3 | 3 | 3 |
现在,我们已经记录下了放第1个物品时,每个背包容量所能达到的最大价值,假设现在我想知道背包的容量是3时,放第1个物品能获得多少价值呢?很简单,dp[1][3]
就是答案。
放完第1个物品后,我们开始放第2个物品,v[2] = 1
,c[2] = 2
,同样从第1个背包开始,第1个背包的体积为1,1 < v[2]
,可以放在第1个背包,但是放之前我们就要考虑了,在这里是放第2件物品获得的价值大,还是不放获得的价值大呢?就第2个物品,第1个背包而言,不放物品获得的价值我们已经在前面放第1个物品时求出来了,就是dp[1][1]
;那如果放第2个物品呢?要想放第2个物品,背包首先要给第二个物品腾出空,这样背包就只剩下1 - v[2]
的空间来放其它物品了,再想一下,其他物品是什么呢?显然就是第2个物品前面的物品了,只有物品1,第一个物品的状态我们已经记录下来了,直接拿来用就行了,所以放完物品后剩余的空间所能获得的最大价值是dp[1][1-v[2]]
,加上第2件物品的价值就得到了将第2件物品放入第1个背包后能获得的价值,也就是dp[1][1-v[2]] + c[2]
。dp[1][1]
和dp[1][1-v[2]] + c[2]
取最大值,即为放第2个物品时,第1个背包所能达到的最大价值。
讲到这里我们就要讲一下dp[0][j]
和dp[i][0]
的意义了,看一下dp[1][1-v[1]] + c[2]
,1-v[1] = 1 - 1 = 0
,dp[1][0]
是多少呢?前面说过dp
是一个全局数组,初始化为0,所以dp[1][0] = 0
,这合理吗?dp[1][0]
代表什么意义呢?它表示容量为0的背包放第1件物品所能获得的最大价值,背包容量为0,什么也不能放,dp[1][0]=0
合理,将1推广到
i
i
i,dp[i][0]=0
合理。再来看 dp[0][j]
, 放第1个物品时,我们来看dp[1][2]
,就像放第2个物品时一样,我们会从dp[1][j]
和dp[1][j-v[2]] + c[2]
中选取一个最大值(当然前提是j>=v[2]
,如果j<v[2]
,就只能放第1个物品了,也就是说只取dp[1][j]
),放第1个物品时也会比较dp[0][j]
和dp[0][j-v[1]] + c[1]
的值,这么比较有意义吗?首先看不放第一个物品时的情况:dp[0][j]=0
,合理;再来看放第一个物品时的情况dp[0][j-v[1]] + c[1]=0+c[1]=c[1]
,合理。所以dp[0][j]
和dp[i][0]
的值为0时初始条件,并且这个初始条件的设置是合理的。
继续放第2个物品,j = 2
时,可以放第2个物品,比较dp[1][2]=3
和dp[1][2-v[2]] + c[2]=dp[1][1] + c[2]=0 + 2=2
,所以我们选择不放第2个物品。如果选择放第2个物品,那么放完后剩余的体积所能获得的最大价值加上放第2个物品所获得的价值相加并不比不放第2个物品所获得的价值大。
接着放第3个物品,dp[1][3]=3
和dp[1][3-v[2]] + c[2]=dp[1][2] + c[2]=3 + 2=5
,选择放第2个物品。因为放完第2个物品后剩余2容量,前面已经求出dp[1][2] = 3
,所以第3个背包可以获得的最大价值是5。
同理,我们可以获得放第2个物品时,dp
数组的值
i \ j | 0 | 1 | 2 | 3 | 4 | 5 |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 3 | 3 | 3 | 3 |
2 | 0 | 2 | 5 | 5 | 5 | 5 |
讲解到这,我们可以得出一个递推公式了:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
,
j < v[i]
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
[
i
]
]
+
c
[
i
]
)
,
j >= v[i]
dp[i][j] = \begin{cases} dp[i-1][j], & \text{j < v[i]} \\ \max(dp[i - 1][j],dp[i - 1][j - v[i]] + c[i]), & \text{j >= v[i]} \end{cases}
dp[i][j]={dp[i−1][j],max(dp[i−1][j],dp[i−1][j−v[i]]+c[i]),j < v[i]j >= v[i]
利用这个递推公式我们可以写出解决这个问题的程序:
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
const int N = 5e3;
int c[N];
int v[N];
int dp[N][N];
int n, V;
void solve() {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
if (j < v[i]) {
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - v[i]] + c[i]);
}
}
}
}
int main(int argc, char** argv) {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> c[i];
}
cin >> V;
solve();
cout << dp[n][V] << endl;
return 0;
}
时间复杂度为 O ( n W ) O(nW) O(nW),完全可以应对这一题的数据量。
完全背包
问题描述
有 n n n 件物品和一个容量为 V V V 的背包。第i件物品的体积是 v i v_i vi,价值是 c i c_i ci。求解将哪些物品装入背包可使这些物品的体积总和不超过背包容量,且价值总和最大。在这里,可以重复选取同一个物品若干次。
数据规模
n
≤
100
n \le 100
n≤100
1
≤
v
i
,
c
i
≤
100
1 \le v_i, c_i \le 100
1≤vi,ci≤100
V
≤
10000
V \le 10000
V≤10000
问题求解
和01背包相比,完全背包可以重复选取同一个物品,理所当然的,我们可以枚举所有的情况,我们来讨论向第j
个背包放第i
个物品,首先看不放的情况,接着看放1个的情况(如果背包容量够的话),然后是第3个、第4个…
可以在01背包的基础上加一层循环实现(只给出关键代码,其余一样):
void solve() {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
for (int k = 0; k * v[i] <= j; ++k) {
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * c[i]);
}
}
}
}
最坏情况下,算法的复杂度能达到 O ( n M M ) O(nMM) O(nMM),就这题而言,达到了 1 0 10 10^ {10} 1010级,解决不了这一题。
所以还需要优化,观察有没有哪里重复计算了,我们以计算dp[3][5]
为例(且不管输入是什么),假设v[3] = 2
,那么,会从dp[2][5]
、dp[2][3] + c[3]
、dp[2][1] + 2 * c[3]
中选一个最大值作为dp[3][5]
的值。
接着看dp[3][7]
,会从dp[2][7]
、dp[2][5] + c[3]
、dp[2][3] + 2 * c[3]
、dp[2][1] + 3 * c[3]
中选一个最大值,
可以看到求dp[3][7]
时,抛去dp[2][7]
不谈,它的选择方案和之前求dp[3][5]
时选取的几组数每个都正好相差c[3]
,推广到全体,我们可以用如下递推关系求解:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
,
j < v[i]
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
]
[
j
−
v
[
i
]
]
+
c
[
i
]
)
,
j >= v[i]
dp[i][j] = \begin{cases} dp[i-1][j], & \text{j < v[i]} \\ \max(dp[i - 1][j],dp[i][j - v[i]] + c[i]), & \text{j >= v[i]} \end{cases}
dp[i][j]={dp[i−1][j],max(dp[i−1][j],dp[i][j−v[i]]+c[i]),j < v[i]j >= v[i]
注意
j
>
=
v
[
i
]
j >= v[i]
j>=v[i] 时,
d
p
[
i
]
[
j
−
v
[
i
]
]
+
c
[
i
]
dp[i][j - v[i]] + c[i]
dp[i][j−v[i]]+c[i] 中求的是dp[i][j - v[i]]
,也就是说会利用前面求将第 i
个物品放入 j - v[i]
时的记录值(这个前面已经推过了)。
根据这个递推式,我们可以写出如下程序:
#include <iostream>
#include <fstream>
#include <algorithm>
using namespace std;
const int N = 5e3;
int c[N];
int v[N];
int dp[N][N];
int n, V;
void solve() {
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= V; ++j) {
if (v[i] > j) {
dp[i][j] = dp[i - 1][j];
}
else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - v[i]] + c[i]);
}
}
}
}
int main(int argc, char** argv) {
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> v[i] >> c[i];
}
cin >> V;
solve();
cout << dp[n][V] << endl;
return 0;
}
优化之后,算法的时间复杂度变为 O ( n M ) O(nM) O(nM),完全可以应对题目的数据量。
总结
动态规划是一种记录结果再利用的算法,观察01背包和完全背包的递推式,可以看到就只有 i
和 i - 1
的区别,两种算法使用前面记录的数据不同,01背包只利用前面一组数据,而完全背包不只会利用前面一组数据,还会利用同一组数据。利用它们的这些特性还能使用一维数组替代二维数组。