线性dp及其模板

所谓线性DP,就是递推方程是有一个明显的线性关系的,一维线性和二维线性甚至多维都有可能。
动态规划里的每一个状态都是一个多维(1-n维)的状态。
比如说背包问题就是一个二维的问题,如果我们把它画出来的话会是一个二维矩阵的形式。
而我们在求的时候,有一个明显的求的顺序:即一行一行地来求。这样的有线性顺序的叫做线性DP
线性dp,是较常见的一类动态规划问题,其是在线性结构上进行状态转移,这类问题不像背包问题、区间DP等有固定的模板。
线性动态规划的目标函数为特定变量的线性函数,约束是这些变量的线性不等式或等式,目的是求目标函数的最大值或最小值。
因此,除了少量问题(如:LIS、LCS、LCIS等)有固定的模板外,大部分都要根据实际问题来推导得出答案。

【经典模型】

1)序列DP

最大最大子序列和

很简单一个问题,看这题就行,1003 Max Sum

最长上升子序列LIS

      输入n及一个长度为n的数列,求出此序列的最长上升子序列长度。上升子序列指的是对于任意的i<j都满足ai<aj的子序列。(1<=n<=1000,0<=ai<=1000000)

样例输入:

5

4 2 3 1 5

样例输出:

3(最长上升子序列为2, 3, 5)

分析:

:O(nlogn)

设dp[i]表示以i为结尾的最长递增子序列的长度,则状态转移方程为:

dp[i] = max{dp[j]+1}, 1≤j<i,a[j]<a[i].

考虑两个数a[x]和a[y],x<y且a[x]<a[y],且dp[x]=dp[y],当a[t]要选择时,到底取哪一个构成最优的呢?显然选取a[x]更有潜力,因为可能存在a[x]<a[z]<a[y],这样a[t]可以获得更优的值。在这里给我们一个启示,当dp[t]一样时,尽量选择更小的a[x].

按dp[t]=k来分类,只需保留dp[t]=k的所有a[t]中的最小值,设g[k]记录这个值,g[k]=min{a[t],dp[t]=k}。

 这时注意到g的两个特点(重点):

1. g[k]在计算过程中单调上升;          

2. g数组是有序的,g[1]<g[2]<..g[n]。

利用这两个性质,可以很方便的求解:

(1).设当前已求出的最长上升子序列的长度为len(初始时为1),每次读入一个新元素x:

(2).若x>g[len],则直接加入到d的末尾,且len++;(利用性质2)

   否则,在g中二分查找,找到第一个比x小的数g[k],并g[k+1]=x,在这里x≤g[k+1]一定成立(性质1,2)。(如果分析思路看不懂,直接看代码,简单)

