相对于转载文章,我更喜欢写上一篇笔记,开篇给出原文链接。这样,能有些自己的东西,总结一番,对知识的理解能加深一层;别人看来,也更有价值。
今天做USACO题目时,一道题不会,网上查到解法是01背包,于是重新看了《背包九讲》。相比第一次看,理解深的多,可见我还是在进步的,只要我没停下脚步。如果大家想看原文,那么只需要百度“背包九讲”就好了,百度文库中的“背包九讲 2.0”是正版,作者是崔添翼前辈,网上好像称他为dd大牛。这篇文章可以说是“背包问题”的权威了,如果我了解无误的话,背包问题的整套解法就是这篇文章总结出来的。在此,感谢崔添翼前辈的无私奉献!
本篇笔记,暂时只对01背包、完全背包问题基本解法做出记录,今后如果用到更多,或许会回来更新。
------------------------------------------------------------------------------------------------------------------------------------------------
背包问题模型:容量为V的背包,有N件物品,编号为i的物品,所占容量为Ci,所创造价值为Wi。
01背包问题:每件物品只有一件,可以选择放或不放(即取0件或1件,故名01)。
完全背包问题:每件物品有无穷件,只要装得下放多少都行(物品所取件数,所有值都可能取到,故名完全)。
背包问题设问:
1、最多能创造多少价值?
2、背包放满时,最多(最少)能创造多少价值?
3、背包放慢时,总共有多少种方案?
……
这就是最基础的背包问题了,下面一一讲解。
------------------------------------------------------------------------------------------------------------------------------------------------
我们首先以01背包+问题1为例。
对于背包问题,我们的基本思路是这样的:我们一件一件去放物品,从第1件开始直到第N件。设d[i][v]为放完第i件物品时,所占容量为v的情况下最大价值是多少。
由于我们的问题1并不要求放满背包,那么显然有边界条件d[0][0~N]=0,这表示我们一件物品不放的时候,可以认为已经占了任意容量,创造的最大价值为0。
而后,我们开始一件一件放物品,c++代码如下:
for(int i=1;i<=N;++i) for(int v=Ci;v<=V;++v) d[v][i]=max(d[v][i-1],d[v-Ci][i-1]+Wi);
应该很容易理解,d[v][i]只可以由两种情况更新,一种是d[v][i-1],这表示第i件物品不放入背包(或者说放入0件);一种是d[v-Ci][i-1]+Wi,这表示第i件物品放入背包。我们用这两种情况的最大值去更新d[v][i],那么便考虑了所有情况,从而得到了d[v][i]得到了满足定义的值。
这便是动态规划的思路了,很容易理解,也很好写。那么我们来分析一下它的复杂度:时间复杂度O(NV),空间复杂度O(NV)。
下面我们来考虑优化。这种算法是考虑了所有情况,从而保证了正确性,因此时间复杂度上基本是无法优化的;而空间上,可以看到我们是层层递推上去的,求第i件物品的d[0~V][i]时,我们只需要用到d[0~V][i-1]而已,因此我们至少可以把空间缩减到两层,也就是O(2V)。而实际上,本题是可以让空间为一层的,即O(V)。
这时,我们的初始化代码是:
for(int v=0;v<=V;++v) d[v]=0;
而递推代码则如下:
for(int i=1;i<=N;++i) for(int v=V;v>=Ci;--v) d[v]=max(d[v],d[v-Ci]+Wi);
其实并不难理解,我们把内层循环的顺序颠倒了。这样一来,很容易发现,我们内层循环至v时,d[v+1~V]实际上都是d[v+1~V][i],而d[0~v]则是d[0~v][i-1]。因此,党我们执行d[v]=max(d[v],d[v-Ci]+Wi)时,它实际上就相当于d[v][i]=max(d[v][i-1],d[v-Ci][i-1]+Wi),因为执行完这句,d[v]就相当于是d[v][i]了。
如果上面的话你理解了,那么你也就明白了01背包问题的解法,时间O(NV)空间O(V)的解法。
那么我们讨论下01背包+问题2:
问题2很容易,我们要把背包放满,那么只需要修改一下初始化代码:
d[0]=0; for(int v=1;v<=V;++v) d[v]=-∞;
很好理解,一件物品不放时,只有d[0]有意义,其余容量都不可达到。因此,我们设为-∞,保证后面不会被max选到。而后面,只需要使用同样的递推代码,意义完全不变。这算法正确性太显然,我就不说明了。
接下来,我们来讨论完全背包问题:
完全背包中,每件物品可以放无穷件。如果我们考虑空间为O(NV)的算法,那么会相对复杂一点,这里不予讨论;而实际上,我们也只需要O(V)的空间,代码如下:
for(int i=1;i<=N;++i) for(int v=Ci;v<=V;++v) d[v]=max(d[v],d[v-Ci]+Wi);
很简单,只需要把内层循环恢复正序即可。我这里不加解释,相信大家简单想想便可以理解。
而对于问题1和问题2,和01背包一样,相对应修改初始化部分即可,代码完全一样。
那么,问题3呢?
这个问题很容易,我也不打算给出代码。有兴趣的读者可以自己想想,我在USACO上所做那道题便是这种类型,想不出来的同学可以参考一下,这是博文链接。
------------------------------------------------------------------------------------------------------------------------------------------------
至此,本篇对背包问题的基础内容讲解完毕。如果大家仍有兴趣了解,不妨去看看《背包九讲》原文。