0%

【动态规划】最长上升子序列模型

  • 主要内容
    • 动态规划中最长上升子序列模型题目总结

Easy Part

都是些常见基本模板题

AcWing 895. 最长上升子序列(序列最长1000)

数列长度1<N<1000,求给定序列的 数值严格递增的子序列长度;【模板题】

思路 【基于动态规划】

  • 状态表示:dp[i]:以 a[i]结尾的最长上升子序列的长度
  • 初始值:dp[i]=1,i∈[0,n−1] 表示最初一个数本身就是最长上升子序列,长度为 1
  • 状态转移:把前i−1个数字中所有满足条件dp[j]<dp[i](上升子序列) 的j找出来,那么dp[i]就可以试着更新为以 dp[j] 结尾的最长上升子序列的长度+自己的长度 1,但可能更新后的结果没之前的大,最后两者取max

代码 $O(n^2)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;

const int N=1005;
int n,a[N],dp[N];

int main(){
cin>>n;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int ans=0;
for(int i=1;i<=n;i++){
dp[i]=1;
for(int j=1;j<i;j++){
if(a[j]<a[i])
dp[i]=max(dp[i],dp[j]+1);
}
ans = max(ans,dp[i]);
}
cout<<ans<<endl;
}

AcWing896 最长上升子序列(序列最长100000)

数列长度1<N<100 000 ,求数值严格递增的子序列长度

思路【基于二分】

数据达到100000,考虑$nlogn$的算法

代码$O(nlogn)$

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N],q[N];//q[i]表示长度是i的上升子序列最后一个数的最小值
int main(){
ios::sync_with_stdio(false);

int n,cnt=0;//cnt:前i个数中最大的最长上升子序列的值
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
q[0]=INT_MIN;
for(int i=1;i<=n;i++){
int l=0,r=cnt;
while(l<r){
int mid=l+r+1>>1;
if(q[mid]<a[i]) l=mid;
else r=mid-1;
}
cnt=max(cnt,r+1);
q[r+1]=a[i];
}
cout<<cnt<<endl;
}

更好理解的一种

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int a[N];
int main(){
int n;cin>>n;
for(int i=0;i<n;i++) cin>>a[i];
vector<int>stk;
stk.push_back(a[0]);
for(int i=1;i<n;i++){
if(a[i]>stk.back()) stk.push_back(a[i]);
else{ //替换掉第一个大于等于它的数
int l=0,r=stk.size()-1;
while(l<r){
int mid=l+r>>1;
if(stk[mid]>=a[i]) r=mid;
else l=mid+1;
} // *lower_bound(stk.begin(), stk.end(), a[i]) = a[i];
stk[l]=a[i];
}
}
cout<<stk.size()<<endl;
}

AcWing897 最长公共子序列

两个长度分别为 N 和 M 的字符串 A 和 B,求既是 A 的子序列又是 B 的子序列的字符串长度最长是多少。

思路

1
2
3
4
5
6
状态表示(集合表示):dp[i][j] 所有在A[1…i]中出现过且在B[1…j]中也出现过的子序列
//以选到A[i]和B[j]结尾的满足条件子序列
属性: 最大值
状态计算(集合划分):根据A[i]是否等于B[j]
1) A[i] == B[j] dp[i][j] → dp[i-1][j-1]+1;
2) A[i] != B[j] dp[i][j] → max(dp[i][j],dp[i-1][j],dp[i][j-1])

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
char a[N],b[N];
int x,y;
int dp[N][N];
int main(){
ios::sync_with_stdio(false);
cin>>x>>y;
for(int i=1;i<=x;i++) cin>>a[i];
for(int i=1;i<=y;i++) cin>>b[i];

for(int i=1;i<=x;i++){
for(int j=1;j<=y;j++){
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
if(a[i]==b[j])
dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
}
}
cout<<dp[x][y]<<endl;
}

P1439 最长公共子序列

给出 1,2,…n 的两个排列 P1 和 P2 ,求它们的最长公共子序列。n<=100 000

数据范围大,按上一题的思路明显不行,所以考虑:

因为是1到 n的排列,所以两个子序列的元素都相同,只是位置不同

比如