代码实现:

 
  1. #include <iostream>

  2. #include <cstdio>

  3. #include <algorithm>

  4. #define maxn 1005

  5. #define INF 99999999

  6. using namespace std;

  7. int n,a[maxn];

  8. int dp[maxn]; //dp[i]:长度为i+1的上升子序列中末尾元素的最小值(不存在的话就是INF)

  9. int main()

  10. {

  11. int i,j;

  12. scanf("%d",&n);

  13. for(i=0;i<n;i++)

  14. scanf("%d",&a[i]);

  15. fill(dp,dp+n,INF); //初始化dp数组为INF

  16. for(i=0;i<n;i++) //找到更新dp[i]的位置并用a[i]更新之

  17. {

  18. *lower_bound(dp,dp+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;

  19. /* for(j=0;j<n;j++) //观察dp数组的填充过程,dp里面保存着最长不下降子序列

  20. printf("%d ",dp[j]);

  21. printf("\n"); */}

  22. printf("%d\n",lower_bound(dp,dp+n,INF)-dp); //第一个INF出现的位置即为LIS长度return 0;}

2)最长公共子序列 LCS

 问题描述:

【问题描述】

  字符序列的子序列是指从给定字符序列中随意地(不一定连续)去掉若干个字符(可能一个也不去掉)后所形成的字符序列。   令给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的一个严格递增下标序列i0,i1,…,ik-1,使得对所有的j=0,1,…,k-1,有xij = yj。例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。
  对给定的两个字符序列,求出他们最长的公共子序列长度,以及最长公共子序列个数。

【输入格式】

第1行为第1个字符序列,都是大写字母组成,以”.”结束。长度小于5000。
第2行为第2个字符序列,都是大写字母组成,以”.”结束,长度小于5000。

【输出格式】

第1行输出上述两个最长公共子序列的长度。
第2行输出所有可能出现的最长公共子序列个数,答案可能很大,只要将答案对100,000,000求余即可。

【输入样例1】

ABCBDAB.
BACBBD.

【输出样例1】

4
7

【时空限制】

1S
256MB

问题分析:

对与第一问,应该都会求,用基本的动态规划就可以搞定。

 问题是怎么求个数。

 令f[i][j]表示a序列到第i个数与b序列到第j个数的LCS长度。

 令g[i][j]表示a序列到第i个数与b序列到第j个数的LCS个数。

 如果a[i]==b[j]

那么f[i][j]==f[i-1][j-1]+1,g[i][j]+=g[i-1][j-1],

如果f[i][j]==f[i-1][j] 那么g[i][j]+=g[i-1][j].

如果f[i][j]==f[i][j-1] 那么g[i][j]+=g[i][j-1].

这很好理解,就是f[i][j]的个数可以是因为a[i],b[j]相同,加1推出来的;也可以是由f[i-1][j]与f[i][j-1]推来(如果长度相等的话)

如果a[i]!=b[j]

那么f[i][j]==max(f[i-1][j],f[i][j-1])

如果f[i][j]==f[i-1][j] 那么g[i][j]+=g[i-1][j].

如果f[i][j]==f[i][j-1] 那么g[i][j]+=g[i][j-1].

如果f[i][j]==f[i-1][j-1]那么g[i][j]-=g[i-1][j-1].

为什么要减呢?首先确定一点,如果a[i]!=b[j] ,必然f[i-1][j-1]≤max(f[i-1][j],f[i][j-1])

当取等号时,必然是f[i-1][j]==f[i][j-1]时。

 又因为g[i-1][j]由g[i-2][j]与f[i-1][j-1]推来,g[i][j-1]由g[i][j-2]与f[i-1][j-1]推来。

f[i-1][j-1]被算了两遍,所以要减去一次f[i-1][j-1]。

代码实现:

 
  1. #include<bits/stdc++.h>

  2. #include<string>

  3. using namespace std;

  4. const int mod=1e8;

  5. char a[5010],b[5010];

  6. int f[5010][5010],g[5010][5010];

  7. int len1,len2;

  8. int main(){

  9. scanf("%s%s",a+1,b+1);

  10. len1=strlen(a+1);

  11. len2=strlen(b+1);

  12. len1--,len2--;

  13. for(int i=0;i<=len1;++i)

  14. for(int j=0;j<=len2;++j)

  15. g[i][j]=1;

  16. for (int i = 1 ;i <=len1; i++)

  17. for( int j = 1; j <=len2; j++) {

  18. if (a[i]==b[j]){

  19. f[i][j]=f[i-1][j-1] + 1;

  20. g[i][j]=g[i-1][j-1];

  21. if(f[i][j]==f[i-1][j]) g[i][j]=(g[i][j]+g[i-1][j])%mod;

  22. if(f[i][j]==f[i][j-1]) g[i][j]=(g[i][j]+g[i][j-1])%mod;

  23. }

  24. else{

  25. f[i][j] = max(f[i - 1][j], f[i][j - 1]);

  26. g[i][j]=0;

  27. if(f[i][j]==f[i-1][j]) g[i][j]=(g[i][j]+g[i-1][j])%mod;

  28. if(f[i][j]==f[i][j-1]) g[i][j]=(g[i][j]+g[i][j-1])%mod;

  29. if(f[i][j]==f[i-1][j-1]) g[i][j]=(g[i][j]-g[i-1][j-1]+mod)%mod;

  30. }

  31. }

  32. cout<<f[len1][len2]<<"\n"<<(g[len1][len2]+mod)%mod<<"\n";

  33. return 0;

  34. }

3)最长公共上升子序列 LCIS

