浅谈DP算法(一)之一维数组解决01背包问题
浅谈DP算法(一)
——如何用一维数组解决01背包问题
DP算法(Dynamic Programming,俗称动态规划)是最经典算法之一.本笔记以耳熟能详的数塔问题为引子,深入讨论01背包的解决方法.
首先,如下图所示,要求从顶层走到底层,若每一步只能走到相邻的结点,则经过的结点的数字之和最大是多少?
这个问题,对于任意一个结点,直接选择数字大的子结点显然是不行的.以9为例,如果选择15,当前和24>21,但是15的两个子结点太小,24+6+18<21+10+18.由此可见,这样求出每阶段的部分最优解并不是全局最优解.另外,如果用蛮力算法,每条路径都遍历一次,那么层数为n时,路径总数呈指数级增长:
显然这种方法的计算量太大,也不可取.那么此时用DP算法是行之有效的.具体思想为:从倒数第二层开始,一层一层向上遍历.倒数第二层第一个结点是2,如果路径经过2,那么肯定会选择数值较大左子结点19.便用19+2=21代替原先的2.同理18改为18+10=28,9改为19,5改为21.这样倒数第二层就变成21 28 19 21四个结点,再将最后一层舍弃.这样一层层向上,直到第一层,选择第二层较大的那个结点加到9上面去,就得出了全局最优解.
代码实现:如果数字塔为n层,开辟一个n*n的二维数组即可,非常简单,此处省略.
对动态规划的思想有一个基本了解后,现总结出动态规划基本概念.不过在此之前,首先解释一下什么是多阶段决策问题,什么是状态.
多阶段决策问题:一个问题可以分为若干个阶段,每个阶段都要做出决策;
状态:每个阶段开始面临的自然状况或客观条件.在上例中每个阶段的状态就是到达当前结点的两个子结点的选择.
动态规划是一个多阶段决策问题中,各个阶段采取的决策,依赖于当前状态,又引起状态的转移,一个决策序列就是在变化的状态中产生出来的,故有“动态”的含义,称这种解决多阶段决策最优化问题的方法为动态规划方法.
适用DP算法的问题一般具备以下特点:
(1)最优化原理(最优子结构性质)
一个最优化策略的子策略也是最优的.举个例子就清楚了,如数字塔问题中,第二层到底层数值和最大的路径一定是从顶层到底层数值和最大的路径的子集;
(2)无后向性(无后效性)
通俗讲,某阶段状态一旦确定,就不受这个状态之后的决策的影响;
(3)子问题重叠
即子问题之间不独立.与前两个不同的是,这个特点不是必要的,如果不满足相比之下DP算法不具备优势.如果独立,分治算法策略将更简单方便.
下面来看经典的01背包问题:
把N个物品放入容量为V的背包里,第i个物品所需要的空间为need[i],同时它的价值为value[i],该如何放才能达到背包里的物品价值最大?
分析:因为对于任何一个物品,都只有放或不放的选择,因而称之为01背包.用best(N,V)*表示N个物品放入容量为V的背包里的最大价值.则对于第N个物品,除非need[N]>V,都有放或不放两个选择:
(1)如果将第N个物品放入背包中
best(N,V)=best(N-1,V-need[N])+value[N]; //即等于第N个物品的价值加上将N-1个物品放入容量为V-need[N]的背包里的最大价值;
(2)如果不放
best(N,V)=best(N-1,V-need[N])+value[N]; //即等于第N个物品的价值加上将N-1个物品放入容量为V-need[N]的背包里的最大价值;
这样我们重复上述步骤,直至N和V减小到0,可以得到对于其中任何一个物品i (1<=i<=N),当前状态背包剩余容量为j (0<=j<=V),都有
if(j
best(i,j)=best(i-1,j);elsebest(i,j)=MAX{best(i-1,j-need[i])+value[i],best(i-1,j)};
这就是从第i阶段到第i-1阶段的状态转移规律,程序员为了把逼格提高,起了一个术语——状态转移方程.
而当i=0时**,只需设置一下边界值best(0,j)=0.这样,求解best(N,V)这个看起来很复杂又无从下手的问题,就变成了从i=0时的best(0,j)=0逐渐到i=N时的best(N,j).
*best(N,V)只是物品、背包容量和价值三者之间的关系表示,千万不要纠结它为什么这么表示,到底什么意思,里面又是如何根据N,V来得到价值的;
**本来i=0是没有意义的,因为是从第N个物品逐渐推导到第1个物品,设置i=0时的best(i,j)只是为了满足数学上的计算;
代码实现:举一个实例助于理解,现有4个物品,编号1、2、3、4;他们所需空间分别为2、3、4、1,而各自价值为2、5、3、2,背包总空间为5,该怎样放才能使得背包内物品价值最大呢?根据状态转移方程,列表如下所示:
j从1递增到5,best(1,1)=best(0,1)=0,依次求best(i,j)并填入表中,得:
i从1到4,填完整个表格:
观察表格,同样是一个二维的数组,直观上看数塔是从下往上阶段递推,01背包是一行一行向数组最右下角递推.定义二维数组best[N+1][V],best[N+1][V]就是全局最优解.核心代码如下:
for(j=1;j<=V;j++)
best[0][j]=0;for(i=1;i<=N+1;i++)for(j=1;j<=V;j++)
{if(j
best[i][j]=best[i-1][j];elsebest[i][j]=MAX{best[i-1][j-need[i]]+value[i],best[i-1][j]};
}
代码优化:如果觉得问题就这么解决,那就没意思了.在上述表格中,其实可以发现,为了得到最后的一个元素,申请一个庞大的(N+1)*V的数组过于铺张浪费了,用ACMer的话来说就是所需存储空间过大.不难发现,求解第i阶段其实只需要第i-1阶段的值,也就是说,无论N有多大,只需要一个2*V的数组就可以解决问题了,这样确实可以省下不少空间.i为偶数存入best[0][],i为奇数存入best[1][].
best(i-1,j-x)
...
best(i-1,j)
...
best(i,j)
for(j=1;j<=V;j++)
best[0][j]=0;for(i=1;i<=N+1;i++)
{if(i%2){if(j
best[1][j]=best[0][j];elsebest[1][j]=MAX{best[0,j-need[i]]+value[i],best[0,j]};
}else{if(j
best[0][j]=best[1][j];elsebest[0][j]=MAX{best[1][j-need[i]]+value[i],best[1][j]};
}
}
继续优化:看到标题就会猜到,问题还可以继续简化成一维数组.至于如何实现呢,看上述表格继续思考(画个表格不是用来占空间的.
...
best(j-2)
best(j-1)
best(j)
...
初始设置一维数组best[]={0},此时要留心两个细节:
(1)必须从best[V],best[V-1]开始计算并覆盖原先数据;
(2)只需要覆盖到j=need[i].
至于原因不难理解,这里直接给出核心代码:
__int64 best[]={0}; //long long
for(i=1; i<=n; i++)for(j=m; j>=needed[i]; j--)
best[j]=MAX{best[j],best[j-need[i]]+value[i]};
优化到现在,也就是说整个01背包问题,只需要三行代码!无论是时间上,还是空间上,还是代码的简洁性,都达到最优!