背包问题

之前看《算法笔记》的时候发现背包问题那里有点难懂,近日看到好多题目都和背包问题有关,特此整理一下:

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件物品:

  1. 不放第i个物品, 那么和01背包问题相同, dp[i][v]=dp[i-1][v]
  2. 如果放第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]=0dp[1...V]=负无穷 。 相应的如果背包不是必须要装满的话, 那么任何容量的背包都有一个"什么都不装"的合法解, 虽然这个合法解可能不是最优的, 所以这个时候就有dp[0...V]=0

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值