01背包 动态规划

对于当前容量的背包,如果放入当前物品(可能为了放入该物品而腾出一些空间)而使背包总价值增大,那就放入背包。
                                                                                                                                                                         --背包哲学


题目:

0-1 背包问题:给定 n 种物品和一个容量为 C 的背包,物品 i 的重量是 wi,其价值为 vi 。

问:应该如何选择装入背包的物品,使得装入背包中的物品的总价值最大?

思考:

为什么0-1背包问题可以用动态规划求解?

我们先看一下什么是动态规划,我们不会用动态规划求解问题的关键,往往是没有把握动态规划的核心思想,有时候以为自己看懂了0-1背包问题的求解方法,但是再遇到类似的问题还是不会。所以说看懂人家写的和自己真正理解并不是一回事。

优化子结构:如果优化问题的解可以通过它的一系列子问题的解构造得到,则称该优化问题具有优化子结构。

子问题题重叠性:如果根据优化问题的优化子结构直接采用分治方法求解该问题将导致子问题被重复求解,则称该优化子问题具有子问题重叠性。

动态规划算法:对于具有优化子结构和子问题重叠性(有时往往并不明显),可以根据优化子结构设计数据结构和子问题的求解顺序,从规模最小的子问题开始自底向上地计算各个子问题的解,确保每个子问题仅求解一次,将求得的子问题的解和构造最优解需要的信息存储在数据结构中。然后,在根据最优解的构造信息得到优化问题的解。

突然列出三个概念,并不容易理解。举个例子,可以考虑斐波那契数列的递归求法和非递归求法。

递归求法

    if ((num == 1) || (num == 0))  
    {  
        return num;  
    }  
    return F(num-1)+F(num-2);  //这里显然子问题被多次求解,

如图所示,递归求法,F(n-2)等重复求解了多次

而非递归求法

    long long* array = new long long[num+1];  
    array[0] = 0;  
    array[1] = 1;  
    for (int i=2; i<=num; i++)  
    {  
        array[i] = array[i-1] + array[i-2];  
    }  
    return array;  

充分利用了前面求解的子问题,但是这种求法往往看不出重叠子问题在哪,但这就是动态规划。

因此,个人认为动态规划的关键点还是根据优化子结构,自底向上求解,将求得的子问题的解和构造最优解需要的信息存储在数据结构中,当前问题的解由之前求得的子问题构造得来,这样一层层最终求得原问题的解,也就是最顶层的解。

再回到0-1背包问题,那么问题的优化子结构是什么呢?

当考虑第1个物品时,我们有两种策略:

1.将它不放进背包,那么问题转化成第2~n个物品放进容量C的背包里去的最优解

2.将它放进背包,那么问题变成将第2~n个物品放进容量为C-w1的背包里面去的最优解

2中背包容量为什么是C-w1?因为要将物品1放进背包,必须至少需要腾出w1大小的位置

1和2都提到了最优解,那么怎么保证子问题的最优解呢?

我们从第n个物品向前求解(为了和参考教材保持一致),设bi,j为将第i~n个物品放到容量为j的背包的最优代价,那么{b_{i,j}}=max ({b_{i+1,j}},vi+{b_{i+1,j-{w_{i}}}})

意思是

取 不装第i个物品时的代价b(i+1,j)和装第i个物品后代价的最大值,vi+b(i+1,j-wi) 中的j-wi很关键,因为当前背包容量为j时,我们非要装第i个物品时,务必要取b(i+1,j-wi) ,即将第i+1~n个物品放进背包容量为j-wi时的代价值,以便为第i个物品装入背包腾空,但是将第i个物品装入背包后一定比不装入背包的代价值大吗?不一定,因为我们在将第i个物品放入背包时,取的是 b(i+1,j-wi) ,j-wi意味着,我们很可能将i+1~n个物品中代价更高的某些物品拿出来替换成第i个物品,但是替换的结果是背包容量为j是代价更小了即b(i+1,j)>vi+b(i+1,j-wi),因此我们还要比较b(i+1,j),vi+b(i+1,j-wi)一下哪个更大,它的含义是,在背包容量都为j时如果强行放入第i个物品后的代价比不放之前更小,那我们为什么还要放入第i个物品呢?一定要体会背包容量都为j这个前提。

