动态规划

来源:互联网 发布:php base64 加号 编辑:程序博客网 时间:2024/04/29 22:16

最近开始学习动态规划

先从dd大神的《背包九讲》开始,准备结合网上各种资料以及《算法导论》,自我摸索和总结并结成报告。

今天看懂了01背包,这里 来说说我的浅见:

01背包

理解01背包的关键时理解转移方程和模拟一次转移过程。首先我们假设一个容量为V的背包(代码中以M表示),然后假设有N种价值不一的物品,每种物品只有一个,每种物品又有不同的体积。怎么在不超过背包容量的前提下,放入尽可能价值大的物品,这就是01背包探讨的问题。

首先我说说自己对dp的理解(在看了不少资料的前提下),感觉是对中间过程的选择,来选择出最合适的结果。中间状态构成最终状态,子问题构成整体问题,有点分治的感觉在里头。(确实略抽象)

打个比方吧:每个事件开始的时候都有很多种选择,每选择一次又有更多选择,这样不断选下去




仔细看看,其实每次选择有点类似递归,解决问题的方法自然也是提炼出每次解决问题相同的那一点,即用相同的方法来完成每一步的选择,这样代码就很简单了。

当然,这其实也是穷举,不过就这样穷举的好处是每次每一步的选择做一次就行了,不必重复做相同的选择,因为之前选择的结果会被保存下来。

不多说了,看上头那题,用dp[i][j]来表示选择前i种物品放入容量为j的背包中最大的价值,用volume[i]来表示第i种物品的体积,用value[i]来表示第i种物品的价钱。那么我们对于前i种物品,我们为了提炼出每次选择之前的联系,先找出第i种物品和第i-1种物品的关系。讨论第i种物品,因为只有一个,所有只有放或者不放两种选择。如果放的话,那么dp[i][j]=dp[i-1][j-volume[i]]+value[i], 如果不放的话,dp[i][j]=dp[i-1][j]。我们需要的是最大的价值总和,因此我们做出的选择是取大的那个

因此dp方程应该为dp[i][j]=max(dp[i-1][j],dp[i-1][j-volume[i]]+value[i]),同时我们还要注意放不放得下的问题,如果j<=volume[i],那么dp[i][j]=dp[i-1][j](必须不放啊,放不下的说)

然后我们需要填充这个dp数组,注意到dp[i][j]只跟后头两个有关系,所以我选择的是物品从1开始编号,同时把dp[0][..]和dp[..][0]初始化。

那它们到底初始化成多少呢?这里又涉及到一个问题,那就是“背包到底要不要装满???”因为如果必须要求背包装满的话,这样解和不一定要装满的求出的最大价值不同。

我目前是这样理解的:dp[0][..]代表不放物品的最大价值,既然放入0种物品到容量不为0的背包,那必然是没有装满的,如果不需要装满的话,那么就初始化成0吧,本来价值也为0嘛。但是如果要求必须装满的话,就应该初始化成∞吧,代表这是不可能的(注意这里dp[0][0]还是0吧,这并不违反题意。)dp[..][0]代表把 1-N种物品装入容量为0的背包,自然全部初始化为0就行了。

PS:上述∞到底是正的还是负的呢?这就跟max有密切关联了,一句话概括,如果是求最大价值,即每次选择是max,则应该是-∞。如果求最小价值,每次选择是min,则为+∞。

说了这么多,下面附上代码

for(int i=1;i<=N;++i)for(int j=0;j<=M;++j)if(j>=volume[i])dp[i][j]=max(dp[i-1][j],dp[i-1][j-volume[i]]+value[i]);else dp[i][j]=dp[i-1][j];

然后就是01背包的简化问题,这个问题我最开始看的卡住了,因为当时对转移方程还没有吃透,还是我手动模拟了一下转移过程才理解的。看来动手模拟模拟也许对一些人有用哦。

所谓的简化就是我们求解问题的时候往往只关心(还是上头那个问题)最大的价值是多少,而不关心装了哪些物品,装了几件物品,这时候我们可以发现dp[i][j]中那个[i]这一维是多余的,如果不需要的话,那就去掉吧。那么可以去掉么?当然是可以的。去掉[i]后转移方程变成了dp[j]=max(dp[j],dp[j-volume[i]]+value[i])

观察这个方程和有[i]方程的可以发现,这对结果并没有本质的影响,依旧是我们所需的结果。但是这时候需要注意j循环的方向了,之前方向是任意的,无所谓,因为当循环内层的j的时候j与j之间互相是不影响,也就是说[i]打头的维数只与[i-1]有关。即使去掉了这一维,这一关系我们也得注意,所以j应该逆向循环。因为现在后头的状态是依赖之前的状态,注意到dp[j-volume[i]]这说明后头的状态依赖之前的状态这点,而我们用i从1-N来是一层一层刷新dp[j]的,所以每次j开始循环的时候,现在都保存的时上一个循环完的状态。为了而本次循环又要依赖上次循环的状态,所以得从后头开始更新dp[j],以此来确保dp[j-一个数]这个东东是上一层循环的产物。

