动态规划-最长子序列模型

动态规划-最长子序列模型


区分概念:子串:要求连续,原序列的一段。子序列:不要求连续,但必须与原序列保持相同顺序

例题1:最长上升子序列问题(LIS)

  • 问题描述: 给定一个长度为 N 的数列 A ,求数值单调递增的子序列长度最长是多少
  • 状态表示 F [ i ] F[i] F[i] 表示以 A [ i ] A[i] A[i] 为结尾的“最长上升子序列”的长度
  • 阶段划分:子序列的结尾位置(数列 A 中 的位置,从前到后)
  • 转移方程 F [ i ] = max ⁡ 0 ≤ j < i , A [ j ] < A [ i ] { F [ j ] + 1 } F[i]=\max\limits_{0\le j< i,A[j]<A[i]} \{F[j]+1\} F[i]=0j<i,A[j]<A[i]max{F[j]+1}
  • 边界条件 F [ 0 ] = 0 F[0]=0 F[0]=0
  • 目标状态 max ⁡ 1 ≤ i ≤ N { F [ i ] } \max\limits_{1\le i \le N}\{F[i]\} 1iNmax{F[i]}
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
const int N=1000;
int mapp[N+2],f[N+2];

int main()
{
  int n,ans=0;
  std::cin>>n;
  for (int i=1;i<=n;i++) {
    std::cin>>mapp[i];
  }
  
  for (int i=1;i<=n;i++) {
    f[i]=1;
    for (int j=1;j<i;j++) {
      if (mapp[i]>mapp[j]) f[i]=std::max(f[i],f[j]+1);
    }
    ans=std::max(ans,f[i]);
  }
  
  std::cout<<ans<<std::endl;
  return 0;
}
例题1.5: 最长上升子序列的长度

如果不求解最长上升子序列的序列内容,仅仅维护最长上升子序列的长度,可以把时间从 O ( n 2 ) O_{(n^2)} O(n2) 压缩到 O ( n l o g n ) O_{(nlogn)} O(nlogn)

  • 技巧:利用lower_bound函数二分查找序列中第一个不小于目标元素的元素

  • 状态表示:与上面相同

  • 步骤放个链接

  • 一点点理解:在f数列上 k 的位置上进行更改,我能保证在这个原数列到此为止,若要找出一个长度为 k 的上升序列,那么这是最优解,换句话说,只要最后一次更改在最后一个数,或者又在 F 后面加了一个数,那么 F 存储的便是真实的最长上升子序列的内容

// 板子代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cmath>
#include <vector>
const int N=200000;
int mapp[N+2];
std::vector<int> f;

int main()
{
    int n;
    std::cin>>n;
    for (int i=1;i<=n;i++) {
        std::cin>>mapp[i];
    }

    for (int i=1;i<=n;i++) {
        if (f.size()==0 || f[(int)f.size()-1]<mapp[i]) f.push_back(mapp[i]);
        else {
            int l=std::lower_bound(f.begin(),f.end(),mapp[i])-f.begin();
            f[l]=mapp[i];
        }
    }
    
    std::cout<<f.size()<<std::endl;
    return 0;
}
练习题:上升点列 CSP-J-2022
练习题:友好城市
练习题:拦截导弹

