4月的最后一天,我4月初就说过,这个月要写出6篇博文,现在就差这篇了。
动态规划是一个比较复杂的算法,最近一直在学习,0-1背包问题属于其中经典中的经典了,现在我们一起重温经典,并谈谈我的理解。
在切入背包问题之前,我们首先要对以下几个概念有所理解:
最优子结构:即全局最优解包含局部最优解。
重叠子问题:在涉及到递归的时候,会遇到相同的子问题被重复计算了多次,可以采用记忆化搜索与递推解决。
那好,我首先通过一个例子引入动态规划。
有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数,如图所示:
从第一行的数开始,每次可以往左下或右下走一格,直到走到最下行,把沿途经过的数全部加起来,如何走才能使这个和尽量大?
首先想到的一定是递归,我们先采用最普通的方法对其求解。
方法一:
这里定义状态指标函数d(i,j)为以格子(i,j)为终点能得到的最大和。
int d(int i ,int j)
{
if(i == 1)
{
return a[1][1];
}else if(j == 1) //注意这种特殊情况
{
return a[i][j] + d(i-1,j);
}
else{
return a[i][j] + (d(i-1,j-1) > d(i-1,j) ? d(i-1,j-1) : d(i-1,j));
}
}
当然你也可以定义d(i,j)为以格子(i,j)为起点能得到的最大和。
这样做是正确的,但时间效率太低,其原因在于重复计算,也就是我之前提到的重复子问题。具体而言,我用以下图示说明。
我要计算d(4,2)对应的要调用的关系树如上所示,显然d(1,1)被计算了3遍,d(2,1)被计算了两遍,也许你会认为重复计算一两个数没什么大不了的,但事实是:这样的重复不是单个节点,而是一颗子树。
记忆化搜索与递推
我们注意到,之所以会出现上述的重复,原因在于之前已经被计算出的值没有被保存而导致重新计算了。
因此我们可以通过简单地赋值操作解决上述问题。
int d(int i ,int j)
{
if(d([i][j] >= 0)
return d[i][j];
else
{
if(i == 1)
{
return d[i][j] = a[1][1];
}else if(j == 1) //注意这种特殊情况
{
return d[i][j] a[i][j] + d(i-1,j);
}
else{
return d[i][j] = a[i][j] + (d(i-1,j-1) > d(i-1,j) ? d(i-1,j-1) : d(i-1,j));
}
}
}
上述程序依然是递归的,但同时计算结果保存在数组d中,题目中说各个书都是非负的,因此如果计算出某个d[i][j],则他应该是非负的,这样只需把所有d初始化-1,即可通过的d[i][j]>=0,得知它是否被计算过。