• 算法笔记 第11章 提高篇(5) --动态规划专题 学习笔记


    11.1 动态规划的递归写法和逆推写法

    动态规划没有固定的写法、极其灵活,常常需要具体问题具体分析。

    11.1.1 什么是动态规划

    动态规划将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。

    11.1.2 动态规划的递归写法

    如下是斐波那契数列的常规写法

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

    但这种写法会涉及很多重复的计算,当n == 5时,可以得到F(5) = F(4) + F(3),接下来在计算F(4) 时又会有F(4) = F(3) + F(2).这时,F(3)被计算了两次。如果n很大,重复计算的次数将难以想象。

    可以开一个一维数组dp[n]用以保存已经计算过的结果,其中dp[n]记录F(n)的结果,并用dp[n] = -1表示F(n)当前还没有被计算过。

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

    如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题。动态规划通过记录重叠子问题的解,来使下次碰到相同的子问题时直接使用之前记录的结果,以此避免大量重复计算。

    11.1.3 动态规划的递推写法

    求上图中路径上所有数字相加后得到的和最大是多少?

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int maxn = 1000;
    int f[maxn][maxn],dp[maxn][maxn];
    int main(){
        int n;
        scanf("%d",&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];
        } 
        //从第n-1层不断往上计算出dp[i][j]
        for(int i = n - 1; i >= 1 ;i--){
            for(int j =1; j <= i;j++){
                //状态转移方程
                dp[i][j] = max(dp[i+1][j],dp[i+1][j+1]) + f[i][j]; 
            }
        }
        printf("%d
    ",dp[1][1]); 
        return 0;
    }

    通过例子再引申出一个概念:如果一个问题的最优解可以由其子问题的最优解有效地构造出来,那么称这个问题拥有最优子结构。

    一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。

    11.2 最大连续子序列和

    给定一个数字序列A1,A2,...,An,求i,j (1 <= i <= j <= n),使得Ai + ..... + Aj最大,输出这个最大和。

    样例:

    -2  11  -4  13  -5  -2

    显然11+(-4) + 13 = 20为和最大的选取情况,因此最大和为20.

    用动态规划求解:

    步骤1:令状态dp[i]表示以A[i]作为末尾的连续序列的最大和。以样例为例:序列-2 11 -4 13 -5 -2,下标分别记为0,1,2,3,4,5,那么

    dp[0] = -2;

    dp[1] = 11;

    dp[2] = 7 (11 + (-4) = 7);

    dp[3] = 20 (11 + (-4) + 13 = 20)

    dp[4] = 15

    dp[5] = 13

    步骤2: 因为dp[i]要求是必须以A[i]结尾的连续序列,那么只有两种情况:

    ①这个最大和的连续序列只有一个元素,即以A[i]开始,以A[i]结尾。

    ②这个最大和的连续序列有多个元素,即从前面某处A[p] 开始,一直到A[i]结尾。

    可以得到状态转移方程:dp[i] = max{A[i] , dp[i-1] + A[i] }

    因此,实现代码如下:

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int maxn = 10010;
    int A[maxn],dp[maxn];
    int main(){
        int n;
        scanf("%d",&n);
        for(int i = 0; i < n;i++){
            scanf("%d",&A[i]);
        }
        //边界 
        dp[0] = A[0];
        for(int i = 1; i < n;i++){
            //状态转移方程
            dp[i] = max(A[i],dp[i-1] + A[i]); 
        }
        //dp[i]存放以A[i]结尾的连续序列的最大和,需要遍历i得到最大的才是结果
        int k = 0;
        for(int i = 1;i < n;i++){
            if(dp[i] > dp[k]){
                k = i;
            }
        } 
        printf("%d
    ",dp[k]);
        return 0;
    }

    状态的无后效性:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或者若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策。
    动态规划的核心:如何设计状态和状态转移方程

    11.3 最长不下降子序列(LIS)

     最长不下降子序列是这样一个问题:在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。

    例如,现有序列A={1,2,3,-1,-2,7,9}(下标从1开始),它的最长不下降子序列是{1,2,3,7,9},长度为5.

    令dp[i]表示以A[i]结尾的最长不下降子序列长度。

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int N = 100;
    int A[N],dp[N];
    int main(){
        int n;
        scanf("%d",&n);
        for(int i = 1; i <= n;i++){
            scanf("%d",&A[i]);
        }
        int ans = -1; //记录最大的dp[i] 
        for(int i = 1;i<=n;i++){    //按顺序计算出dp[i]的值 
            dp[i] = 1;//边界初始条件(即先假设每个元素自成一个子序列) 
            for(int j=1;j<i;j++){
                if(A[i] >= A[j] && (dp[j]+1 > dp[i])){
                    dp[i] = dp[j] + 1;    //状态转移方程,用以更新dp[i] 
                }
            }
            ans = max(ans,dp[i]);
        }
        printf("%d",ans);
        return 0;
    }

    11.4 最长公共子序列(LCS)

    最长公共子序列的问题描述为:给定两个字符串(或数字序列)A和B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)。

    如样例所示,字符串"sadstory"与"adminstory"的最长公共子序列为“adsory”,长度为6.

    令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始)

    状态转移方程:

    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    using namespace std;
    const int N = 100;
    char A[N],B[N];
    int dp[N][N];
    int main(){
        int n;
        gets(A+1);
        gets(B+1);
        int lenA = strlen(A+1);
        int lenB = strlen(B+1);
        //边界
        for(int i = 0; i <= lenA;i++){
            dp[i][0] = 0;
        } 
        for(int j = 0; j <= lenB;j++){
            dp[0][j] = 0;
        }
        //状态转移方程
        for(int i=1; i <= lenA;i++){
            for(int j=1; j <= lenB;j++){
                if(A[i] == B[j]){
                    dp[i][j] = dp[i-1][j-1] + 1;
                }else{
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }
            }
        } 
        printf("%d
    ",dp[lenA][lenB]);
        return 0;
    }

    11.5 最长回文子串

    最长回文子串的问题描述:给出一个字符串S,求S的最长回文子串的长度。  

    样例:字符串“PATZJUJZTACCBCC”的最长回文子串为"ATZJUJZTA",长度为9.

    #include<cstdio>
    #include<cstring>
    const int maxn = 1010;
    char S[maxn];
    int dp[maxn][maxn];
    int main(){
        gets(S);
        int len = strlen(S),ans = 1;
        memset(dp,0,sizeof(dp));
        for(int i = 0;i < len;i++){
            dp[i][i] = 1;
            if(i < len -1){
                if(S[i] == S[i+1]){
                    dp[i][i+1] = 1;
                    ans = 2;
                }
            }
        }
        
        for(int L = 3; L <= len;L++){
            for(int i=0;i+L-1<len;i++){
                int j = i + L - 1;
                if(S[i] == S[j] && dp[i+1][j-1] == 1){
                    dp[i][j] = 1;
                    ans = L;
                }
            }
        }
        printf("%d
    ",ans);
        return 0;
    }

     11.6 DAG最长路

    问题:给定一个有向无环图,怎样求解整个图的所有路径中权值之和最大的那条。

    令dp[i]表示从i号顶点出发能获得的最长路径长度,这样所有dp[i]的最大值就是整个DAG的最长路径长度。

    11.7 背包问题

    11.7.1 多阶段动态规划问题

    多阶段动态规划问题:一个动态规划可解的问题,可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关。

    对这种问题,只需要从第一个问题开始,按照阶段的顺序解决每个阶段中状态的计算,就可以得到最后一个阶段中的状态的解。01背包问题就是这样一个例子。

    11.7.2 01背包问题

    问题:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有1件。

    令dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。怎么求解dp[i][v]呢?

    两种策略:

    ①不放第i件物品,问题转化为前 i-1 件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v].

    ②放第i件物品,那么问题转化为前i-1件物品恰好装入v - w[i]的背包中所能获得的最大价值,也即dp[i-1][v-w[i]] + c[i]

    因此状态转移方程为: dp[i][v] = max{dp[i-1][v] , dp[i-1][v-w[i]] +  c[i] },(1 <= i <= n,w[i] <= v <= V)

    #include<cstdio>
    #include<algorithm>
    using namespace std;
    const int maxn = 100;
    const int maxv = 1000;
    int w[maxn],c[maxn],dp[maxv];
    int main(){
        int n,V;
        scanf("%d%d",&n,&V);
        for(int i = 1; i <= n; i++){
            scanf("%d",&w[i]);
        } 
        for(int i = 1; i <= n ;i++){
            scanf("%d",&c[i]);
        }
        for(int v = 0; v <= V;v++){
            dp[v] = 0;
        }
        for(int i = 1; i <= n;i++){
            for(int v = V; v>=w[i];v--){
                dp[v] = max(dp[v],dp[v - w[i]] + c[i]); 
            }
        }
        int max = 0;
        for(int v = 0; v <= V; v++){
            if(dp[v] > max){
                max = dp[v];
            }
        }
        printf("%d
    ",max);
        return 0;
    }

    11.7.3 完全背包问题

    完全背包问题的叙述如下:有n种物品,每种物品的单件重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。

    一维形式的状态转移方程与01背包完全相同,唯一的区别在于这里的v的枚举顺序是正向枚举,而01背包的一维形式中v必须是逆向枚举。

    完全背包的一维形式代码如下:

    for(int i = 1; i <= n;i++){
      for(int v = w[i]; v <= V;v++){
        dp[v]
    = max(dp[v],dp[v - w[i]] + c[i]);
      }
    }

    11.8总结

    当题目与序列或字符串(记为A)有关时,可以考虑把状态设计成下面两种形式,然后根据端点特点去考虑状态转移方程。

      ① 令dp[i]表示以A[i]结尾(或开头)的XXX。

      ②令dp[i][j]表示A[i]至A[j]区间的XXX。

      其中XXX均为原问题的表述。

  • 相关阅读:
    ​《数据库系统概念》5-连接、视图和事务
    ​《数据库系统概念》4-DDL、集合运算、嵌套子查询
    ​《数据库系统概念》3-主键、关系运算
    ​《数据库系统概念》2-存储、事务等的简介
    ​《数据库系统概念》1-数据抽象、模型及SQL
    Web API与JWT认证
    巨杉Tech | 十分钟快速搭建 Wordpress 博客系统
    巨杉内核笔记(一)| SequoiaDB 会话(session)简介
    SequoiaDB巨杉数据库入门:快速搭建流媒体服务器
    微服务?数据库?它们之间到底是啥关系?
  • 原文地址:https://www.cnblogs.com/coderying/p/12293481.html
Copyright © 2020-2023  润新知