题意:

给出有 n 个元素的数组 a[] , m 个元素的数组 b[] ,求出它们的最长上升公共子序列的长度.

定义状态

F[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCIS的长度。

状态转移方程:

①F[i][j] = F[i-1][j] (a[i] != b[j])

②F[i][j] = max(F[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])

现在我们来说为什么会是这样的状态转移方程呢?

对于①,因为F[i][j]是以b[j]为结尾的LCIS,如果F[i][j]>0那么就说明a[1]..a[i]中必然有一个整数a[k]等于b[j],因为a[k]!=a[i],那么a[i]对F[i][j]没有贡献,于是我们不考虑它照样能得出F[i][j]的最优值。所以在a[i]!=b[j]的情况下必然有F[i][j]=F[i-1][j]。

对于②,前提是a[i] == b[j],我们需要去找一个最长的且能让b[j]接在其末尾的LCIS。之前最长的LCIS在哪呢?

首先我们要去找的F数组的第一维必然是i-1。因为i已经拿去和b[j]配对去了,不能用了。并且也不能是i-2,因为i-1必然比i-2更优。

第二维呢?那就需要枚举b[1]...b[j-1]了,因为你不知道这里面哪个最长且哪个小于b[j]。

这里还有一个问题,可不可能不配对呢?也就是在a[i]==b[j]的情况下,需不需要考虑F[i][j]=F[i-1][j]的决策呢?答案是不需要。因为如果b[j]不和a[i]配对,那就是和之前的a[1]...a[j-1]配对(假设F[i-1][j]>0,等于0不考虑),这样必然没有和a[i]配对优越。(为什么必然呢?因为b[j]和a[i]配对之后的转移是max(F[i-1][k])+1,而和之前的i`配对则是max(F[i`-1][k])+1。

代码实现:

 
  1. #include <iostream>

  2. #include <cstdlib>

  3. #include <cstdio>

  4. #include <cstring>

  5. #include <string>

  6. #include <algorithm>

  7. using namespace std;

  8. const int MAXN = 1001;

  9. int a[MAXN], b[MAXN];

  10. int f[MAXN][MAXN];

  11. int n, m,ans;

  12. void init()

  13. {

  14. memset(f, 0, sizeof(f));

  15. }

  16. void dp()

  17. {

  18. init();

  19. int i, j, k;

  20. for(i = 1; i <= n; i++)

  21. {

  22. for(j = 1; j <= m; j++)

  23. {

  24. f[i][j] = f[i-1][j]; // if(a[i] != b[j])

  25. if(a[i] == b[j])

  26. {

  27. int MAX = 0;

  28. for(k = 1; k <= j-1; k++) if(b[j] > b[k]) //枚举最大的f[i-1][k]

  29. {

  30. MAX = max(MAX, f[i-1][k]);

  31. }

  32. f[i][j] = MAX+1;

  33. }

  34. }

  35. }

  36. ans = 0;

  37. for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);

  38. }

  39. int main()

  40. {

  41. int T;

  42. cin>>T;

  43. while(T--)

  44. {

  45. scanf("%d",&n) ;

  46. for(int i=1;i<=n;i++)

  47. scanf("%d",&a[i]);

  48. scanf("%d",&m);

  49. for(int i=1;i<=m;i++)

  50. scanf("%d",&b[i]);

  51. dp();

  52. printf("%d\n", ans);

  53. if(T)

  54. printf("\n");

  55. }

  56. }

时间优化:

