如何运用动态规划解题

本文内容整理自中国大学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)那么什么样的问题适合用动态规划呢?

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

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

②无后效性。

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

时间: 2024-10-06 17:36:00

如何运用动态规划解题的相关文章

动态规划解题的一般思路

*  许多求最优解的问题可以用动态规划来解决. *  首先要把原问题分解为若干个子问题.注意单纯的递归往往会导致子问题被重复计算,用动态规划的方法,子问题的解一旦求出就要被保存,所以每个子问题只需求解一次. *  子问题经常和原问题形式相似,有时甚至完全一样,只不过规模从原来的n 变成了n-1,或从原来的n×m 变成了n×(m-1) ……等等. *  找到子问题,就意味着找到了将整个问题逐渐分解的办法. *  分解下去,直到最底层规模最小的的子问题可以一目了然地看出解. *  每一层子问题的解决

poj1651 最优矩阵乘法动态规划解题

题目描述: 有若干个矩阵{Ai},元素都为整数且已知矩阵大小. 如果要计算所有矩阵的乘积A1 * A2 * A3 .. Am,最少要多少次整数乘法? 输入 第一行一个整数n(n <= 100),表示一共有n-1个矩阵.第二行n个整数B1, B2, B3... Bn(Bi <= 100),第i个数Bi表示第i个矩阵的行数和第i-1个矩阵的列数.等价地,可以认为第j个矩阵Aj(1 <= j <= n - 1)的行数为Bj,列数为Bj+1. 输出 一个整数,表示最少所需的乘法次数 采用动

sicily 1176 two ends 动态规划解题

1176. Two Ends Constraints Time Limit: 1 secs, Memory Limit: 64 MB Description In the two-player game "Two Ends", an even number of cards is laid out in a row. On each card, face up, is written a positive integer. Players take turns removing a c

【算法学习笔记】27.动态规划 解题报告 SJTU_OJ 1254 传手绢

1254. 传手绢 Description 活动的时候,老师经常带着同学们一起做游戏.这次,老师带着同学们一起传手绢. 游戏规则是这样的:n个同学站成一个圆圈,其中的一个同学手里拿着手绢,当老师吹哨子时开始传,每个同学可以把手绢传给自己左右的两个同学中的一个(左右任意),当老师在此吹哨子时,游戏停止,此时,拿着手绢的那个同学要给大家表演一个节目. abc提出一个有趣的问题:有多少种不同的传手绢方法可以使得从abc手里开始传的手绢,传了m次以后,又回到abc手里.两种传手绢方法被视作不同的方法,当

[Leetcode188] 买卖股票的最佳时机IV 动态规划 解题报告

题源:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-iv/ 本题代码: 1 /** 2 * @author yuan 3 * @version 0.1 4 * @date 2019/4/5 5 */ 6 public class Leetcode188 { 7 8 private int getBetterBuy(int[] buy, int[] sell, int price, int i) { 9 if (i

动态规划的泛式解题思路

写在前面:笔者将参加16年北大的暑期acm的集训,考虑到拿到比较好的学习效果,笔者开这个专栏用于整理一下15年集训的课件和资料.考虑到时间非常有限(一个月),加上考试缠身,很难以做到每个专题都结合例题代码,因此对于笔者比较熟悉的专题(dp.数论.组合.博弈)主要以整理思想和方法为主,对于没学过的专题以建立裸体模型为主. 利用动态规划解题的泛式思路: 1. 将原问题分解为子问题  ? 把原问题分解为若干个子问题,子问题和原问题形式相同 或类似,只不过规模变小了.子问题都解决,原问题即解 决(数字三

动态规划——入门篇

动态规划相信大家都知道,动态规划算法也是新手在刚接触算法设计时很苦恼的问题,有时候觉得难以理解,但是真正理解之后,就会觉得动态规划其实并没有想象中那么难.网上也有很多关于讲解动态规划的文章,大多都是叙述概念,讲解原理,让人觉得晦涩难懂,即使一时间看懂了,发现当自己做题的时候又会觉得无所适从.我觉得,理解算法最重要的还是在于练习,只有通过自己练习,才可以更快地提升.话不多说,接下来,下面我就通过一个例子来一步一步讲解动态规划是怎样使用的,只有知道怎样使用,才能更好地理解,而不是一味地对概念和原理进

LeetCode -- Word Break 动态规划,详细理解

Given a string s and a dictionary of words dict, determine if s can be segmented into a space-separated sequence of one or more dictionary words. For example, given s = "leetcode", dict = ["leet", "code"]. Return true because

教你彻底学会动态规划——进阶篇

在我的上一篇文章中已经详细讲解了动态规划的原理和如何使用动态规划解题.本篇文章,我将继续通过例子来让大家更加熟练地使用动态规划算法. 话不多说,来看如下例题,也是在动态规划里面遇到过的最频繁的一个题,本题依然来自于北大POJ:     最长公共子序列(POJ1458) 给出两个字符串,求出这样的一个最长的公共子序列的长度:子序列中的每个字符都能在两个原串中找到, 而且每个字符的先后顺序和原串中的先后顺序一致.     Sample Input abcfbc abfcab programming