动态规划学习笔记

一.动态规划的含义

动态规划是用来解决一类最优化问题的算法。它将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。为提高动态规划的效率,还需要将每个子问题的最优解记录下来,省去不必要的重复计算。

二.动态规划的两种写法

递归写法:

典型的例子就是斐波那契数列,定义为:F(0)=F(1)=1,F(n)=F(n-1)+F(n-2)(n不小于2)。递归写法如下:

int F(int n){
    if(n == 0 || n == 1) return 1;
    else return F(n - 1) + F(n - 2);
}

这样将一个较大的整数不断分解成若干个小的整数,从而求出解。但在这个过程中,有些整数的值可能会被重复计算,例如求解F(5)时,会分解成求解F(4)和F(3)的值,而求解F(4)时又会去求F(3)的值,为了避免重复计算,可用一个一维数组dp,用来保存已经计算过的结果,其中dp[n]记录的就是F(n)的结果,因此代码修改如下:

int dp[maxn] = {0};

int F(int n){
    if(n == 0 || n == 1) return 1;
    if(dp[n] != 0) return dp[n];
    else{
        dp[n] = F(n - 1) + F(n - 2);
        return dp[n];
    }
}

递推写法:

以数塔问题为例,如下图所示,将一些数字排成数塔形状,其中第一层有一个数字,第二层有两个数字……第n层有n个数字。现在要从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个,求最后将路径上所有数字相加后得到的和最大是多少?

 

 如果采用暴力解,穷举所有路径情况,然后筛选出最大值,那么时间复杂度为O(2^n),当n很大时就会超时,所以必须采用动态规划的方式来解决这个问题。

先将所有的数字存入二维数组f中,即f[i][j]表示第i层第j个数字,在上图中就是f[1][1]=5,f[2][1]=8,……,f[5][5]=4。再开一个二维数组来记录每个子问题的最优解,即二维数组dp,其中dp[i][j]代表第i层第j个数字到达底层的最优解,由此可以得出一个关键等式,状态转移方程

dp[i][j]=max(dp[i+1][j], dp[i+1][j+1])+f[i][j]

其中max表示两者中的较大值。那么再考虑边界问题,当来到了最底层,即i=n时,此时每个元素走到最底层的路径就是元素的本身数字,即有

dp[n][j]=f[n][j](1≤j≤n)

因此数塔问题的代码如下:

#include <stdio.h>
#include <algorithm>

using namespace std;

int main(){
    int n;
    while(scanf("%d", &n) != EOF){
        int f[n][n], dp[n][n];
        for(int i = 1; i <= n; i++){
            for(int j = 1; j <= i; j++){
                scanf("%d", &f[i][j]);
            }
        }
        for(int j = 1; j <= n; j++){
            dp[n][j] = f[n][j];
        }
        for(int i = n - 1; i >= 1; i--){
            for(int j = n - 1; j >= 1; j--){
                dp[i][j] = max(dp[i + 1][j], dp[i + 1][j + 1]) + f[i][j];
            }
        }
        printf("%d", dp[1][1]);
    }
    return 0;
}

再看另外一个例子:最大连续子序列。给定一个数字序列A1,A2,……,An,求i,j(1≤i≤j≤n),使得Ai+……+Aj最大,输出这个最大和。

样例:

输入:
-2 11 -4 13 -5 -2
输出
20

  样例解释:

11+(-4)+13=20为和最大的选取情况

同样的先来看暴力解,枚举左右端点i、j,需要O(n^2)的复杂度,再计算Ai+……+Aj需要O(n)的复杂度,所以总复杂度为O(n^3),如采用上述方法记录每个子问题的解,使计算时间变为O(1),也仍然需要O(n^2)复杂度,也会有超时的可能性。

因此用动态规划来解这个问题,一维数组dp存储着每个子问题的最优解,即dp[i]表示以A[i]结尾的最大连续序列和,那么初始情况为

dp[0]=A[0]

而关键的状态转移方程

dp[i]=max(A[i], A[i]+dp[i-1])

只需要一次循环遍历,就能得出A[n]数组中所有元素为结尾的最大连续序列和,时间复杂度降低到了O(n)。

#include <stdio.h>
#include <algorithm>

using namespace std;

const int maxn = 10010;

int main(){
    int n;
    int A[maxn], dp[maxn];
    while(scanf("%d", &n) != EOF){
        for(int i = 1; i <= n; i++){
            scanf("%d", &A[i]);
        }
        dp[1] = A[1];
        int ans = dp[1];
        for(int i = 2; i <= n; i++){
            dp[i] = max(A[i], dp[i - 1] + A[i]);
            if(dp[i] > ans) ans = dp[i];
        }
        printf("%d", ans);
    }
    return 0;
}

三.总结

因此,动态规划问题的核心,就是划分复杂问题为容易解决的子问题,并给出每一层问题划分的状态转移方程。如何设计状态和状态转移方程,这才是动态规划最难的地方。

posted @ 2019-03-22 14:39  苏安  阅读(235)  评论(0编辑  收藏  举报