算法笔记 第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\n",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\n",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\n",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\n",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\n",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均为原问题的表述。

 

posted @ 2020-02-10 23:39  程序员小应  阅读(230)  评论(0编辑  收藏  举报