• 如何运用动态规划解题


    本文内容整理自中国大学MOOC郭炜老师的程序设计与算法(二)


    首先由数字三角形问题出发( ̄︶ ̄)↗ ,题目描述如下

             7 
               3   8 
             8   1   0 
          2   7   4   4 
       4   5   2   6   5

    在上面的数字三角形中寻找一条从顶部到底部的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或右下走。只需要求出这个最大和即可,不必给出具体路径。(三角形的行数大于1小于100,数字为0~99)

    输入:

    第一行是数字三角形的行数,接下来 n 行是数字三角形中的数字。  

      比如:

      5

      7

      3  8

      8 1 0

      2 7 4 4

      4 5 2 6 5  

    输出:

      最大和

    分析:

    1)首先最容易想到的是递归算法,第n行第n列到底部的路径最大和为:自身+第n+1行最大的;临界条件:最后一行的路径最大和即为自身。

    递归参考代码

    ///递归
    #include <iostream>
    
    using namespace std;
    int n;
    int MaxSum(int i,int j,int a[][101]);
    int main()
    {
        int a[101][101];
    
        cin>>n;
        for(int i=0;i<n;i++){
            for(int j=0;j<i+1;j++){
                cin>>a[i][j];
            }
        }
        cout<<MaxSum(0,0,a);
        return 0;
    }
    int MaxSum(int i,int j,int a[][101]){
    if(i==n-1){
        return a[i][j];
    }else if(i<n-1){
        int x=MaxSum(i+1,j,a);
        int y=MaxSum(i+1,j+1,a);
        return max(x,y)+a[i][j];
    }
    
    }

    2)可是该递归方法有很多很多重复的计算,所以我们想到可以记下已经算过的,当用到时直接拿过来用,这样就避免了大量的重复运算。

    ///记忆递归型动规
    #include <iostream>
    
    using namespace std;
    int n;
    int b[101][101];//记忆数组用来存已经计算出来的结果
    int MaxSum(int i,int j,int a[][101]);
    int main()
    {
        int a[101][101];
    
        cin>>n;
        for(int i=0;i<n;i++){
            for(int j=0;j<i+1;j++){
                cin>>a[i][j];
                b[i][j]=-1;//初始化为-1
            }
        }
        cout<<MaxSum(0,0,a);
        return 0;
    }
    int MaxSum(int i,int j,int a[][101]){
    if(b[i][j]!=-1){//如果不等于-1说明已经计算过,所以直接返回
        return b[i][j];
    }
    else
    {
        if(i==n-1){
        return b[i][j]=a[i][j];
        }
        else if(i<n-1)
        {
        int x=MaxSum(i+1,j,a);
        int y=MaxSum(i+1,j+1,a);
        return b[i][j]=max(x,y)+a[i][j];
        }
    }
    }

    3)其实呢,一般都是用递推的方式来推出动态规划算法的,用循环递推不但比递归快,而且还能节省空间,写起来也比较方便,但是还是基于递归的思想才能递推出递推式。

    第一步:在草稿纸或者脑子里想出解决该题的递归函数。

    第二步:递归函数有n个参数,就定义一个n维的数组,数组的下标是,递归函数参数的取值范围,数组元素的值是递归函数的返回值。

    第三步:根据边界条件,初始化n维数组。

    第四步:得到递推式,根据递推式逐步填充数组,相当于递归的逆过程。

    如本题:

    ///第一步写递归函数已经在上面写完了
    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    int main()
    {
        int i,j;
        int n;
        int D[101][101];
        ///第二步,因为递归函数有行i和列j两个参数所以定义一个二维数组
        int maxSum[101][101];
        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];
        }
    
        /**第四步,根据递归函数推出递推式
                    a[i][j],i==n-1;
        dp[i][j]=
                    max(dp[i+1][j],dp[i+1][j+1])+a[i][j];
        */
        ///得到递推式,把递推式写出来
        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;
    }

    4)往往由递推式写出的程序可以进行空间优化,如样例输入

    ①用一维记忆数组代替多维记忆数组

    其一维记忆数组为maxSum={4,5,2,6,5}

    那么算n-1行经过2的路径的最大和,在4和5中选出最大数,将结果存在4的位置,即maxSum={7,5,2,6,5}。这样并不会影响其他的路径和,比如第n-1行经过7的路径的最大和,是在5和2中选出最大数。

    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    int main()
    {
        int i,j;
        int n;
        int D[101][101];
        int maxSum[101];///此处由二维数组变成了一维数组
        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[i]=D[n][i];
        }
    
        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];///取i下面和右面的最大值+本身,存在i下面数的位置
            }
        }
        cout<<maxSum[1]<<endl;
        return 0;
    }

    ②删去一维记忆数组,用保存输入数据的数组的最后一行当作记忆数组保存计算结果

    #include <iostream>
    #include <algorithm>
    using namespace std;
    
    int main()
    {
        int i,j;
        int n;
        int D[101][101];
        cin>>n;
        for(i=1;i<=n;i++){
            for(j=1;j<=i;j++){
                cin>>D[i][j];
            }
        }
        //因为一维记忆数组初始化是和D数组第n行一样,而且D数组第n行只在n-1才会用到
        for(int i=n-1;i>=1;i--){
            for(int j=1;j<=i;j++){
                    D[n][j]=max(D[n][j],D[n][j+1])+D[i][j];///取i下面和右面的最大值+本身,存在第n行
            }
        }
        cout<<D[n][1]<<endl;
        return 0;
    }

    4)那么什么样的问题适合用动态规划呢?

    ①问题具有最优子结构性质。

    如果问题的最优解所包含的子问题的解也是最优的,我们就称该问题具有最优子结构性质。  

    ②无后效性。

    当前的若干状态的值一旦确定,则此后过程的演变就只和这若干个状态的值有关,和之前是采取哪种手段或经过哪条路径演变到当前的这若干个状态,没有关系。

  • 相关阅读:
    POJ 1095 Trees Made to Order 最详细的解题报告
    Producter and Consumer
    How to use the function of bind
    How to use the functions of apply and call
    Configurate vim tool
    #4713. 方程
    #4709. 树
    #4718. 管理
    #4710. 并
    #4707. 点分治
  • 原文地址:https://www.cnblogs.com/LuRenJiang/p/7360498.html
Copyright © 2020-2023  润新知