【分析】
如果熟悉回溯法,可能会立刻发现这是一个动态的决策问题:每次有两种选择——左
下或右下。如果用回溯法求出所有可能的路线,就可以从中选出最优路线。但和往常一样
回溯法的效率太低:一个 n 层数字三角形的完整路线有 2
n
-
1
条,当 n 很大时回溯法的速度将
让人无法忍受。
为了得到高效的算法,需要用抽象的方法思考问题:把当前的位置 (i, j)看成一个状态
(还记得吗?),然后定义状态(i, j)的指标函数 d(i, j)为从格子(i, j)出发时能得到的最大和
(包括格子(i, j)本身的值)。在这个状态定义下,原问题的解是 d(1, 1)。
下面看看不同状态之间是如何转移的。从格子(i, j)出发有两种决策。如果往左走,则走
到(i+1, j)后需要求“从(i+1, j)出发后能得到的最大和”这一问题,即 d(i+1, j)。类似地,往右走
之后需要求解 d(i+1, j+1) 。由于可以在这两个决策中自由选择,所以应选择 d(i+1,j)和
d(i+1,j+1)中较大的一个。换句话说,得到了所谓的状态转移方程:
如果往左走,那么最好情况等于(i, j)格子里的值 a(i, j)与“从(i+1, j)出发的最大总和”之和,
此时需注意这里的“最大”二字。如果连“从(i+1,j)出发走到底部”这部分的和都不是最大的,
加上 a(i, j)之后肯定也不是最大的。这个性质称为最优子结构(opmal substructure),也
可以描述成“全局最优解包含局部最优解”。不管怎样,状态和状态转移方程一起完整地描
述了具体的算法。
提示 9-1:动态规划的核心是状态和状态转移方程。
9.1.2 记忆化搜索与递推
有了状态转移方程之后,应怎样计算呢?
方法 1:递归计算。程序如下(需注意边界处理):
int solve(int i, int j){
return a[i][j] + (i == n ? 0 : max(solve(i+1,j),solve(i+1,j+1)));
}
这样做是正确的,但时间效率太低,
其原因在于重复计算。
如图 9-2 所示为函数 solve(1, 1) 对
应的调用关系树。看到了吗? solve(3,
2)被计算了两次(一次是 solve(2, 1)需
要的,一次是 solve(2, 2)需要的)。也
许读者会认为重复算一两个数没有太大
影响,但事实是:这样的重复不是单个
结点,而是一棵子树。如果原来的三角
形有 n 层,则调用关系树也会有 n 层,
一共有 2
n
-1 个结点。
提示 9-2:用直接递归的方法计算状态转移方程,效率往往十分低下。其原因是相同的子
问题被重复计算了多次。
评论1
最新资源