题目

有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。(这个是完全背包类问题的主要特点)

第 i 种物品的体积是 vi,价值是 wi。

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0<N,V≤1000
0<vi,wi≤1000

输入样例

1
2
3
4
5
4 5
1 2
2 4
3 4
4 5

输出样例:

1
10

代码

最直白代码

这个代码在提交时会超时,但是对于理解很直观:

它的主要思想就是对于 i,j 位置遍历所有拿 i 的个数 k0 <= k <= j/v[i])的情况得到最优的价值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N], dp[N][N];

int main () {
// 输入数据
cin >> n >> m;
for (int i = 1; i <= n ; ++i) cin >> v[i] >> w[i];

for (int i = 1; i <= n ; ++i) {
for (int j = 0; j <= m; ++j) {
for (int k = 0; k * v[i] <= j; ++k) // 判断拿k个时的大小,选择最大的
dp[i][j] = max(dp[i][j], dp[i-1][j - v[i]*k] + w[i]*k);
}
}
cout << dp[n][m] << endl;
return 0;
}

优化为两重循环

因为我们是按照读入的顺序,进行DP的,所以其实可以在读入数据时,就进行DP。例如读入第 i 个物品的体积和价值,那么我们就去把对于第 i 个物品的所有价值的情况遍历一次,这个时候就不需要v数组,w数组了,只需要两个变量即可。

真正实现状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i][j-v] + w)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int dp[N][N];

int main(){
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
int v, w;
cin >> v >> w;
for(int j = 0; j <= m; ++j){
dp[i][j] = dp[i - 1][j];
if(j >= v)
dp[i][j] = max(dp[i - 1][j], dp[i][j - v] + w);
}
}
cout << dp[n][m] << endl;
}

再优化空间复杂度

很多时候的DP问题其实都可以优化空间,因为一般状态转移方程都只和几个状态有关,例如最基础的爬楼梯问题,就只是需要前面两个状态,最终可由数组变成三个变量,即 O(n) => O(1)。

同理这里也可以降低纬度:我们发现当前状态只与上一个状态有关,这里的上一个状态泛指还没有这个物品时的各种容量的状态。那么很容易想到:用两个数组来滚动存储状态,A数组存有n-1个物体时的各种容量的状态,B数组就是当前n个物体的状态。

但是这样不好写代码,所以用到奇偶的思想。i 为奇数时有 A 数组,i 为偶数时用 B 数组。

看代码理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int dp[2][N];

int main() {
cin >> n >> m;
for(int i = 1; i <= n; ++i){
int v, w;
cin >> v >> w;
for(int j = 0; j <= m; ++j){
dp[i & 1][j] = dp[(i - 1) & 1][j];
if(j >= v)
dp[i & 1][j] = max(dp[i & 1][j], dp[i & 1][j - v] + w);
}
}
cout << dp[n & 1][m] << endl;
}

所以空间复杂度从O(n^2) 变成了 O(n)

优化最终版代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>

using namespace std;

const int N = 1010;

int n, m;
int dp[N];

int main() {
cin >> n >> m;
for(int i = 1; i <= n; ++i) {
int v, w;
cin >> v >> w;
for(int j = v; j <= m; ++j)
dp[j] = max(dp[j], dp[j - v] + w);
}
cout << dp[m] << endl;
}

这里注意先看下标,逐步理解:

我们在01背包时没有做优化,那就用最直观的未优化版本来对比:

dp[i][j] = max(dp[i][j], dp[i-1][j-v[i]]+w[i]);//01背包

dp[i][j] = max(dp[i][j], dp[i][j-v[i]]+w[i]);//完全背包问题

可以看出来唯一的区别就是在状态转移方程后面,如果要使用的状态变了,变成了dp[i][j-v[i]] + w[i]

很简单,不用上个物品,容量为j-v[i]的状态了,就用前面算出来的当前物品,容量为j-v[i]的状态(容量是从小到大算出来的,所以j-v[i]容量时一定是计算了的)

这里理解之后就是套用上面的空间优化思路:然后发现状态转移方程既然已经和上一个物品状态无关了,那么也没有必要两个数组了,最终一个数组搞定。