题目
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。(这个是完全背包类问题的主要特点)
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
输出样例:
代码
最直白代码
这个代码在提交时会超时,但是对于理解很直观:
它的主要思想就是对于 i,j
位置遍历所有拿 i
的个数 k
(0 <= 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) 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]
容量时一定是计算了的)
这里理解之后就是套用上面的空间优化思路:然后发现状态转移方程既然已经和上一个物品状态无关了,那么也没有必要两个数组了,最终一个数组搞定。