最长公共上升子序列
题目描述
核心思路
状态表示
- 集合: d p [ i ] [ j ] dp[i][j] dp[i][j]表示第一个序列的前i个元素(即A[1…i])以及第二个序列的前j个元素(即B[1…j]),并且以 b [ j ] b[j] b[j]结尾的公共上升子序列的集合。
- 属性:Max。表示 d p [ i ] [ j ] dp[i][j] dp[i][j]这个集合子序列中长度最大值。
如何划分集合呢?
根据公共子序列中是否包含 a [ i ] a[i] a[i]来划分,实质就是根据 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]来划分。
我们可以先根据 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]成立与否,将这个集合划分为两部分。
- 如果 a [ i ] ! = b [ j ] a[i]!=b[j] a[i]!=b[j],说明当加入 a [ i ] a[i] a[i]和 b [ j ] b[j] b[j]后,并不能构成新的公共子序列。那么此时dp值仍然是继承前i-1个元素的,即 d p [ i ] [ j ] = d p [ i − 1 ] [ j ] dp[i][j]=dp[i-1][j] dp[i][j]=dp[i−1][j]。
- 如果
a
[
i
]
=
=
b
[
j
]
a[i]==b[j]
a[i]==b[j],说明当加入
a
[
i
]
a[i]
a[i]和
b
[
j
]
b[j]
b[j]后,可以构成新的公共子序列,那么我们如何求解这一半集合中的dp值呢?划分依据:公共子序列倒数第二个元素在b[]序列中所对应的数
- 子序列不存在倒数第二个数,只有唯一的一个数 b [ j ] b[j] b[j],由于我们是在 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]的前提下,当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时说明这俩可以配对,所以 d p [ i ] [ j ] dp[i][j] dp[i][j]的值至少是1.设为maxv=1。
- 子序列的倒数第二个数是 b [ 1 ] b[1] b[1],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ 1 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][1]+1) dp[i][j]=max(maxv,dp[i−1][1]+1)。为什么要+1呢,因为 d p [ i − 1 ] [ 1 ] dp[i-1][1] dp[i−1][1]表示第一个序列A的前i-1个元素以及第二个序列B的前1个元素,并且以 b [ 1 ] b[1] b[1]结尾的公共上升子序列,+1是由于要加上 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]这一对。
- 子序列的倒数第二个数是 b [ 2 ] b[2] b[2],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ 2 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][2]+1) dp[i][j]=max(maxv,dp[i−1][2]+1).
- $\cdots $
- 子序列的倒数第二个数是 b [ j − 1 ] b[j-1] b[j−1],则 d p [ i ] [ j ] = m a x ( m a x v , d p [ i − 1 ] [ j − 1 ] + 1 ) dp[i][j]=max(maxv,dp[i-1][j-1]+1) dp[i][j]=max(maxv,dp[i−1][j−1]+1).
现在当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时,我们已经对这一半集合中的公共子序列进行了细划分了,但是还要保持上升子序列的性质。为此,需要当 b [ k ] < b [ j ] b[k]<b[j] b[k]<b[j]时才能构成上升子序列,其中 k = 1 , 2 , ⋯ , j − 1 k=1,2,\cdots ,j-1 k=1,2,⋯,j−1。
如何理解优化代码呢?
然后我们发现每次循环求得的maxv是满足a[i] > b[k]的 f [ i − 1 ] [ j ] + 1 f[i-1][j]+1 f[i−1][j]+1的前缀最大值。因此可以直接将maxv提到第一层循环外面,减少重复计算,此时只剩下两重循环。我们发现只有当 a [ i ] = = b [ j ] a[i]==b[j] a[i]==b[j]时, d p [ i ] [ j ] dp[i][j] dp[i][j]才会被更新,那么 b [ k ] < b [ j ] b[k]<b[j] b[k]<b[j]可以替换为 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]。观察k循环这一层,其实就是处理了一个关于 d p [ i ] [ j ] dp[i][j] dp[i][j]前缀的问题,即是在求 b [ 1 ] b[1] b[1]到 b [ j ] b[j] b[j]中满足 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]条件下最大的dp值。真正被用到的也只有那个最大的dp值而已啦。所以我们只需要用一个变量来存储这个前缀并在符合条件 b [ k ] < a [ i ] b[k]<a[i] b[k]<a[i]时的最大dp值即可。
代码
未优化版
#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N][N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
//第一个循环控制第一个序列a[i]
for(int i=1;i<=n;i++)
{
//第二个循环控制第二个序列b[j]
for(int j=1;j<=n;j++)
{
//根据是否包含a[i]来将集合划分为两部分,实质划分依据就是:a[i]==b[j]与否
//如果a[i]!=b[j],说明不包含a[i],那么就继承a[i-1]的dp值
if(a[i]!=b[j])
{
dp[i][j]=dp[i-1][j];
}
//如果a[i]==b[j],说明包含a[i],那么需要根据公共子序列倒数第二个元素在b[]序列中所对应的数来将这一部分集合继续划分
//求出这一部分集合中的每一类情况的最大值maxv,最终就可以求解出这一部分集合的最大值maxv了
else
{
int maxv=1;
//判断b[1..j-1]是否<b[j],即我们现在是在公共子序列的基础上,要保证是上升子序列,因此需要b[k]<b[j]
for(int k=1;k<j;k++)
{
if(b[k]<b[j])
maxv=max(maxv,dp[i-1][k]+1);//求解出这一部分集合中的每一类情况的最大值
}
dp[i][j]=max(dp[i][j],maxv);//比较得出这两部分集合的最大值
}
}
}
int ans=0;//最长公共上升子序列的长度
//dp[n][i]表示第一个序列的前n个元素以及第二个元素的前j个元素,并且以b[j]结尾的公共上升子序列集合
//那么我们需要比较比较枚举是以哪一个b[j]结尾(b[1],b[2],...,b[j]),可以得到最大的公共上升子序列。
for(int j=1;j<=n;j++)
ans=max(ans,dp[n][j]);
printf("%d\n",ans);
return 0;
}
优化版
#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N][N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
int maxv=1;//记录前缀中最大的dp值
for(int j=1;j<=n;j++)
{
//将一个大集合划分为了两部分
//这里求出了当a[i]!=b[j]时,一部分集合的最大值
if(a[i]!=b[j])
{
dp[i][j]=dp[i-1][j];
}
//这里求出了当a[i]==b[j]时,另一部分的最大值
else
{
//dp[i][j]是求出的一部分集合的最大值,maxv是求出的另一部分集合的最大值
//最终取这两部分集合中的最大的那个最大值,作为整个集合最终的最大值
dp[i][j]=max(dp[i][j],maxv);
}
//当b[j]<a[i]时,说明这些b[j]被打上了绿勾,我们就求出这些绿勾中所对应的最大的那个dp值,并用maxv来记录
//比如原来j=6,有b1,b2,b3,b4,b5,假设b2,b3被打上了绿色勾,我们找到了这些绿勾中最大的dp值是b3所对应的dp[i-1][3]
//因此maxv此时记录的就是b1,b2,b3,b4,b5中被打上绿勾中的最大dp值,即为dp[i-1][3]
//然后j增大1,即此时j=7,有b1,b2,b3,b4,b5,b6,假设b6也被打上了绿勾,那么我们此时只需要比较前j=5中的最大dp值和b6所对应的dp值dp[i-1][6]+1即可
if(b[j]<a[i])
{
maxv=max(maxv,dp[i-1][j]+1);
}
}
}
int ans=0;
for(int j=1;j<=n;j++)
ans=max(ans,dp[n][j]);
printf("%d\n",ans);
return 0;
}
优化为一维
我们不难发现, d p [ i ] [ j ] dp[i][j] dp[i][j]的状态都是来源于上一层,所以可以去掉第一维做个等价变形。即可以只用一维dp。
#include<iostream>
#include<algorithm>
using namespace std;
const int N=3010;
int n;
int a[N],b[N];
int dp[N];
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
int maxv=1;
for(int j=1;j<=n;j++)
{
// if(a[i]!=b[j])
// dp[j]=dp[j];
if(a[i]==b[j])
dp[j]=max(dp[j],maxv);
if(b[j]<a[i])
maxv=max(maxv,dp[j]+1);
}
}
int ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]);
printf("%d\n",ans);
return 0;
}