例题2:最长公共子序列问题(LCS)

  • 问题描述:给定两个长度分别为 NM 的字符串 AB ,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少
  • 状态表示 F [ i ] [ j ] F[i][j] F[i][j] 表示 A [ i ] A[i] A[i] 前的序列与 B [ j ] B[j] B[j] 前的序列的“最长公共子序列的长度”
  • 阶段划分:已经处理的前缀长度(两个字符串中的位置,即一个二维坐标)
  • 转移方程 F [ i ] [ j ] = max ⁡ { F [ i − 1 ] [ j ] F [ i ] [ j − 1 ] F [ i − 1 ] [ j − 1 ] + 1 i f   A [ i ] = B [ j ] F[i][j]=\max \begin{cases} F[i-1][j] \\ F[i][j-1] \\ F[i-1][j-1]+1 \quad\quad if\,A[i]=B[j] \end{cases} F[i][j]=max F[i1][j]F[i][j1]F[i1][j1]+1ifA[i]=B[j]
  • 边界条件 F [ i ] [ 0 ] = f [ 0 ] [ j ] = 0 F[i][0]=f[0][j]=0 F[i][0]=f[0][j]=0
  • 目标状态 F [ N ] [ M ] F[N][M] F[N][M]
// 板子代码
#include <iostream>
#include <cstdio>
#include <cmath>
#include <cstring>
#include <algorithm>

const int N=1000;
int A[N+2],B[N+2],f[N+2][N+2];

int main()
{
  int n;
  std::cin>>n;
  for (int i=1;i<=n;i++) {
    std::cin>>A[i]>>B[i];
  }
  
  for (int i=1;i<=n;i++) {
    for (int j=1;j<=n;j++) {
      f[i][j]=std::max(f[i-1][j],f[i][j-1]);
      if (A[i]==B[j]) f[i][j]=std::max(f[i][j],f[i-1][j-1]+1);
    }
  }
  
  std::cout<<f[n][n]<<std::endl;
  return 0;
}

例题3:最长公共上升子序列问题(LICS)

  • 思路:结合上述两算法的综合运用
  • 问题描述:就是俩数列 A B ,求俩数列的最长的公共的上升的子序列
  • 状态设计 F [ i ] [ j ] F[i][j] F[i][j] 表示 A 1 − A i A_1-A_i A1Ai B 1 − B j B_1-B_j B1Bj 中可以构成以 B j B_j Bj 为结尾的最长公共上升子序列的长度
  • 状态转移 F [ i ] [ j ] = { F [ i − 1 ] [ j ] i f    A [ i ] ≠ B [ j ] max ⁡ 0 ≤ k < j , B k < B j { F [ i − 1 ] [ k ] } + 1 i f    A [ i ] = b [ j ] F[i][j]=\begin{cases} F[i-1][j] \quad if\;A[i]\ne B[j] \\ \max\limits_{0\le k <j,B_k<B_j} \{ F[i-1][k] \}+1 \quad if\; A[i]=b[j] \end{cases} F[i][j]= F[i1][j]ifA[i]=B[j]0k<j,Bk<Bjmax{F[i1][k]}+1ifA[i]=b[j]
  • 可能的问题:不想等的时候为什么转移变少了,没有 F [ i ] [ j − 1 ] F[i][j-1] F[i][j1] ,由于增加了条件,必须以 B j B_j Bj 为结尾,所以另一种情况不成立了。
  • 仔细观察产生的优化:仔细观察下面两个代码片段
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>

const int N=3000;
int a[N+2],b[N+2],f[N+2][N+2];  // 集合: 所有由第一个序列的前i个字母,和第二个序列的前j个字母构成的,且以b[j]为结尾的公共上升子序列的合集
                                // 性质: 最大值

int main()  
{
    int n;
    std::cin>>n;
    for (int i=1;i<=n;i++) std::cin>>a[i];
    for (int i=1;i<=n;i++) std::cin>>b[i];

//case1:未优化版本,时间复杂度O(n^3)
    int ans=0;    
    for (int i=1;i<=n;i++) {
        for (int j=1;j<=n;j++) {
            if (a[i]==b[j]) {
                f[i][j]=std::max(f[i][j],1);
                for (int k=1;k<j;k++) {
                    if (b[k]<a[i]) f[i][j]=std::max(f[i][j],f[i-1][k]+1);
                }
            }
            else f[i][j]=f[i-1][j];
            if (i==n) ans=std::max(ans,f[n][j]);
        }
    }
    
//case2:优化版本,时间复杂度O(n^2)
    int ans=0;    
    for (int i=1;i<=n;i++) {
        int tag=0;
        for (int j=1;j<=n;j++) {
            if (a[i]==b[j]) {
                f[i][j]=std::max(f[i][j],1);
                f[i][j]=std::max(f[i][j],f[i][tag]); //(没有这句话也对,我也不知道为啥)
                for (int k=tag+1;k<j;k++) {
                    if (b[k]<a[i]) f[i][j]=std::max(f[i][j],f[i-1][k]+1);
                }
                tag=j;
            }
            else f[i][j]=f[i-1][j];
            if (i==n) ans=std::max(ans,f[n][j]);
        }
    }

//输出
    std::cout<<ans<<std::endl;
    return 0;
}

/*
优化内容:
当i不变的时候,对于一个新的j,使得a[i]==b[j]成立的时候,k的范围其实不需要从1开始枚举,因为a[i]不变,所以可以打标记减少重复枚举,减小一维度的枚举
*/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值