之前看《算法笔记》的时候发现背包问题那里有点难懂,近日看到好多题目都和背包问题有关,特此整理一下:
1 01背包问题
1.1 问题描述:
有n件物品,每件物品的重量是w[i],价值为c[i]。现在有一个容量为V的包,问如何选择物品放入背包中,使得背包中的物品的总价值最大。其中每个物品都只有一件。
注意到,01背包问题的“01”也就体现在每个物品只有一件。
1.2 基本思路:
如果采用暴力枚举的方法,每一件物品都有放或者不放两种情况,所以n件物品有2^n中情况,这种复杂度是十分糟糕的,下面,我们使用动态规划的方法可以将复杂度降为O(nV)。
令dp[i][v]
表示前i件物品(1<=i<=n,0<=v<=V)恰好装入容量为v
的背包中所能获得的最大价值。 此处的恰好理解为: 前i件物品放入容量为v(注意此处不是V),中所能获得的最大价值。
考虑对第i件物品的选择策略:
1。 不放第i个物品,那么问题转化成前i-1件物品恰好装入容量为v的背包中所获得的最大的价值。
2。 放第i和物品, 那么问题转化成前i-1件物品恰好放入v-cost[i]
中所获得的最大的价值。 也即dp[i-1][v-cost[i]]+c[i]
因此,状态转移方程如下:
dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+cost[i])
其中1<=i<=n, w[i]<=v<=V。
边界条件是dp[0][v]=0,即前0件物品放入任何容量的背包中所获得的的最大价值都是0。
注意到, 当前的dp[i][v]
只与前一个状态dp[i][]有关,所以,可以枚举i从1到n,v从0到V,就可以获得整个数组。
因此可以写出如下代码:
for (int i = 1; i <= n; i++) {
for (int v = w[i]; v <= V; v++) {
dp[i][v] = max(dp[i - 1][v], dp[i - 1][v - w[i]] + cost[i]);
}
}
这个算法的时间算法复杂度为O(nV)
对于这个算法, 有如下样例:
对于给定的w和cost数组:
该算法的过程如下:
1.3 优化
注意到上面的状态转移方程中计算的dp[i][v]
时总是需要使用dp[i-1][v]
左侧部分的数据,且当计算dp[i+1][v]
的时候又用不到dp[i-1]
了, 因此不放将dp[i][v]
数组进行降维处理, 降维为dp[v]
, 并且将枚举方向改变为i从1到n,v从V到w[i]
(注意这里和之前的方法有所不同, 这里逆序了)
这样的话, 状态转移方程就改变为:
dp[v]=max(dp[v-1],dp[v-w[i]]+cost[i])
可以写出代码如下:
for (int i = 1; i <= n; i++) {
for (int v = V; v >= w[i]; v--) {
dp[v]=max(dp[v-1],dp[v-w[i]]+cost[i]);
}
}
那么上述优化后的程序运行过程如下:
这样修改的话可以这么理解:v的枚举顺序变为从右到左, dp[i][v]
右边的部分为刚计算过的需要保存的给下一行使用的数据, 而dp[i][v]
左上角的阴影部分为当前需要使用的部分。 将两者结合一下,即把dp[i][v]
左上角和右边的部分放在一个数组里, 计算出每一个dp[i][v]
,就相当于把dp[i-1][v]
抹消, 因为在后面的运算中dp[i-1][v]
再也用不到了。称这种技巧为滚动数组
2 完全背包问题
问题描述:
有n件物品,每件物品的重量是w[i],价值为c[i]。现在有一个容量为V的包,问如何选择物品放入背包中,使得背包中的物品的总价值最大。其中每个物品都都有无穷件。
和01背包问题同样分析: 首先,令dp[i][v]
表示前i件物品恰好放入容量为v的包内所获得的最大值。 和01背包问题一样,这也有两种策略, 但是也有不同,对于第i件物品:
- 不放第i个物品, 那么和01背包问题相同,
dp[i][v]=dp[i-1][v]
- 如果放第i个物品, 那么与01背包问题不同, 因为01背包问题中如果选择物品i,那么就转移到
dp[i-1][v-w[i]]
这个状态; 但是完全背包问题不同, 完全背包问题选择第i件物品后不是直接转移到dp[i-1][v-w[i]]
这个状态,而是转移到dp[i][v-w[i]]
;这是因为每个物品可以放任意个,也就是放了第i件物品之后还可以放第i个物品, 知道第二维的v-w[i]
无法满足大于零的条件为止。
由上面的分析可以写出如下的状态转移方程:
dp[i][v]=max(dp[i-1][v],dp[i][v-w[i]]+cost[i])
1<=i<=n,w[i]<=v<=V
边界条件: dp[0][v]=0; 0<=v<=n
同样地,这个方程也可以改成一维的形式:
dp[v]=max(dp[v-1],dp[v-w[i]]+cost[i])
1<=i<=n,w[i]<=v<=V
边界条件: dp[0][v]=0; 0<=v<=n
这里注意到,完全背包问题写成一维的形式之后, 和01背包问题的状态转移方程完全相同。 区别是这里的枚举方式变成了正向枚举(01背包是逆向枚举),代码如下:
for (int i = 1; i <= n; i++) {
for (int v = w[i]; v <=V ; v++) {
dp[v]=max(dp[v-1],dp[v-w[i]]+cost[i]);
}
}
具体的运行过程这里不做描述。
关于逆向和正向枚举, 简单一句话就是无后效性, 本人水平有限也不在此解释,待以后有更好的方法之后再来填坑。
讲到这里, 还有一些初始化的时候的细节问题,在背包问题中, 有两种不同的问法, 第一种是要求"恰好装满背包", 第二种没有要求必须把背包装满。 对于这两种不同的问法的解决方案也就是初始化dp数组的时候有所不同。
- 对于"恰好装满背包",这种问题, 那么初始化的时候除了dp[0]=0,其他的dp[1…V]都设置为负无穷,这样的话就可以保证dp[V]是一种恰好装满的最优解。
- 对于没有要求必须装满的问题, 而是只希望价格尽量大,初始化时应该将dp[1…V]都设为0。
对于以上这两种初始化方法, 可以这么理解: 初始化的dp数组实际上就是在没有任何物品可以放入背包时的合法状态。 如果要求背包恰好装满, 那么此时只有容量为0的背包可以在什么也不装的情况下被"恰好装满了",其他容量的背包均没有合法解,也就是dp[0]=0
, dp[1...V]=负无穷
。 相应的如果背包不是必须要装满的话, 那么任何容量的背包都有一个"什么都不装"的合法解, 虽然这个合法解可能不是最优的, 所以这个时候就有dp[0...V]=0
。