大名鼎鼎的“背包问题”我不敢企及去探讨,最近初学,感觉背包及相关问题真乃博大精深,于是把自己初学的体会写下来,帮助那些跟我一样也许暂时还没有思路的人。本文的核心算法都是动态规划。
【问题】
与背包问题近似的经典动态规划题目有(附相应的wiki条目):
背包问题(knapsack problem)http://en.wikipedia.org/wiki/Knapsack_problem
找零问题(coin changing/change making)http://en.wikipedia.org/wiki/Change-making_problem
子集和问题(subset sum)http://en.wikipedia.org/wiki/Subset_sum
子集分割问题(subset partition problem)http://en.wikipedia.org/wiki/Partition_problem
编辑距离(edit distance/levenshtein distance)http://en.wikipedia.org/wiki/Edit_distance
换行/段落调整问题(line breaking/word wrap)http://en.wikipedia.org/wiki/Word_wrap
【动态规划与回溯】
另外,本文中都是采用了动态规划的思想解决这些问题。由于动态规划和回溯法之间往往是可以转化的(都有状态空间),关于其中一个问题“子集和问题”的回溯法解决详见这里:
http://hi.baidu.com/microgrape/blog/item/c78fa7f41057b22fbd310938.html
【0/1背包】
本文所说的两种背包问题是0/1背包(0-1 knapsack)和无限背包问题(unbounded knapsack,又叫完全背包Complete knapsack)。下面分别说明。
先说简单的0/1背包。举个实际的例子,我们在出远门旅行的时候,常常会犹豫再三:我们的背包容量是有限的,怎么才能带上我们旅途中最需要的东西呢?转化成抽象的语言就是:一个给定容量N的背包和一堆物品的集合,每个物品Xi有两个属性:价值Vi和大小Ci,如何才能使背包中所装物品的价值最大?
贪心算法在这种情况下是不能使用的,即取当前价值最大的物体放入背包是无法实现最优的。举一个简单的反例:假设一个物体A的大小恰好是N,但是价值是10;如果此时还有两个物体B和C的大小分别是N/2,但是价值分别是7,那么显然将BC放入背包要优于只放A,但贪心算法会选择A。也就是贪心的决策是不可行的。
那应该用什么样的决策呢?首先来进行动态规划的基本工作:“划分状态”。假设当前状态为:容量为j的背包里装了i件东西,其最大价值为Value[i,j]。然后进行动态规划的另一个基本工作:“状态转移”,由于第i件物品我们只有两种选择(选择或者不选),也就是状态转移的可能只有两种。那么它的前一个状态有可能是以下的两种(逆推回去):
a)第i件并没有装进包里。因为可能装第i-1件物品之后,包里已经满了,不能再装了,那么前一状态其价值为Value[i-1,j]。
b)第i件放进包里。那怎么表示第i件东西放进包里了呢?说明在放第i件之前,也就是说包里有i-1件物品的时候,包内物品的体积最多只能达到了j-Ci(Ci是第i件物品的体积,要把这个预留出来给第i件物品)。也就是说之前的一个状态就是Value[i-1,j-Ci]。对应的现在的状态就是:装上第i件物品之后包内物品价值变成了Value[i-1,j-Ci]+Vi(Vi是第i件物品的价值)。
那既然有这两种可能,我们到底该选哪种呢?决策就是:哪个价值大,我们就选哪个,所以最后的递推关系式就是:
Value[i,j] = max { Value[i-1,j], Value[i-1,j-Ci]+Vi }
附注:虽然状态j应该是以1为间隔的,但是由于物品的体积实际上并不是一定以1为间隔的,所以j的状态转移并不连续。
【无限背包】
无限背包与0/1背包的区别就是:0/1背包中每种物品就只有1件,从第i-1件到第i件只有两种可能(第i件东西放与不放);但无限背包中,每件物品都有无数件,但是这并不是意味着每次都有无数种选择,实际上这意味着从第i-1件到第i件时,需要选择到底放哪件,也就是说只有i种选择。由于有i种选择,那么可能就有i种的状态转移,对应的前一个状态也就有i种。都是哪i种呢?经过与上面0/1背包的比较之后就发现需要修改上面的情况b),其中的第i件物品可以是物品集合中的任意一个(当然不能大于背包容量),如下:
情况b)若选择了体积为C0,价值为V0的物品,即当前状态是从Value[i-1,j-C0]状态转移而来,当前状态是Value[i-1,j-C0];若选择了体积为C1,价值为V1的物品,则对应的当前状态为Value[i-1,j-C1]。。。以此类推。最后从这些当中选择最大的即可。
综上,最后的解的形式为:
Value[i,j] = max{ Value[i-1,j], max{ Value[i-1,j-Ci]+Vi } } 。其中Ci<=j,0<=i<=N(即所有物品中选)
附注:由于在这个递推关系式中,i只是一个循环变量(因为每一步面临的选择都一样),所以可以写成以下的版本:
Value[j] = max{ Value[j-1], max{ Value[j-Ci]+Vi } } 。其中Ci<=j,0<=i<=N(即所有物品中选)
【找零问题】
找零问题是给出一种硬币的集合,例如{1分,2分,5分,1毛,。。。},然后给定一个需要找零的数额,例如3毛7分;求出找回硬币数目最少的方案。
这个问题在大多数情况下,贪心是可行的。但是并不是所有的情况都符合。所以还是用动态规划来解,归结到动态规划上面就变成了无限背包问题(因为收银台的硬币默认是无穷的,但一种改进版本可以考察有限硬币的情况)。区别在于,现在我们需要求一个最少的硬币数而不是最大值。但是选择的情况也是相同的,即每次选择都可以选择任何一种硬币。
分析:详尽分析请看“参考资料”中的那份英文教程。我这里仅作简单引用,设p为需要找零的数额,Coins[p]为所需要的最少硬币数(即最优解)。假设从p中除去任意一枚硬币Ci,那么剩下的解Coins[p-Ci]必然还是一个最优解(当需要找零的面值为p-Ci时的最优解),很显然Coins[p] = Coins[p-Ci]+1。但问题是除去的这一枚硬币Ci必须是最优解中的一个,但我们怎么找到这样的Ci呢?答案跟上面的无穷背包一样,尝试所有的选择,选择其中产生最小值的那个决策。
最后的递推式为:Coins[p] = min{ Coins[p-Ci]+1 },其中0<=i<=N,N为硬币的种类数。
【参考资料】
“背包问题九讲”--这个也是Cui Tianyi大牛的经典:http://xxjs.kmip.net/pack/
下面是此牛牛的blog:http://cuitianyi.com/
这里有一篇不错的分析(但是略微潦草):http://blog.csdn.net/hhygcy/archive/2009/03/04/3955683.aspx
动态规划的基本模型:http://daothree.blog.hexun.com/13126729_d.html
找零问题的一份JAVA代码:http://www.javaeye.com/topic/320498
找零问题的一份英文详细分析:http://www.ccs.neu.edu/home/jaa/CSG713.04F/Information/Handouts/dyn_prog.pdf (看完之后觉得,老外的思路真是严谨,讲这么一个问题都很清晰认真)
【结语】
发现自己水平就只能到这里了。。。再多一句都写不出来。。。sigh