算法之动态规划(DP)求解01背包问题
上面这篇文章主要讲解了01背包问题和动态规划算法,如果你不了解动态规划算法,建议先浏览一下这篇文章熟悉一下,因为,本文的算法思想是基于这篇文章的。
01背包问题:有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品只能取1次放入背包中,背包所有物品权重和最大是多少?
完全背包 :有一个背包的容积为V,有N个物品,每个物品的体积为v[i],权重为w[i],每个物品可以取无限次放入背包中,背包所有物品权重和最大是多少?
01背包问题和完全背包问题的区别就在于,每个物品取的最大次数是1次还是无限次。
参考01背包的状态和转移方程,来推导完全背包的状态和转移方程。
01背包问题的状态和转移方程:
状态: f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移:f[i][j] = max(f[i-1][j-v[i]]+w[i], f[i-1][j])
现在物品可以取无数次,那么f[i][j]怎么构造合适呢?
完全背包,有N个物品,背包容量V,最终求解背包的最大权重,因此,完全背包和 01背包这些都一致,因此,状态一致。f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移方程如何构造呢?
01背包问题每次增加第i物品时,因为只能取一次,从j中减去v[i],再加上w[i],这个操作只进行了一次。现在可以增加无数次,大家想想,怎么做呢?
时间到,聪明的朋友肯定想到了,第i个物品可以增加0次(即f[i-1][j]),增加1次,增加2次,增加3次…增加k-1次,(这里k*v[i] > j),然后求所有值的最大值作为f[i][j]的值。
因此我们可以写出状态和状态转移方程:
状态: f[i][j] 选择前i个物品,体积为j时的最优方案,即所选物品的最大权重和。
状态转移:f[i][j] = max(f[i-1][j- k * v[i]]+ k * w[i] (k= 0, 1, 2, 3, 4,...))
恭喜你,完全背包问题已经被你攻克了,下面来看一下代码吧。
练习题:完全背包问题
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。
第 i 种物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
程序:
白银级代码
#include<stdio.h>
int f[1050][1050];
int v[1050];
int w[1050];
int max(int a, int b)
{
if (a > b) {
return a;
}
return b;
}
int main()
{
int n,m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &v[i], &w[i]);
}
for (int i = 1; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ) {
f[i][j] = f[i - 1][j];
if (v[i] > j) { // 防止数组越界
continue;
}
for (int k = 0; k * v[i] <= j; k++) { // 添加k个物品i
f[i][j] = max(f[i][j], f[i - 1][j - k * v[i]] + k * w[i]);
}
}
printf("%d\n", f[n][m]);
}
作为一个研究算法的coding farmer,不对算法精益求精的进行优化,那就不叫学习算法。
如果你是高手,肯定发现了上面代码中有很多可以优化的地方,不妨花3分钟尝试找一下优化点吧。
优化点:
黄金级:
1)if (v[i] > j) 可以优化到 for (int j = 1; j <= m; j ++ ) 中,优化后为:
for (int j = v[i]; j <= m; j ++ ) {
......
}
2)w[] 和 v[]可以优化掉。代码中的v和w数组使用仅限于i状态下,因此,可以定义两个变量,v和w,在for (int i = 1; i <= n; i ++ )中输入即可,优化后:
for (int i = 1; i <= n; i ++ ) {
int v, w;
scanf("%d %d", &v, &w);
}
铂金级:
二维数组f可以优化成一维的f,具体优化可以参考这篇文章,这里不过多介绍。
王者级::
从状态转移方程下手:f[i][j] = max(f[i-1][j- k * v[i]]+ k * w[i] (k= 0, 1, 2, 3, 4,…))
方程拆开:
f[i][j] = max(f[i-1][j], f[i-1][j - v[i]]+w[i], f[i-1][j-2*v[i]]+2*w[i], f[i-1][j-3*v[i]]+3*w[i] ,......)
使用代入法,将j= j-v[i]带入上面的方程中得到:
f[i][j-v[i]] = max(f[i-1][j-v[i]], f[i-1][j - 2*v[i]]+w[i], f[i-1][j-3*v[i]]+2*w[i], f[i-1][j-3*v[i]]+3*w[i] ,......)
对比第二个和第一个方程,我们发现,方程1可以简化成:
f[i][j] = max(f[i-1][j], f[i][j - v[i]] + w[i])
怎么看起来跟01背包模型的好像啊,我们对比一下:
f[i][j] = max(f[i-1][j], f[i-1][j - v[i]] + w[i]) // 01背包
f[i][j] = max(f[i-1][j], f[i ][j - v[i]] + w[i]) // 完全背包
发现了区别在下标从i-1变为i。为什么呢?
f[i][j - v[i]] 已经将除去1个物品i时的所有最优解已经求出来了,因此在计算f[i][j]时,无需再重复计算k=2,3,4,5…时的值了。
好了,上最终的代码了:
#include
int f[1050];
int main()
{
int n,m;
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++ ) {
int v,w;
scanf("%d %d", &v, &w);
for (int j = v; j <= m; j++ ) {
f[j] = (f[j] > f[j - v] + w) ? f[j] :f[j - v] + w;
}
}
printf("%d\n", f[m]);
return 0;
}