本文始发于个人公众号:TechFlow,原创不易,求个关注
今天是周三算法与数据结构专题的第12篇文章,动态规划之零一背包问题。
在之前的文章当中,我们一起探讨了二分、贪心、排序和搜索算法,今天我们来看另一个非常经典的算法——动态规划。
在acm-icpc竞赛领域,动态规划是一个非常大的范畴,当中包含了许多变种,而且很多变种难度极大。比如在各种树上和图上以及其他数据结构上做动态规划,这会使得问题非常复杂。好在非竞赛选手并不需要了解到那么深入,一般来说,吃透背包九讲,就足够笑傲各种面试了。所以周三的算法专题我们开始全新的篇章——背包系列,今天和大家分享背包九讲中的第一讲,也是最简单的零一背包问题。
背包和零一背包
没有竞赛经验的同学在看到这个标题的时候可能会一头雾水,动态规划和背包有什么关系。其实没有关系,我也不是陈奕迅的粉丝,只是当初最经典的动态规划问题用背包做了题面,还引发出了各种变种。后来在教学的时候为了方便,于是沿用了前人的名称。
之前我们在怪盗基德偷宝石的问题当中提到过背包问题,其实很简单,就是说我们当下有一个容量是V的背包,和n个体积分别是v[i],价值是w[i]的五品。请问,在背包容量允许的前提下,我们最多能够获得多少价值的物品?
由于每种物品只有一个,也就是物品只有拿和不拿两种状态,所以这个问题被称为零一背包问题。
贪心与反例
这种问题我们最先想到的就是贪心法,比如优先拿价值大的物品,或者是性价比高的物品,但是我们很容易构思出反例。
举个例子,比如背包的容量是10,我们有3个物品,体积分别是6,5,5,价值是10,8,8。这个反例可以证明两种贪心策略都不生效,因为价值最大的是10,它的体积是6,我们一旦拿了它就没有空间再继续获取其他物品,而显然拿两个5的情况是最优的。同样,体积是6的物品也是性价比最高的,性价比优先的贪心策略同样不生效。
实际上不仅这两种贪心策略不生效,所有能够想到的贪心策略都不生效。这个问题看起来简单,但是并不是那么容易解决。实际上这个问题一直困扰着计算学家,直到上世纪六十年代,动态规划算法横空出世,完美地解决了这个问题。
动态规划
动态规划算法的英文是dynamic programming,算是很直白的翻译了。规划我们都很好理解,但是动态应该怎么理解呢?又怎么来动态地规划呢?关于这个问题的思考直接关系到算法的本质。
动态规划算法的本质是状态的记录和转移,我们结合刚才的问题,有没有想过为什么贪心算法不可行?其实很简单,因为我们没办法确定背包什么状态是完美的。虽然我们知道背包的容量是V,但是我们并不知道最优的情况下我们能装多少,最优的结束状态是什么。我们把空间V看成了一个状态来进行贪心,贪心得到的结果是最优的,但是只是贪心能达到的状态的最优解,并不是全局的最优解,因为背包容量的限制,很有可能我们贪心策略下无法达到真正最优的状态。
用刚才的例子解释一下上面这段话,在贪心算法下,我们会选取容量是6,价值是10的物品,这个物品拿取了之后背包的状态是6,获取的价值是10。这个状态是贪心能够达到的最终状态,对于这个状态而言,它是最优解,但是这个状态并不是整体最优的情况,因为在贪心策略下,无法达到容量10全用完的状态。
理解了这个问题之后,再去推导解法就顺其自然了。贪心策略可以获取一些状态最优的情况,那么我们能不能记录下所有状态能够达到的最优的情况,最后在这些最优的情况当中选取一个最优的,它不就是整体最优解了吗?
动态规划正是基于上述思路展开的,它解决的不是一个状态的最优解,而是所有状态的最优解。
状态与转移
看到这里,你肯定还没理解动态规划算法,但是应该已经有一些大概的感觉了。这是对的,有正确的感觉是正确认识的前提。我们循序渐进,再来看状态这个概念。
我们刚才提了这么多次,究竟状态是什么呢?这是一个比较抽象的概念,在不同的问题当中它有着不同的含义。在背包问题当中,状态指就的是