1
2
P1: 3 1 2 4 5 映射成→ a b c d e  那么
P2: 5 3 1 2 4 映射成→ e a b c d 所以转换成求单个序列的最长上升子序列问题(AcWing896

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n;
int a[N],b[N];
int main(){
cin>>n;
for(int i=1;i<=n;i++){
int x;cin>>x;
a[x]=i;
}
for(int i=1;i<=n;i++){
int x;cin>>x;
b[i]=a[x];
}
//求b[]的最长上升子序列
vector<int>stk;
stk.push_back(b[1]);
for(int i=2;i<=n;i++){
if(stk.back()<b[i]) stk.push_back(b[i]);
else{
*lower_bound(stk.begin(),stk.end(),b[i])=b[i];
}
}
cout<<stk.size()<<endl;
}

AcWing 902. 最短编辑距离

给两字符串 A 和 B,将 A 经过若干操作变为 B,求出将 A 变为B 至少需要的操作次数。可进行操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A 的某个位置插入某个字符。
  3. 替换–将字符串 A 中的某个字符替换为另一个字符。

思路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
状态表示:dp[i][j]: 将A的前i个字符变成B的前j个字符
属性:最小操作次数
状态计算:
1) a[i]==b[j]: → dp[i-1][j-1];
2) 删除:dp[i-1][j]+1;
插入:dp[i][j-1]+1;
替换: dp[i-1][j-1]+1

关于初始化:
1.for遍历的时候需要用到的但是事先没有的 就要预处理 → 往往就是01之类的
2.如果要找min → INF; 要找有负数的max → -INF
因此:
f[0][i]:如果a初始长度是0,只能用插入操作让a变成b
f[i][0]:如果b初始长度是0,只能用删除操作让a变成b

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
char a[N],b[N];
int dp[N][N];//dp[i][j]将A的前i个字母变成B的前j个字母的操作次数
int main(){
int n,m;
cin>>n>>a+1;//使得a从下标1开始存储
cin>>m>>b+1;
for(int i=1;i<=n;i++) dp[i][0]=i;
for(int i=1;i<=m;i++) dp[0][i]=i;

for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(a[i]==b[j]) dp[i][j]=dp[i-1][j-1];
else{
//删除or删除
dp[i][j]=min(dp[i-1][j]+1,dp[i][j-1]+1);
//or替换
dp[i][j]=min(dp[i-1][j-1]+1,dp[i][j]);
}
}
}
cout<<dp[n][m]<<endl;
}

AcWing 899. 编辑距离

n个长度不超过 10 的字符串以及m 次询问,每次询问给出一个字符串和一个操作次数上限。

对每次询问,求给定的 n 个串中有多少个串可以在上限操作次数内经过操作变成询问给出的字符串。

思路

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int n,m;
char s[N][15];
int dp[15][15];

int dis(char a[],char b[]){
int lena=strlen(a+1);
int lenb=strlen(b+1);
for(int i=0;i<=lena;i++) dp[i][0]=i;
for(int i=0;i<=lenb;i++) dp[0][i]=i;
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];
else{
dp[i][j]=min(dp[i-1][j],dp[i][j-1])+1;
dp[i][j]=min(dp[i][j],dp[i-1][j-1]+1);
}
}
}
return dp[lena][lenb];
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++) cin>>(s[i]+1);
while(m--){
char b[15];cin>>(b+1);
int len=0; cin>>len;
int cnt=0;
for(int i=0;i<n;i++){
memset(dp,0,sizeof dp); //注意点
if(dis(s[i],b)<=len) cnt++;
}
cout<<cnt<<endl;
}
return 0;
}

Medium Part

非模板题目,在基础模板上的一丢丢变化且不难的题

AcWing 1014. 登山

给定序列,求先单调上升再单调下降的最长子序列长度

思路

先分别按数组 顺序和逆序 求以a[i]结尾的最长上升子序列长度f[i]

得到以a[i]结尾的先上升后下降的最长子序列长度即为f[i]+g[i]-1

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
int f[N],g[N],a[N];
int n;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
f[i]=1,g[i]=1;
}
for(int i=1;i<=n;i++){
for(int j=1;j<i;j++){
if(a[i]>a[j]) f[i]=max(f[i],f[j]+1);
}
}
for(int i=n;i>=1;i--){
for(int j=n;j>i;j--){
if(a[i]>a[j]) g[i]=max(g[i],g[j]+1);
}
}
int ans=0;
for(int i=1;i<=n;i++) ans=max(ans,f[i]+g[i]-1);
cout<<ans<<endl;
}