这就是子问题最优解产生的原因,即当前容量为j时,选择代价最大的方式装入物品。

当计算装入第i个物品时的代价时,我们需要用到b(i+1,j-wi) ,因此我们在求装入第i+1~n物品放入背包的代价时应该将背包容量从0到C是的代价值都求一遍,一个原因就是当计算b(i+1,j-wi) 时j-wi是根据wi的不同而变化的,即我们只有将b(i+1,0) b(i+1,c的情况全都求出来,我们才能够计算b(i+1,j-wi) ,关键就是wi的不确定性。

我们可以看出,计算b(i+1,j)需要用到b(i+1,j-wi),因此我们可以先将b(n,j)求解,从这个最小的子问题开始,以此就能够求解b(n-1,j) b(n-2,j)……b(2,j),b(1,j)。正如上一节所说的,而这其中的关键处理方式就在于j一定要取0~C的每种情况。

下面给出伪代码:

输入:正整数重量数组W[1:n]和正整数代价(即价值量)数组V[1:n],正整数容量C
输出:0-1背包问题所有相关子问题的最优解代价数组B[1:n][0:C]
n=数组W[]的长度

//首先构建最小问题(自底向上),即当前只有第n个物品时的情况
For j=1 To W[n]-1 DO 
	B[n][j]=0		    //显然,因为j<W[n]时,第n个物品显然放不下,价值为0
For j=W[n] To C Do
	B[n][j]=V[n]		//当背包容量大于等于W[n]时,可以装入第n个物品,价值量为V[n]
//现在从上面已经解决的最小问题出发,构建最终问题的解
For i=n-1 To 2 Do
	For j=0 To W[i]-1 Do				//从这开始的两层循环为了求出B[i][j]中j从0~C的每种情况
		B[i][j]=B[i+1][j]
	For j=W[i] To C Do				    //将第i个物品放入背包情况,为了知道B[i+1][j-W[i]],
		B[i][j]=B[i+1][j-W[i]]+V[i]		//我们在上一次最外层循环时求出了B[i+1][j]中j从0~C的每种情况
		If B[i][j]<B[i+1][j]  Then  B[i][j]=B[i+1][j]
					
B[1][C]=B[2][C-W[1]]+V[1]	//为何只遍历到2?因为我们仅仅需要求得是B[1][C],
				            //在这之后没有其他子问题了,我们无需为子问题准备B[1][j],j从0到C的每一种情况
If B[1][C]<B[2][C] Then B[1][C]=B[2][C]	//当然我们也可以遍历到1,这样也能够得到B[1][C]
输出B

 

其实伪代码已经很详细,只有读者举个例子,按照伪代码的流程自己画一遍整个流程,0-1背包问题也就理解了,这是理解0-1背包问题最直接的方式,而且还很有效。手动做完这个流程后,再回过头来看上面的一大堆废话,可能就有所理解了。

 

总结

总觉得还是没有讲到关键,但是根据叙述,再结合伪代码手动走一遍流程,一定会真正理解这个问题。总的来说解决0-1背包问题的关键在于:

1.先求出最底层的那个问题,这个问题不依赖其他任何子问题

2.然后在根据之前求得的子问题,结合递推公式,每次利用之前求得的子问题依次求解当前问题

3.注意求解子问题时需要记录背包容量从0到C时的对应的代价。

推荐阅读骆吉洲老师的《算法设计与分析》动态规划算法相关章节,讲的比较透彻,本文大量资料都来源于此。

0-1背包问题其实并不难,但是要真正的理解,可能真的需要耐下心来想一想为什么这样做就能得到最优解。有些问题我们往往知道怎么做,怎么去套用,但是我们可能并不真的理解。算法不是靠记住解决问题的流程,真正理解了问题才算是解决了问题。

参考资料:

《算法设计与分析》骆吉洲

斐波那契数列的递归算法和非递归算法

总结——01背包问题 (动态规划算法)

 

 

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值