7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
(图1)
图1给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最大的和。
注意:路径上的每一步只能从一个数走到下一层上和它最近的左边的那个数或者右边的那个数。
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
30
动态规划算法与分治法类似,其基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。
动态规划算法与分治法不同的是,经分解得到的子问题往往不是相互独立的,有大量子问题会重复出现。
为了避免重复计算,动态规划法是用一个表来存放一计算过的子问题。
关于动态规划的题很多都可以使用递归的方法,如果将记忆化的搜索加进去,可能就是动态规划的递归形式,
不过,在动态规划中使用递推的方法一般会比较好,因为这是讲函数调用的时间也省略了。
所以建议使用递推性来解题,不过在使用过程中,递推方程就是最大的问题,如果递推方程推导出来了,那么题目也就解完了;
下面看一看这个题目的分析:
这是先用递归的方法解体;
用二维数组存放数字三角形。
从第一行开始访问每一个数字;
D( r, j) : 第r行第 j 个数字(r,j从1开始算)
MaxSum(r, j) : 从D(r,j)到底边的各条路径中,最佳路径的数字之和。
问题:求 MaxSum(1,1)
在递归过程中每一个数字都会访问到,
典型的递归问题。
D(r, j)出发,下一步只能走D(r+1,j)或者D(r+1, j+1)。故对于N行的三角形:
当到达最后一行的时候已经没有下一行可以选择了,只能将这一行的各个数字作为最大值;
MaxSum(r,j) = D(r,j) 当r==N
如果不是最后一行;就会有以下的递归公式:可以试着自己推导一下;
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j) ;
int D[MAX][MAX];
int n;
int Maxsum(int i,int j)
{
if(i == n)
return D[i][j];//如果等于n,表示到了底边;
int x = Maxsum(i+1, j);
int y = Maxsum(i+1, j+1);
return max(x, y)+D[i][j];
}//暴力递归函数的思想;不出意外,这在poj上是超时的;因为很多的重复计算是没必要的;
下面可以看看使用递归的动态规划;
改进
如果每算出一个MaxSum(r,j)就保存起来,下次用
到其值的时候直接取用,则可免去重复计算。那么可以用O(n2)时间完成计算。因为三角形的数字总数是 n(n+1)/2
//递归上减去重复的项;
int D[MAX][MAX];
int n;
int nummax[MAX][MAX];//使用nummax数组进行储存已经计算过的数;
int Maxsum(int i,int j)
{
if(nummax[i][j]!= -1)
return nummax[i][j];//这一步就是将递归转换为动态规划的方法,就是这么简单;
if(i == n)
nummax[i][j] = D[i][j];
else{
int x = Maxsum(i+1, j);
int y = Maxsum(i+1, j+1);
nummax[i][j] = max(x,y)+D[i][j];
}
return nummax[i][j];
}
递归函数有n个参数(这里的参数是状态参数),就定义一个n维的数组,数组的下标是递归函数参数的取值范围,数组元素的值是递归函数的返回值,这样就可以从边界值开始,逐步填充数组,相当于计算递归函数值的逆过程。
数字三角形的递推型动归程序:
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int maxSum[MAX][MAX];
int main() {
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
for( int i = 1;i <= n; ++ i )
maxSum[n][i] = D[n][i];
for( int i = n-1; i>= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[i][j] = max(maxSum[i+1][j],maxSum[i+1][j+1]) + D[i][j]
cout << maxSum[1][1] <<endl;
return 0;
}
在这个过程中,可能你会发现,在每一行中的计算其实就只需要一行就可以储存了;就可进行空间优化;
//不使用递归的方法实现动态规划;;使用递推的方法;
int main()
{
int D[MAX][MAX];
int *nummax;
int n;
cin>>n;
for(int i = 1;i<=n;i++){
for(int j = 1;j<=i;j++){
cin>>D[i][j];
}
}
nummax = D[n];//存放每一轮新的结果;不断子安更新中;
for(int i = n-1;i>=1;i--){
for(int j = 1;j<=i;j++){
nummax[j]=max(nummax[j],nummax[j+1])+D[i][j];
cout <<nummax[j]<<" ";
}
cout <<endl;
}
cout <<nummax[1]<<endl;
return 0;
}
动规解题的一般思路
1. 将原问题分解为子问题
把原问题分解为若干个子问题,子问题和原问题形式相同或类似,只不过规模变小了。子问题都解决,原问题即解决(数字三角形例)。
子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
2. 确定状态
在用动态规划解题时,我们往往将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。
所有“状态”的集合,构成问题的“状态空间”。“状态空间”的大小,与用动态规划解决问题的时间复杂度直接相关。在数字三角形的例子里,一共有N×(N+1)/2个数字,所以这个问题的状态空间里一共就有N×(N+1)/2个状态。
整个问题的时间复杂度是状态数目乘以计算每个状态所需 时间。
在数字三角形里每个“状态”只需要经过一次,且在每个状态上作计算所花的时间都是和N无关的常数。
用动态规划解题,经常碰到的情况是,K个整型变量能构成一个状态(如数字三角形中的行号和列号这两个变量构成“状态”)。如果这K个整型变量的取值范围分别是N1, N2, ......Nk,那么,我们就可以用一个K维的数组array[N1] [N2]......[Nk]来存储各个状态的“值”。这个“值”未必就是一个整数或浮点数,可能是需要一个结构才能表示的,那么array就可以是一个结构数组。一个“状态”下的“值”通常会是一个或多个子问题的解。
3. 确定一些初始状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
4. 确定状态转移方程
定义出什么是“状态”,以及在该 “状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值”(“人人为我”递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。
能用动规解决的问题的特点
1) 问题具有最优子结构性质。如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。
2) 无后效性。当前的若干个状态值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。