一、问题描述
如上图所示,有一个由非负整数组成的三角形,第一行只有一个数,除了最下行之外每个数的左下方和右下方各有一个数。现请你在此数字三角形中寻找一条从首行到最下行的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99 。
二、问题分析
要求找出一条路径,它经过的数字之和最大。我们可以细化到每一步,每一次往下走都要选择较大的数。
于是可以得出下面的以下的伪代码:
if(当前行是最下行) 当前行到最下行的最大和 = 当前数字的值 else 当前行到最下行的最大和 = 下一行到最下行的最大和 + 当前数字的值
这样的伪代码实在是难读,因此我们需要用抽象的方法思考问题(即变量化):
- (i , j):当前的位置(状态)
- a(i , j):表示第 i 行的第 j 个数字 //i , j > 0
- d(i , j):从位置(i , j)到最下行的最大和 //状态(i , j)的指标函数,且原问题的解是d(1,1)
于是,上面的伪代码转化为:
if(i == n) d(i,j) = a(i,j); else d(i,j) = max{d(i+1,j),d(i+1,j+1)} + a(i,j);
我们来看不同的状态之间是怎么转移的:从位置(i , j)出发有两种决策,①往左走,则走到(i+1 , j)后,将要求解d(i+1 , j);②往右走,则走到(i+1 , j+1)后,将要求解d(i+1 , j+1)。
由于可以在这两个决策中自由选择,所以应选择d(i+1 , j)和d(i+1 , j+1)中较大的那个。这一步正导出了所谓的状态转移方程:
- d(i , j)= max{d(i+1 , j), d(i+1 , j+1)} + a(i , j)
这个方程已经蕴含了最优质结构性质(全局最优解包含局部最优解)。即如果连“从(i+1 , j)或(i+1 , j+1)出发到最下行”这部分的和都不是最大的,加上a(i , j)之后肯定也不是最大的。
三、解题方式
1. 递归计算
int solve(int i,int j) { if(i == n) return a[i][j]; else return max(solve(i+1,j),solve(i+1,j+1)) + a[i][j]; }
分析:用直接递归的方法计算状态转移方程,效率往往十分低下。其原因是相同的子问题被重复计算。
2. 递推计算
for(int j=1;j<=n;j++) d[n][j] = a[n][j]; //最后一行 for(int i=n-1;i>=1;i--) for(int j=1;j<=i;j++) d[i][j] = max(d[i+1][j],d[i+1][j+1]) + a[i][j];
分析:i 是逆序枚举的,所以在计算d[i][j]前,它所需要的d[i+1][j]和d[i+1][j+1]都已经计算出来了。
提示:可以用递推法计算状态转移方程,递推的关键是边界和计算顺序。
3. 记忆化搜索
/* 第一部分:将d全部初始化为-1 */ memset(d,-1,sizeof(d)); /* 第二部分:编写递归函数 */ int solve(int i,int j) { if(d[i][j] != -1) return d[i][j]; //判断状态(i,j)是否已经被计算过 if(i == n) return d[i][j] = a[i][j]; else return d[i][j] = max(solve(i+1,j),solve(i+1,j+1)) + a[i][j]; }
分析:此程序是递归的,但是它同时把计算结果保存在数组d中。所以,千万别忘记在计算之后把它保存在d[i][j]中。此程序的方法称为记忆化,它虽然不像递推法那样显式地指明了计算顺序,但仍然可以保证每个结点只访问一次。
提示:根据C语言“赋值语句本身有返回值”的规定,可以把保存d[i][j]的工作合并到函数的返回语句中。
提示:可以用记忆化搜索的方法计算状态转移方程。当采用记忆化搜索时,不必事先确定各状态的计算顺序,但需要记录每个状态“是否已经计算过”。