背包九讲的总结笔记(一)

基础的01背包

题目:

有N件物品和一个容量为V的背包。放入第i件物品耗费的空间是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值总和最大。

基本思路:

F[i,v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:F[i,v]=max{F[i−1,v],F[i−1,v−Ci]+Wi}
然后根据这个方程便可以写出伪代码,具体代码最好根据不同题目具体要求来写,过度依赖源码很容易犯错。
F[0, 0...V] <- 0
for i <- 1 to N
	for v <- Ci to V
		F[i, v] <- max(F[i-1, v], F[i-1, v-Ci] + Wi)

优化空间复杂度:

其实可以省去状态里面的一维。具体原理如下:F[i, v]是由F[i-1, v]和F[i-1, v-Ci]两个子问题递推而来,所以只需在主循环中以 v <- V...Ci的方式来递推的话,就能够保证使用的是上一组的F[v],F[i-1]。具体伪代码如下:
F[0...V] <- 0
for i <- 1 to N
	for v <- V to Ci
		F[v] <- max(F[v], F[v-Ci] + Wi)

初始化的细节:

求最优解的背包类型题目中,有两种类似的问法。一种是问“ 恰好装满背包”的最优解,有的只要求“ 不超过背包容量即可”。
如果是 要求恰好装满的问法时候,那么在初始化时候只有F[0]被初始化为0,其他的状态都应该为未定义状态-INF。如果 是后者的问法时,应该将F[0...V]全部初始化成0。可以这样理解:初始化的F数组实际上就是什么都不放的情况下的合法状态,在第一种问法下很明显只有F[0]才满足什么都不装的情况下被“装满”,而后者的问法没有要求必须把背包装满,所以什么都不装也是合法的! 可以将这个初始化技巧推广到其它类型的背包问题

完全背包问题

题目:

有N种物品和一个容量为V的背包,每种物品都有无限件可用。放入第i种物品的耗费的空间是Ci,得到的价值是Wi。求解:将哪些物品装入背包,可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

基本思路:

一种最容易想到的思路就是用01背包的思路来求解,状态定义与01背包一样。方程为:F[i,v]=max{F[i−1,v−kCi]+kWi|0≤kCi≤v}。k表示第i中物品选多少个。可以将伪代码描述如下。
F[0, 0...V] <- 0
for i <- 1 to N
	for v <- Ci to V
		for k <- 0 to V/Ci
			F[i, v] <- max(F[i-1, v], F[i-1, v-k*Ci] + k*Wi)
但是这个算法复杂度有点高,复杂度大概为O(VN * sum(V/Ci))。用于题目中很可能超时,所以要给出优化。

对转化成01背包算法的优化

  • 第一个很容易想到的优化,实现起来也比较简单。假设有N种物品里面有a和b两种物品存在关系:C[a] > C[b] && W[a] < W[b],很显然就可以将a物品排除掉,因为选择一个代价高且获益少的方案肯定不如代价少获益高的方案。
  • 第二个优化方案有点小复杂,需要用到2进制的思想。考虑每一个物品,虽然说有无限件可以使用,但是最多选择的个数肯定不超过V/C[i],根据这一隐含条件就可以采取2进制方法将其拆分为01背包中的物品(即每种物品只有一件)。举个例子:将编号为i的物品拆分,最多有m=V/C[i]个,可以将其拆分成代价为2^k*C[i]和获益为2^k*W[i]的一些物品,其中k取遍所有2^k*C[i] <= V的整数。

完全背包的O(VN)解法

首先给出伪代码:
F[0...V] <- 0

for i <- 1 to N
	for j <- Ci to V
		F[j] <- max(F[j], F[j - Ci] + Wi)
和01背包的滚动数组方法很像,只有循环顺序不一样。
对于以上的这份伪代码,可以这样理解:01背包之所以选择逆序来遍历第二重循环,是为了 保证在求F[j]时,用的是上一次所更新的F值。换个思路,也就是说是为了让每个物品能够恰好被选入一次!如果不是逆序来遍历的话,很可能将已经选入的物品再次选入。
所以类比一下完全背包,完全背包需要的正是可以将已经选入的物品再次选入的性质,所以采取了这种写法!

多重背包问题

题目:

有N种物品和一个容量为V的背包。第i种物品最多有Mi件可以用,每件耗费的空间是Ci,价值是Wi。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

基本思路:

最基本的思路还是转换成01背包问题来求解。具体状态和转移方程和完全背包大同小异。具体的伪代码也和完全背包一样,只不过将V/Ci代替成Mi而已,不再赘述。

基本思路的优化:

在完全背包里面已经说明了这种最朴素的算法复杂的比较高,所以还是依照 完全背包的思路来优化该算法。但是除了完全背包提供的那两个优化思路外,对于多重背包其实还可以再加上一个思路:考虑对于物品i, 如果有C[i]*M[i]>=V,则可以使用完全背包的求法来对这一种物品求解!这个思路的正确性也是显而易见的。如果这类物品的所有耗费大于或者等于容量限制,则不论怎么选也不会超过这类物品的数量C[i],所以相当于数量是无限的!伪代码如下:
F[0...V] <- 0

for i <- 1 to N
	if(C[i] * M[i] >= V) {	//完全背包的O(VN)解法
		for j <- C[i] to V
			F[j] = max(F[j], F[j - C[i]] + W[i])
	}
	else {	//2进制拆分成01背包
		k <- 1
		while(k < M[i]) {
			for j <- V to k * C[i]
				F[j] = max(F[j], F[j - k * C[i]] + k * W[i])
			M[i] -= k;
			k *= 2;
		}
		for j <- V to M[i] * C[i]	//注意最后还有一次
			F[j] = max(F[j], F[j - M[i] * C[i]] + M[i] * W[i])
	}

可行性问题的O(VN)解法

若问题只是问是否能够装满容量为j的背包,则有一个复杂度为O(VN)的解法。设dp[i][j]为用前i种物品填满容量为j的背包后,最多还剩下多少种i类物品。根据状态不难定义出转移方程。若dp[i-1][j]>=0,说明用前i-1种物品就可以填满j容量的物品,i物品可以一个不用,所有此时dp[i][j] = M[i]。若j < C[i],或者dp[i - 1][j - C[i]] < 0,说明用前i-1种物品无法凑成容量j-C[i]的背包,则再加上第i种背包任然无法凑成容量为j的背包,所以此时d[i][j] = -1。若是前i-1种能够凑出容量j - C[i]的背包,则再加上1个i类物品就可以凑成容量j的背包,有dp[i][j] = dp[i-1][j-C[i]] - 1。伪代码描述如下(为了节省空间,使用了滚动数组):
dp[0...V] = -1
dp[0] = 0;
for i <- 1 to N
	for j <- 1 to V
		if(dp[j] >= 0)							dp[j] = M[i]
		else if(j < C[i] || dp[j - C[i]] <= 0)	dp[j] = -1
		else									dp[j] = max(dp[j], dp[j - C[i]] - 1)
		
if(dp[V] >= 0)	return true
else			return false






你可能感兴趣的:(理论性总结)