算法15——动态规划专题

文章目录

  • 一、动态规划的递归和递推写法
      • 1、递归写法
      • 2、递推写法
      • 3、分治、贪心与动态规划
  • 二、最大连续子序列和
  • 三、最长不下降子序列(LIS)
  • 四、最长公共子序列(LCS)
  • 五、最长回文子串
  • 六、背包问题
      • 1、0-1背包问题
      • 2、完全背包问题


一、动态规划的递归和递推写法

动态规划: 将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样的子问题的时候就可以不用重复计算。

1、递归写法

以斐波拉契数列为例:

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)+F(2)。这个时候,如果不采取措施,那么F(3)就会计算两次。为了避免重复计算,可以开一个一维数组dp,用来保存已经计算过的结果。并且dp[n]=-1表示F(n)当前还没有计算过:

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];
    }
}

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

2、递推写法

算法15——动态规划专题_第1张图片
以如图的数塔问题为例,现在从最顶层走到最底层,每次只能走向下一层连接数字中的一个,请问最后将路径上的数字相加之后得到的数值最大是多少?

用一个二维数组f,其中f[ i ] [ j ] 表示第i层的第j个数字,比如f[1][1]=13。首先,从第一层的13开始,按照13->11->7的路径到达7后,之后要枚举7到达底层的最大和。从13->8->7到达7之后,也要枚举7到达底层的最大和。因此,不妨设一个二维数组dp,dp[3][2]表示7到达底层的最大和

从顶部开始求解,那么其实dp[1][1] = f[1][1]+max{dp[2][1],dp[2][2]}

因此,可以得到递推公式:dp[i][j] = f[i][j]+max{dp[i+1][j],dp[i+1][j+1]}。而dp[n][j] = f[n][j]是递推的边界。而递推的方法总是从这些边界开始,然后不断向上求出每一个dp的值,最后得到想要的答案。

#include
#include
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层开始,不断向上计算
    for(int i = n-1 ; i >= 1 ; i--){
        for(int j = 1 ; j <= i ; j++){
            dp[i][j] = f[i][j] + max(dp[i+1][j],dp[i+1][j+1]);
        }
    }
    printf("%d",dp[1][1]);
    return 0;
}

算法15——动态规划专题_第2张图片
显然,也可以用递归来实现。区别在于:递推的计算方式是自底而上,而递归是自顶向下。

如果一个问题的最优解可以由其子问题的最优解有效构造出来,那么称这个问题拥有最优子结构。最优子结构保证动态规划中元有问题的最优解可以由子问题的最优解推导出来。因此,一个问题必须要有最优子结构才能够用动态规划去解决问题。

综上所述,一个问题必须要有重叠子问题和最优子结构,才能够用动态规划去解决。

3、分治、贪心与动态规划

分治与动态规划:都是将问题分解为子问题,但是分治并没有重叠子问题。
贪心与动态规划:贪心并不要求等待子问题全部求解完毕之后再选择使用哪一个,而是用一次策略去选择一个子问题去求解,没有被选择到的子问题就被抛弃了。

二、最大连续子序列和

题目:给定一个数字序列A1,A2,…,An,求i,j,使得Ai+…+Aj最大,输出这个最大和

样例:-2 11 -4 13 -5 -2 显然11+(-4)+13=20位最大和

思路:

  1. 令状态dp[i]表示以A[i]最为结尾的最大子序列的和(A[i]必须是连续子序列的末尾)
dp[0]=-2
dp[1]=11
dp[2]=7(11-4)
sp[3]=20(11-4+13)
dp[4]=15(11-4+13-5)
dp[5]=13(11-4+13-5-2)
  1. 因为dp[i]要求必须以A[i]为结尾,因此就有两种情况:

    (1)这个最大和的连续序列只有一个元素:A[i]
    (2)这个最大和的连续序列有多个元素,最大和就是dp[i-1]+A[i]
    因此,dp[i]=max{A[i],dp[i-1]+A[i]}

#include
#include
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]);
    }
    int k = 0;
    for(int i = 1 ; i < n ; i++){
        if(dp[i]>dp[k]){
            k=i;
        }
    }
    printf("%d",dp[k]);
    return 0;
}

