7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
在上面的数字三角形中寻找一条从顶部到底边的路径,使得
路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。
三角形的行数大于1小于等于100,数字为 0 - 99
输入格式:
5 //三角形行数。下面是三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
要求输出最大和
解题思路:
用二维数组存放数字三角形。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行的三角形:
if ( r == N)
MaxSum(r,j) = D(r,j)
else
MaxSum( r, j) = Max{ MaxSum(r+1,j), MaxSum(r+1,j+1) } + D(r,j)
《数字三角形的递归程序》
#include <iostream>
#include <algorithm>
#define MAX 101
using namespace std;
int D[MAX][MAX];
int n;
int MaxSum(int i, int j)
{
if(i == n)
return D[i][j];
int x = MaxSum(i + 1,j);
int y = MaxSum(i + 1,j+1);
return max(x,y) + D[i][j];
}
int main()
{
int i,j;
cin >> n;
for(i = 1;i <= n;i++)
for(j = 1;j <= i;j++)
cin >> D[i][j];
cout << MaxSum(1,1) << endl;
}
如果采用递规的方法,深度遍历每条路径,存在大量重复计算。则时间复杂度为 2的n次方,对于 n = 100 行,肯定超时。
举个例子,要求7的MaxSum的时候你就要把2拿出来,求4的MaxSum的时候你要把2的值拿出来, 然后7的MaxSum被求了3次,4的MaxSum也被求了3此,所以2的值一共被拿出来6次, 总数就会是2的n次方减1,总的次数全加起来会是2的n次方减1,那时间复杂度当然就是 2的n次方,那如果这个n是100,那时间就很大很大了。所以要想办法去改进。
现在思考一下,如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,就可以免去重复计算。那么 可以用O(n2)时间完成计算。因为三角形的数字总 数是 n(n+1)/2。
《数字三角形的记忆递归型动归程序》
#include<iostream>
#include<algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int maxSum[MAX][MAX];
int MaxSum(int i,int j)
{
if(maxSum[i][j] != -1)//这个数字的最大值已经被求出来了
return maxSum[i][j];
if(i == n)//如果i==n,那就说明它已经是最后一行的那个数字了, 那最大和就是那个数字它自己
maxSum[i][j] = D[i][j];
else //否则呢,我们就要算一下 它正下方的那个数字走到底边得到的最大和是什么
{
int x = MaxSum(i + 1, j);
int y = MaxSum(i + 1, j + 1);
maxSum[i][j] = max(x,y) + D[i][j];
}
return maxSum[i][j];
}
int main()
{
int i,j;
cin >> n;
for(i = 1 ; i <= n ; i++)
for(j = 1 ; j <= i; j++)
{
cin >> D[i][j];
maxSum[i][j] = -1;
}
cout << MaxSum(1,1) << endl;
}
时间复杂度就变成了O(n)平方,因为这个数字三角形里面数字的总数是n(n+1)再除以2,对于每一个数字 它到底边的最大和都只需要算一次就行了。 那这样需要算的总的次数就是这么多。时间复杂度就是n平方了。
递归转换成递推:
递归,它是有一层层的函数调用。如果递归次数特别多的话,可能会爆掉, 另外呢,递归函数调用本身也会有一些时间上的开销,那么实际上不用递归,也可以解决这个问题。而且能够解决的更快,更节省空间
它的第n行的值我们是能够一下就写出来,可以从底下这一层一层层的往上推,最后能够算出这个位置的值。 怎么一层层的往上推呢?现在要求2(坐标(4,1))的这个数字到底边的最大和, 是多少呢?那就是4,5这两个数字里面 更大的,找一个出来,然后再加上2,对吧? 那这个位置我们就知道了,2这个数字到底边的最大和就在这个位置,那就是5+2就是7, 中间过程就掠过去了, 那这个过程并没有什么递归的这个概念在里面。那可以写一个程序模拟这个过程, 二维数组最后一行的值已经知道了,一层层 往上推,把这个最左上角的那个数字的元素求到就完了嘛, 这个过程可以用递推来实现而不需要写递归函数,那具体的程序当然也是很简单的。
《递推型动归程序》
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int n;
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];//最后一行的值正好就等于D的最后一行的值
for( int i = n-1; i >= 1; --i )//从n-1行向上一直推到第一行
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;
}
要求上一行的MaxSum(i,j)的时候下一行的MaxSum(i,j)肯定都已经求完了。 这MaxSum(i,j)最早求出来的是最底下一行的,然后行从n-1,就是往上走, 第n行的求出来,第n-1当然也就能求出来,第n-1行求出来, 第n-2行也能求出来,所以这个程序是没有问题的, 那么这个程序的时间复杂度是多少呢?这个两重循环典型的时间复杂度是n平方, 这个动归程序很典型的就是要从已知推出未知
其实呢,这个程序还可以改进,在哪可以改进呢?空间上,这个MaxSum二维数组 有点浪费了,实际上我们只需要 不需要那么多的存储空间,为什么呢,因为在从一行的MaxSum值算出上一行的MaxSum值以后, 这一行就没有用了, 那再算上一行的MaxSum的值,实际上可以把算出来的值直接就存在 这个地方,对吧,不用再寻找空间,直接存在 原来下一行的位置。然后呢,这个新的值再算出来上一行的,就又存在这。 只要有两行就交替使用, 就足够了。这种形式呢叫做滚动数组,能够节省空间。那我们来看看为什么只要一行就够。
实际上我们在这里列出了这个MaxSum最底层的那一行, 它对于这个D数组里面这数字的MaxSum
要求 2这个数字的MaxSum,那从4,5这两个数字找一个大的,5然后再加上2, 可以算出一个7,那7要用到存放到别处吗? 其实,经观察可知,4这个最大和,它跟5一起推出7以后,这个数字就再也没有用了,那么 我们就可以直接把7放在这, 我们不需要为7找别的地方
现在算出7这个数字的最大和 它是多少啊?它是7 + 5 = 12,那12用不着找别的地方存放,可以直接存放在5这个位置,就行了。 就这样。那接下来,由2,6两个最大和算出来4所对应的最大和是10也不用找别的地方放,可以直接放在这。以此类推。
没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上 递推,那么只要一维数组maxSum[100]即可,即只要存储一行的MaxSum值就 可以。
进一步考虑,连maxSum数组都可以不要,直接用D的 第n行替代maxSum即可。
节省空间,时间复杂度不变,用一个MaxSum的这个指针,变成了 用D的第n行来存放max sum。 那这个程序比刚才的那个空间上又做了一定的这个优化,实际的复杂度是没有什么改变的。
《空间优化》
#include <iostream>
#include <algorithm>
using namespace std;
#define MAX 101
int D[MAX][MAX];
int n;
int * maxSum;
int main()
{
int i,j;
cin >> n;
for(i=1;i<=n;i++)
for(j=1;j<=i;j++)
cin >> D[i][j];
maxSum = D[n]; //maxSum指向第n行,所有的maxsum过程都全部发生在D的第n行上面了
for( int i = n-1; i >= 1; --i )
for( int j = 1; j <= i; ++j )
maxSum[j] = max(maxSum[j],maxSum[j+1]) + D[i][j];
cout << maxSum[1] << endl;
}
递归到动规的一般转化方法
递归函数有n个参数,就定义一个n维的数组,数组 的下标是递归函数参数的取值范围,数组元素的值 是递归函数的返回值,这样就可以从边界值开始, 逐步填充数组,相当于计算递归函数值的逆过程。
动规解题的一般思路
- 将原问题分解为子问题
把原问题分解为若干个子问题,子问题和原问题形式相同 或类似,只不过规模变小了。子问题都解决,原问题即解 \决(数字三角形例)。子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。 - 确定状态
和子问题相关的各个变量的一组取值,是一个“状态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次。 - 确定一些初始状态(边界状态)的值
以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。 - 确定状态转移方程
找出如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(可以用递推公式表示)
能用动规解决的问题的特点
- 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,该问题就具有最优子结 构性质。
- 无后效性。当前的若干个状态值一旦确定,则此后过程 的演变就只和这若干个状态的值有关,和之前是采取哪 种手段或经过哪条路径演变到当前的这若干个状态,没 有关系。