动态规划详解(数字三角形POJ1163)

算法进阶 同时被 3 个专栏收录
115 篇文章 0 订阅
6 篇文章 0 订阅
13 篇文章 0 订阅

				 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维的数组,数组 的下标是递归函数参数的取值范围,数组元素的值 是递归函数的返回值,这样就可以从边界值开始, 逐步填充数组,相当于计算递归函数值的逆过程。

动规解题的一般思路

  1. 将原问题分解为子问题
     把原问题分解为若干个子问题,子问题和原问题形式相同 或类似,只不过规模变小了。子问题都解决,原问题即解 \决(数字三角形例)。子问题的解一旦求出就会被保存,所以每个子问题只需求解一次。
  2. 确定状态
     和子问题相关的各个变量的一组取值,是一个“状态”。一个“状态”对应于一个或多个子问题, 所谓某个“状态”下的“值”,就是这个“状 态”所对应的子问题的解。
    整个问题的时间复杂度是状态数目乘以计算每个状态所需时间。在数字三角形里每个“状态”只需要经过一次。
  3. 确定一些初始状态(边界状态)的值
    以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。
  4. 确定状态转移方程
    找出如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(可以用递推公式表示)

能用动规解决的问题的特点

  1. 问题具有最优子结构性质。如果问题的最优解所包含的 子问题的解也是最优的,该问题就具有最优子结 构性质。
  2. 无后效性。当前的若干个状态值一旦确定,则此后过程 的演变就只和这若干个状态的值有关,和之前是采取哪 种手段或经过哪条路径演变到当前的这若干个状态,没 有关系。
  • 18
    点赞
  • 0
    评论
  • 58
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值