P1439 【模板】最长公共子序列 (最长不下降序列 (单调队列优化))
题目大意:
解题思路:
只要有一点DP基础,就知道这题肯定是用最长公共子序列的DP来做。
但是我们再看看这数据范围 对于 100% 的数据
n
<
=
1
0
5
n<=10^5
n<=105。由于基础的公共序列DP是用二维数组做的,因此不难发现,如果没有神学优化,一定会愉快超时(时间是 O(
n
2
n^2
n2))。
那么怎么办?
任何所谓困难的题目,都离不开对题意的转换
我们可以通过题目发现,两个数列都是属于同一个全排列里的——说明什么?说明在这两个数列中,数的个数是相同的,而且不会有任何一个数字重复,并且数列A中出现的数 数列B中都会出现——这意味着什么?这意味着我们可以通过一个性质,将这个问题玄学转换!
- 这个性质一般是要见过才能想出来的,所以想不出来这个性质的人不要灰心哈。
重点来了!神学优化来了!
首先我们先设定两个数列:
3 2 1 4 5
1 2 3 4 5
我们可以以第一个数列中的数为基准,将数字按顺序标位
a
、
b
、
c
…
…
a、b、c……
a、b、c……
然后第一个数列就会变成这样:
a b c d e
3 2 1 4 5 (我是对照表)
然后我们再以第一个数列为基准,如果在第一个数列中3表示a 那么第二个数列中的3也替换为a……以此类推,最后得到的第二个数列是这样的:
c b a d e
1 2 3 4 5 (我是对照表)
通过这样的转换,公共子序列的最长长度当然不会变。
但是,不难发现,因为最开始的转换是由第一种数列来做基础的,所以转换后的数列1一定是一个单调递增的数列。
我发现,如果某个子序列在数列二转换后也是递增的,那么这个子序列肯定是数列一的子序列
就像这样:
c b a d e
中a d e是递增的,他们原本代表的数字是:3 4 5,同样,3 4 5正好也是数列一的一个子序列。
发现没有,原本的公共子序列问题经过转换降维,变成了一道最长不下降子序列的模板题!
于是转换代码就是这样的:
void input()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>num[i];
f[num[i]]=i; //用一个F数组表示对数列一的转换
}
for(int i=1;i<=n;i++)
{
cin>>num[i]; //因为转换后就变成了最长不下降子序列的问题,所以数列一可以不要了,直接覆盖掉
num[i]=f[num[i]]; //根据数列一的转换,更新数列二的数
}
}
但是,最长不下降子序列DP的标准复杂度也是O(
n
2
n^2
n2),和公共子序列相差无几。
怎么办呢?这时候我们就要用单调队列的特性对不下降子序列的DP进行优化,有了它,算法复杂度将达到
O
(
n
l
o
g
n
)
O\ (n\ log\ \ n)
O (n log n)
O ( n l o g n ) O(n\ log\ n) O(n log n)的算法关键是它建立了一个数组 b b b, b i b_i bi表示长度为 i i i 的不下降序列中结尾元素的最小值,用 l e n len len 表示数组目前的长度,算法完成后k的值即为最长不下降子序列的长度。
具体点来讲:
不妨假设,当前已求出的长度为
l
e
n
len
len,则判断a[i]和b[k]:
如果
b
l
e
n
≤
a
i
b_{len}≤a_i
blen≤ai,即
a
i
a_i
ai 大于长度为
l
e
n
len
len的序列中的最后一个元素,这样就可以使序列的长度增加1,即
l
e
n
=
l
e
n
+
1
len=len+1
len=len+1,然后更新
b
l
e
n
=
a
i
b_{len}=a_i
blen=ai;
如果 b l e n > a i b_{len}>a_i blen>ai,那么就在 b 1 … b l e n b_1…b_{len} b1…blen 中找到最大的j,使得 b j < a i b_j<a_i bj<ai,即 a i a_i ai 大于长度为j的序列的最后一个元素,显然, b j + 1 ≥ a i b_{j+1}≥a_i bj+1≥ai, 那么就可以更新长度为 j + 1 j+1 j+1的序列的最后一个元素,即 b j + 1 = a i b_{j+1}=a_i bj+1=ai。
可以注意到: b i b_i bi单调递增,很容易理解,长度更长了, b l e n b_{len} blen的值是不会减小的,更新数组可以用二分查找,所以算法近于线性,复杂度为 O ( n l o g n ) O\ (n\ log\ n) O (n log n)
我们甚至可以将 b b b 数组省略掉,直接用 d p dp dp 数组来存
所以单调队列优化后的不下降DP是这样的:
void DP()
{
len++;
dp[len]=num[len]; //数组中先存下第一个数为基准
for(int i=1;i<=n;i++)
{
if(dp[len]<num[i])
{
len++;
dp[len]=num[i]; //更新dp数组的长度,并改变基准
}
else
{
int t=search(1,len,num[i]); //由于dp数组是单调队列,所以可以用二分查找搜索位置
dp[t]=num[i];
}
}
cout<<len;
}
}
关于最后的CODE:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
int n,len=0,num[110000];
int dp[110000]={0};
int f[110000]={0};
void input()
{
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>num[i];
f[num[i]]=i; //用一个F数组表示对数列的转换
}
for(int i=1;i<=n;i++)
{
cin>>num[i]; //因为转换后就变成了最长不下降子序列的问题,所以数列一可以不要了,直接覆盖掉
num[i]=f[num[i]]; //根据数列一的转换,更新数列二的数
}
}
int search(int l,int r,int x)
{
int mid,tryy,ans;
while(l<=r)
{
mid=l+(r-l)/2;
tryy=dp[mid];
if(tryy>=x)
{
ans=mid;
r=mid-1;
}
else l=mid+1;
}
return ans;
}
void DP()
{
len++;
dp[len]=num[len];
for(int i=1;i<=n;i++)
{
if(dp[len]<num[i])
{
len++;
dp[len]=num[i];
}
else
{
int t=search(1,len,num[i]);
dp[t]=num[i];
}
}
cout<<len;
}
int main()
{
input();
DP();
return 0;
}
总结:
任何问题都离不开转换,任何转换都离不开优化
- 关于上升子序列(LIS)的一些定理:
dilworth定理:不下降子序列的最小划分数等于最长上升子序列的长度
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!