ACM 动态规划例题详解

来源:互联网 发布:乐视tv下载软件 编辑:程序博客网 时间:2024/05/21 07:51
描述
73   88   1   02   7   4   44   5   2   6   5(图1)

图1给出了一个数字三角形。从三角形的顶部到底部有很多条不同的路径。对于每条路径,把路径上面的数加起来可以得到一个和,你的任务就是找到最大的和。

注意:路径上的每一步只能从一个数走到下一层上和它最近的左边的那个数或者右边的那个数。
输入
输入的是一行是一个整数N (1 < N <= 100),给出三角形的行数。下面的N行给出数字三角形。数字三角形上的数的范围都在0和100之间。
输出
输出最大的和。
样例输入
573 88 1 0 2 7 4 44 5 2 6 5
样例输出
30


解题思路:(递归)

以D( r, j)表示第r 行第 j 个数字(r,j 都从1 开始算),以MaxSum(r, j) 代表从第 r 行的第 j 个数字到底边的最佳路径的数字之和,则本题是要求 MaxSum(1, 1) 。

从某个D(r, j)出发,显然下一步只能走D(r+1, j)或者D(r+1, j+1)。如果走D(r+1, j),那么得到的MaxSum(r, j)就是MaxSum(r+1, j) + D(r, j);如果走D(r+1, j+1),那么得到的MaxSum(r, j)就是MaxSum(r+1, j+1) + D(r, j)。所以,选择往哪里走,就看MaxSum(r+1, j)和MaxSum(r+1, j+1)哪个更大了。

#include<stdio.h>#define MAX_NUM 100int D[MAX_NUM + 10][MAX_NUM + 10];int N;int MaxSum(int r, int j) {if(r == N){return D[r][j];}int nSum1 = MaxSum(r+1, j);int nSum2 = MaxSum(r+1, j+1);if(nSum1 > nSum2){return nSum1 + D[r][j];}else{return nSum2 + D[r][j];}}int main(void){int m;scanf("%d",&N);for(int i = 1; i <= N; i++){for(int j = 1; j <= i; j++){scanf("%d",&D[i][j]);}}printf("%d",MaxSum(1,1));return 0;}
但是提交结果超时大哭

上面的程序,效率非常低,在N 值并不大,比如N=100 的时候,就慢得几乎永远算不出结果了。
为什么会这样呢?是因为过多的重复计算。
我们不妨将对MaxSum 函数的一次调用称为一次计算。那么,每次计算MaxSum(r, j)的时候,都要计算一次MaxSum(r+1, j+1),而每次计算MaxSum(r, j+1)的时候,也要计算一次MaxSum(r+1, j+1)。重复计算因此产生。
在题目中给出的例子里,如果我们将MaxSum(r, j)被计算的次数都写在位置(r, j),那么就能得到下面的三角形:
                                                                            
                                                                    
1、从上图可以看出,最后一行的计算次数总和是16,倒数第二行的计算次数总和是8。不难总结出规律,对于N行的三角形,总的计算次数是2^0+2^1+2^2+…+2^(N-1)=2^N-1。当N=100 时,总的计算次数是一个让人无法接受的大数字。
2、既然问题出在重复计算,那么解决的办法,当然就是,一个值一旦算出来,就要记住,以后不必重新计算。即第一次算出MaxSum(r,j)的值时,就将该值存放起来,下次再需要计算MaxSum(r,j)时,直接取用存好的值即可,不必再次调用MaxSum进行函数递归计算了。这样,每个MaxSum(r,j)都只需要计算1次即可,那么总的计算次数(即调用MaxSum函数的次数)就是三角形中的数字总数,即1+2+3+…+N= N(N+1)/2。
3、如何存放计算出来的MaxSum(r,j)值呢?显然,用一个二维数组aMaxSum[N][N]就能解决。aMaxSum[r][j]就存放MaxSum(r,j)的计算结果。下次再需要MaxSum(r,j)的值时,不必再调用MaxSum函数,只需直接取aMaxSum[r][j]的值即可。
#include <stdio.h>#include <memory.h>#define MAX_NUM 100int D[MAX_NUM + 10][MAX_NUM + 10];int N;int aMaxSum[MAX_NUM + 10][MAX_NUM + 10];int MaxSum( int r, int j){    if( r == N )    return D[r][j];    if( aMaxSum[r+1][j] == -1 ) //如果MaxSum(r+1, j)没有计算过        aMaxSum[r+1][j] = MaxSum(r+1, j);    if( aMaxSum[r+1][j+1] == -1)        aMaxSum[r+1][j+1] = MaxSum(r+1, j+1);    if( aMaxSum[r+1][j] > aMaxSum[r+1][j+1] )        return aMaxSum[r+1][j] +D[r][j];    return aMaxSum[r+1][j+1] + D[r][j];}int main(void){    int m;    scanf("%d", & N);    //将 aMaxSum 全部置成-1, 开始时所有的 MaxSum(r, j)都没有算过    memset(aMaxSum, -1, sizeof(aMaxSum));    for( int i = 1; i <= N; i ++ )        for( int j = 1; j <= i; j ++ )            scanf("%d", & D[i][j]);    printf("%d", MaxSum(1, 1));    return 0;}
    这种将一个问题分解为子问题递归求解,并且将中间结果保存以避免重复计算的办法,就叫做“动态规划”。动态规划通常用来求最优解,能用动态规划解决的求最优解问题,必须满足,最优解的每个局部解也都是最优的。以上题为例,最佳路径上面的每个数字到底部的那一段路径,都是从该数字出发到达到底部的最佳路径。
实际上,递归的思想在编程时未必要实现为递归函数。在上面的例子里,有递推公式:



因此,不需要写递归函数,从aMaxSum[N-1]这一行元素开始向上逐行递推,就能求得aMaxSum[1][1]的值了。

动态规划解题的一般思路 
1、许多求最优解的问题可以用动态规划来解决。
2、首先要把原问题分解为若干个子问题。注意单纯的递归往往会导致子问题被重复计算,用动态规划的方法,子问题的解一旦求出就要被保存,所以每个子问题只需求解一次。
3、子问题经常和原问题形式相似,有时甚至完全一样,只不过规模从原来的n变成了n-1,或从原来的n×m变成了n×(m-1)……等等。
4、找到子问题,就意味着找到了将整个问题逐渐分解的办法。
5、分解下去,直到最底层规模最小的的子问题可以一目了然地看出解。
6、每一层子问题的解决,会导致上一层子问题的解决,逐层向上,就会导致最终整个问题的解决。
7、如果从最底层的子问题开始,自底向上地推导出一个个子问题的解,那么编程的时候就不需要写递归函数。
n用动态规划解题时,将和子问题相关的各个变量的一组取值,称之为一个“状态”。一个“状态”对应于一个或多个子问题,所谓某个“状态”下的“值”,就是这个“状态”所对应的子问题的解。
n比如数字三角形,子问题就是“从位于(r,j)数字开始,到底边路径的最大和”。这个子问题和两个变量r和j相关,那么一个“状态”,就是r,j 的一组取值,即每个数字的位置就是一个“状态”。该“状态”所对应的“值”,就是从该位置的数字开始,到底边的最佳路径上的数字之和。
n定义出什么是“状态”,以及在该 “状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的“状态”,求出另一个“状态”的“值”。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。

0 0