动态规划-最长子序列模型
区分概念:子串:要求连续,原序列的一段。子序列:不要求连续,但必须与原序列保持相同顺序
例题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]=0≤j<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]\} 1≤i≤Nmax{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)
- 问题描述:给定两个长度分别为 N 和 M 的字符串 A 和 B ,求既是 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[i−1][j]F[i][j−1]F[i−1][j−1]+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 A1−Ai 和 B 1 − B j B_1-B_j B1−Bj 中可以构成以 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[i−1][j]ifA[i]=B[j]0≤k<j,Bk<Bjmax{F[i−1][k]}+1ifA[i]=b[j]
- 可能的问题:不想等的时候为什么转移变少了,没有 F [ i ] [ j − 1 ] F[i][j-1] F[i][j−1] ,由于增加了条件,必须以 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]不变,所以可以打标记减少重复枚举,减小一维度的枚举
*/