老生常谈-01背包

        01背包这个问题,已经被表的不能再表了。然而为了加深理解,我还是要再把他拖出来表一次。

每个学DP的人,必然要接触到01背包。01背包这个问题的意义也像他的名字——从0到1,从不会到理解动态规划。这一步,天资聪颖的可能要几天,略微愚钝的可能就要十天半个月来理解,因此也可以说,01背包是新手进阶动态规划问题的第一个门槛。

         01背包问题,简而言之,就是给一个容量V有限的背包,和若干个有体积Vi和价值Wi的物品,要求从物品中选择若干个填充入背包,使背包物品的总价值最大。

        第一次接触到这种题目,很容易会想到使用贪心策略。对每个物品,按照价值/体积(性价比)进行排序,然后从高到底选择——然而这么做,非常容易陷入贪心算法的陷阱。很简单的数据,设背包容量为10,a,b,c三个物品的体积和价值分别是1 2,1 3,10,6;按照贪心算法,会选择a和b做最优解,很显然,最优解是只放c这个物品。

        那么可不可以用回溯来计算呢?答案是可以的。每次对当前物品进行分析,如果可行那么放入背包,并将该物品作为左节点,继续向下遍历。当遍历到不可行结果时,回溯再遍历右节点。当所有结点遍历完成,取值最大的解为最优解。然而这种算法效率堪忧,若物品的数量非常大,那回溯时需要的内存开销和时间开销也是惊人的。

        那么,假设你正在进行这个操作,你的面前有一个背包,若干物品,你会怎么放呢?

        不难想象,我们会拿起一件物品,放入背包,然后拿起第二件物品进行比对。直到我们将背包填到一个快要满的值的时候,我们拿起新的物品和背包中已有的物品作比较,然后选择放入或者丢弃——bingo!背包问题,特点就是每一件物品的状态,只有”放入“和”不放入“,而是否放入这件物品,只取决于”放入上一件物品后的状态“。

        现在把这段话抽象一下,用F[i][j]表示前i件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值。根据第i件物品放或不放进行决策

                                               

        其中F[i-1][j]表示前i-1件物品中选取若干件物品放入剩余空间为j的背包中所能得到的最大价值;

        而F[i-1][j-C[i]]+W[i]表示前i-1件物品中选取若干件物品放入剩余空间为j-C[i]的背包中所能取得的最大价值加上第i件物品的价值。

        根据第i件物品放或是不放确定遍历到第i件物品时的状态F[i][j]。

        设物品件数为N,背包容量为V,第i件物品体积为C[i],第i件物品价值为W[i]。

用大白话讲,什么意思呢?就是,你这件物品放不放,取决于你上一件物品放不放,你上一件物品放不放,取决于你上上一件物品放不放……直到问题转移到第一件物品放不放,然后根据第一件的状态,可以求得最终的状态。

我们将状态,保存在一个大小为N*V的数组中。N是物品数,V是体积,该数组的意义如其名字——当放入N件物品时,容积剩余为V的背包能放物品的最大价值。这种算法,时间复杂度和空间复杂度都是O(NV)。时间复杂度上,经过严(bie)格(ren)的证明,已经被证明是基本不能再优化的。然而空间复杂度却可以继续优化。

如何优化?先写一写上面的代码吧。

for(int i = 1;i<n+1;i++)
{
	for(int j = 1;j<m+1;j++)
	{
		//当物品为i件重量为j时,如果第i件的重量(w[i-1])小于重量j时,c[i][j]为下列两种情况之一:
		//(1)物品i不放入背包中,所以c[i][j]为c[i-1][j]的值
		//(2)物品i放入背包中,则背包剩余重量为j-w[i-1],所以c[i][j]为c[i-1][j-w[i-1]]的值加上当前物品i的价值
		if(w[i-1]<=j)
		{
			if(c[i-1][j]<(c[i-1][j-w[i-1]]+p[i-1]))
			c[i][j] = c[i-1][j-w[i-1]]+p[i-1];
			else
			c[i][j] = c[i-1][j];
		}
		else
		c[i][j] = c[i-1][j];
	}
}

        可以看到,影响时间的是两层循环,1-N和1-V(M)的循环。而f[i][v]=max(f[i-1][v],f[i-1][v-c[i]]+w[i])这一句,就是我们需要的状态转移方程。画两个表吧。

表1-1背包问题数据表

物品号i123456
体积C231465
价值W5651197

 

表1-2前i件物品选若干件放入空间为j的背包中得到的最大价值表


012345678910
000000000000
100555555555
2056611111111111111
30551011111616161616
40551011111616161617
50551011111924242930
60551011111924242930

        从表中可以看出,每一行,我们只需要一个状态,然后就跳跃进了下一列。另外n-1个状态都是没用的,并没有储存什么东西。也就是说,我们的状态数组其实可以压缩到f[v]这样的一个数组中。如何压缩呢?f[i][v]是由f[i-1][v]f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。再看一次状态转移方程吧,i-1有什么卵用么?似乎并没有。

于是我们得到了新的转移方程,f[v]=max{f[v],f[v-c[i]]}。代码如下:

for (i=1;i<=n;i++)
		for (j=t;j>=m[i];j--)
		{
			if (f[j]<f[j-m[i]]+w[i]) 
				f[j]=f[j-m[i]]+w[i];
		}	


        我对背包的理解也止步于此了。下面引用一段DD大神的话吧。

初始化的细节问题

        我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

        如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

        这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值