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…… abc
然后第一个数列就会变成这样:
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 blenai,即 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} b1blen 中找到最大的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+1ai, 那么就可以更新长度为 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;
} 

总结:

任何问题都离不开转换,任何转换都离不开优化


刷题专区:

黄道十二宫(外网进不去)

猜你喜欢

转载自blog.csdn.net/SAI_2021/article/details/119831982