因此代码如下:

for(int i=1;i<=N;++i)for(int j=M;j>=0;--j)if(j>=volume[i])dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);

今天继续看02背包,不过在看02背包之前,我又看了一下别人写对01背包的看法,并且发现其实01背包是可以在上述基础上得以优化的。

优化主要争对第二层for循环,注意到当j<=volume[i]时,语句其实是不执行的,因此可以去掉这一部分

for(int i=1;i<=N;++i)for(int j=M;j>=volume[i];--j)dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);

继而该进成上述代码。不过我还看到貌似还可以继续优化


优化成这个样子。那个max()里头代表的是算出总容量V减去从i到N的物品的价值之和,以及当前物品的体积,取二者的较大值。这点我暂时还没看懂。。。。




02背包

02背包也称为完全背包,其实跟01背包很相似。我们依旧假设有一个容量为V的背包,然后假设有N种价值不一的物品,但是每种物品都有无数个了,当然每种物品有不同的体积。还是求怎么在不超过背包容量的前提下,放入尽可能价值大的物品,这就是02背包探讨的问题。

说起来每种物品是无限个,但是背包容量有限,因此第i种物品放入的最大个数应该是V/volume[i],仔细对比01背包的思路,不难想到解决的办法。依旧用dp[i][j]来表示选择前i种物品放入容量为j的背包中的最大价值,用volume[i]来表示第i种物品的体积,用value[i]来表示第i种物品的价值。再多加一个k来i表示第i种物品的个数,用跟01背包完全相同的思考方式,那么状态转移方程应该是:dp[i][j]=max(dp[i-1][j],dp[i][j-k*volume[i]]+k*value[i]),如果不放第i种物品的话,显然dp[i][j]=dp[i-1][j],但是如果放入第i中物品的话,注意到dp[i][j]=dp[i][j-k*volume[i]]+k*value[i],后头那个仍然是i哦,01背包里头是i-1对吧。因为这里即使放了k个第i种物品,仍然可以继续放入第i种物品,所以i不需要减一。

因此方程应该可以写成是:

for(int i=1;i<=N;++i)for(int j=0;j<=M;++j)for(int k=0;k*volume[i]>=0 && k*volume[i]<=j;++k)dp[i][j]=max(dp[i-1][j],dp[i][j-k*volume[i]]+k*value[i]);
这样写很明显复杂度太高了,一般情况下应该是难以接受的。

下面当然再来改改01背包里头的改进版本,把那个版本改成02背包。这里的思路有两条:

①根据上述已经推出来了的二维dp方程改进

②根据01背包的改进的一维dp方程进行改进

先说说第一种方法:犹记得当初修改01背包,将二维改成一维是去掉[i]这一维,这里同理这么去掉,于是,状态转移方程变成了dp[j]=max(dp[j],dp[j-k*volume[i]]+k*value[i]);

但这里和01背包不同的是这里是i并不是依赖[i-1那一维]的,而就是依赖[i]这一维的,因此循环的时候,j应该是正向循环的,这样才能保证第i次更新用的是[i]这一层的状态,而不是上一层的状态。

由此我们得到了空间简化版02背包解法。

第二种讨论方法大同小异,在此就不赘述了。下面最最主要的是要把这个解法时间复杂度优化。怎么可以去掉一层循环呢?

我是这样想的,让我们从结论出发,考察dp[j]=min(dp[j],dp[j-weight[i]]+value[i]),我们不要管每一种物品有多少件,我们就用dp[j]表示容量为j的背包在选择物品个数最合适的条件下取得的最大价值,就是不管选多少件物品,反正dp[j]表示就是那个最大的物品价值。因此去掉k那一层的循环。

因此改进后的02背包解法就是

for(int i=1;i<=N;++i)for(int j=volume[i];j<=M;++j)dp[j]=max(dp[j],dp[j-volume[i]]+value[i]);

这样的话就和01背包几乎相同的了,除了j循环的方向相反以外。这样理解我目前感觉是习惯就好了,毕竟我也才学会一两天,不过感觉上自己模拟去推推会飞快的加深这个理解。我举个例子来说明为什么dp[j]=max(dp[j],dp[j-weight[i]]+value[i])已经代表了物品无限的情况,这也是我请教别人这个问题时候,他给我举的例子:我们特殊一点,每种物品的体积均是2,价值按物品种类顺序,依次为1,2,3,4(假设只有4种物品),假设背包不用塞满,背包容量为4,下面手动模拟整个过程:

当i=1时,dp[1]=0;

                 dp[2]max(dp[4,dp[2-2]+1])=1

                dp[3]max(dp[4,dp[3-2]+1])=1

                dp[4]=max(dp[4,dp[4-2]+1])=2

注意到没啊,dp[4]是2哦,不是1哦,因为已经考虑到可以塞2个进去了,这是为什么呢,因为dp[2]已经塞了一个进去了啊,这也就是为什么要顺着循环的原因了。

有兴趣的话,可以多多模拟哦。

后面一段时间我还会继续并且陆续更新背包滴!!o(∩_∩)o