三、最长不下降子序列(LIS)

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

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

令dp[i]表示以A[i]结尾的最长不下降子序列的长度,这样对于A[i]来说就会有两种情况:

  1. 如果A[i]之前元素A[j],使得A[j]<=A[i]且dp[j]+1>dp[i],那么就让A[I]跟在A[j]后面形成一条更长的子序列,即令dp[i] = dp[j]+1
  2. 如果A[i]之前的元素逗比A[i]大,那么A[i]只能自己形成一条LIS,长度为1

比如说有一个序列A={1,5,-1,3},下标从1开始,现在如何直到以A[4]结尾的最长子序列呢:依次判断A[1]、A[2]、A[3],经判断,A[4]可以跟在A[1]或者A[3]后面形成LIS,长度为2。又如序列{1,2,3,-1},这里的A[4]并不能跟在任何一个元素后面,因此LIS就为-1一个元素,长度为1

代码:

#include
#include
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;//标记最大的子序列长度
    for(int i = 1 ; i <= n ; 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;
        }
        ans = max(ans , dp[i]);
    }
    printf("%d",ans);
    return 0;
}

算法15——动态规划专题_第3张图片

四、最长公共子序列(LCS)

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

如:字符串"sadstory"和"adminsorry"的最长公共子序列为"adsory"

思路:令dp[i][j]表示字符串A的i号位和字符串B的j号位之间的LCS长度,下标从1开始,如dp[4][5]表示"sads"和“admin”的LCS长度,那么可能就会有两种情况:

  1. 若A[i]==B[j],那么字符串A和字符串B之间的公共子序列长度增加一位,即dp[i][j] = dp[i-1][j-1]+1,比如说dp[4][6]表示"sads"和"admins"的LCS长度,而A[4]==B[6],那么dp[4][6]=dp[3][5]+1,即为3
  2. 若A[i]!=B[j],则字符串A的i号位与字符串B的j号位之间的LCS无法延长,那么dp[i][j]将会继承dp[i-1][j]和dp[i][j-1]之间的较大值。比如dp[3][3]为"sad"和"adm"的LCS长度,A[3]不等于B[3],那么继承"sa"、“adm"的LCS或者"sad”、“ad"的LCS的较大值,即"sad”、“ad”,为2

代码:

#include
#include
#include
using namespace std;
const int N = 100;
char A[N],B[N];
int dp[N][N];
int main(){
    int n;
    gets(A+1);//从下标为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;
}

算法15——动态规划专题_第4张图片

五、最长回文子串

题目:给出一个字符串S,求出S的最长回文子串的长度
例如:字符串“PATZJUJZTACCBCC”的最长回文子串为"ATZJUJZTA",长度为9

思路:令dp[i][j]表示S[i]置S[j]的子串是否是回文子串,是则为1,不是则为0。这样就会有两种情况:

  1. 若S[i] = S[j],那么只要S[i+1]和S[j-1]是回文子串,那么S[i]到S[j]之间就为回文字串
  2. 若S[i]!=S[j],那么S[i]到S[j]一定不是回文子串

代码:

#include
#include
const int maxn = 1010;
char S[maxn];
int dp[maxn][maxn];
int main(){
    gets(S);
    int len = strlen(S);
    int 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;
}

六、背包问题

1、0-1背包问题

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

样例:

5  8//n=5 V=8
3 5 1 2 2 //w[i]
4 5 2 1 3 //c[i]

思路:令dp[i][v]表示前i件物品恰好装入容量为v的背包中所得的最大价值。对于每件物品:

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

代码:

#include
#include
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;
}

2、完全背包问题

和0-1背包问题相区别的就是,每种物品都有无穷件

那么对于每件物品:

  1. 不放第i件物品,那么dp[i][v] = dp[i-1][v]
  2. 放第i件物品,那么要转移到dp[i][v-w[i]]

即:dp[i][v] = max(dp[i-1][v],dp[i][v-w[i]]+c[i])

你可能感兴趣的:(算法,算法,动态规划,c++)