以上的代码的时间复杂度是O(n^3),那我们怎么去优化呢?通过思考发现,第三层循环找最大值是否可以优化呢?我们能否直接把枚举最大的f[i-1][k]值直接算出来呢?假设存在这么一个序列a[i] == b[j],我们继续看状态转移方程②,会发现b[j] > b[k],即当a[i] == b[j]时,可以推出a[i] > b[k],那么有了这个表达式我们可以做什么呢?可以发现,我们可以维护一个MAX值来储存最大的f[i-1][k]值。即只要有a[i] > a[j]的地方,那么我们就可以更新最大值,所以,当a[i] == b[j]的时候,f[i][j] = MAX+1,即可。

 
  1. void dp()

  2. {

  3. for(int i = 1; i <= n; i++)

  4. {

  5. int MAX = 0; //维护最大值

  6. for(int j = 1; j <= m; j++)

  7. {

  8. f[i][j] = f[i-1][j]; //a[i] != b[j]

  9. if(a[i] > b[j]) MAX = max(MAX, f[i-1][j]);

  10. if(a[i] == b[j]) f[i][j] = MAX+1;

  11. }

  12. }

  13. int ans = 0;

  14. for(int i = 1; i <= m; i++) ans = max(ans, f[n][i]);

  15. printf("%d\n", ans);

  16. }

可以发现,其实上面的代码有些地方与0/1背包很相似,即每次用到的只是上一层循环用到的值,即f[i-1][j],那么我们可以像优化0/1背包问题利用滚动数组来优化空间。

 
  1. void dp()

  2. {

  3. init();

  4. for(int i = 1; i <= n; i++)

  5. {

  6. int MAX = 0;

  7. for(int j = 1; j <= n; j++)

  8. {

  9. if(a[i] > b[j]) MAX = max(MAX, f[j]);

  10. if(a[i] == b[j]) f[j] = MAX+1;

  11. }

  12. }

  13. int ans = 0;

  14. for(int j = 1; j <= m; j++) ans = max(ans, f[j]);

  15. printf("%d\n", ans);

  16. }

如果是求最长公共下降子序列呢?很明显嘛,把状态定义改动一下,即f[i][j]表示以a串的前i个整数与b串的前j个整数且以b[j]为结尾构成的LCDS的长度,具体实现的时候只要把a[i] > b[j]改为a[i] < b[j]就可以啦。

还有一种打印路径的模板:

 
  1. #include<iostream>

  2. #include<math.h>

  3. #include<stdio.h>

  4. #include<algorithm>

  5. #include<string.h>

  6. #include<vector>

  7. #include<map>

  8. using namespace std;

  9. typedef long long lld;

  10. const int oo=0x3f3f3f3f;

  11. const lld OO=1LL<<61;

  12. const lld MOD=1000000007;

  13. #define eps 1e-6

  14. #define maxn 505

  15. int dp[maxn][maxn];

  16. int path[maxn][maxn];

  17. int a[maxn],b[maxn];

  18. int n,m;

  19. void dfs(int x)

  20. {

  21. if(path[n][x]==-1)

  22. {

  23. printf("%d",b[x]);

  24. return ;

  25. }

  26. dfs(path[n][x]);

  27. printf(" %d",b[x]);

  28. }

  29. int main()

  30. {

  31. while(scanf("%d",&n)!=EOF)

  32. {

  33. for(int i=1;i<=n;i++)

  34. scanf("%d",&a[i]);

  35. scanf("%d",&m);

  36. for(int i=1;i<=m;i++)

  37. scanf("%d",&b[i]);

  38. memset(dp,0,sizeof dp);

  39. memset(path,-1,sizeof path);

  40. for(int i=1;i<=n;i++)

  41. {

  42. int pos=-1,Max=0;

  43. for(int j=1;j<=m;j++)

  44. {

  45. dp[i][j]=dp[i-1][j];

  46. path[i][j]=path[i-1][j];

  47. if(a[i]==b[j]&&dp[i][j]<Max+1)

  48. {

  49. dp[i][j]=Max+1;

  50. path[i][j]=pos;

  51. }

  52. if(a[i]>b[j]&&dp[i-1][j]>Max)

  53. {

  54. Max=dp[i-1][j];

  55. pos=j;

  56. }

  57. }

  58. }

  59. int ans=1;

  60. for(int i=1;i<=m;i++)

  61. if(dp[n][ans]<dp[n][i])

  62. ans=i;

  63. printf("%d\n",dp[n][ans]);

  64. if(dp[n][ans])dfs(ans);

  65. puts("");

  66. }

  67. return 0;

  68. }

  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值