最长子序列(线性DP)学习笔记

子序列和子串不一样。子串要求必须连续,而子序列不需要连续。

比如说{a1,a2an},他的子串就是{ai,ai+1,,aj|1ijn},而子序列就是{at1,at2ati|t1<t2<<tn}只要子序列里面元素的顺序仍然保持原序列里面的顺序就可以了,不要求连续


最长上升子序列

最长上升/不下降/下降/不上升子序列

这四个序列本质都是一样的……理解了本质就可以写出来了

这里用最长上升序列LIS(Longest Increasing Subsequence)来讨论

LIS,顾名思义,就是子序列里面要求严格上升,就是严格升序排列,连相同的都不能有

Θ(n2) DP

很容易想到线性DP,设fi表示ai结尾的最长的LIS的长度,那么就可以得到

fi=max(fj)+1(j<i,aj<ai)

如果没有找到aj<ai呢?那么fi就只有ai自己,就是1

所以我们可以初始化fi全部为1,然后对于ni,我们都遍历所有的j<i如果满足aj<ai,那么fi=max(fi,fj+1)

分析时间复杂度,对于ni,我们都遍历i个点判断是否满足,复杂度是二次方级别的,大概就是Θ(n2)

如果要准确算的话……Θ(i=1ni)=Θ(n(n+1)2),大概就是Θ(n22)的样子

如果我们看看题目:

P1020 [NOIP1999 普及组] 导弹拦截

题目描述

某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

输入导弹依次飞来的高度(雷达给出的高度数据是50000的正整数),计算这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入格式

1行,若干个整数(个数100000

输出格式

2行,每行一个整数,第一个数字表示这套系统最多能拦截多少导弹,第二个数字表示如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

输入输出样例

**输入 #1 **

389 207 155 300 299 170 158 65

输出 #1

6
2

根据即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难可得,第一问这套系统最多拦截的导弹数就是求最长不上升子序列,而第二问,就是求最长不上升子序列的最少个数。

那么第二问,对于每一个最长不上升子序列的末尾,肯定都比下一个数要小,也就是说我们只要求出原序列的最长严格上升序列,就是这个问的答案,因为即得易见平凡,仿照上例显然,留作习题答案略,读者自证不难

所以我们就很开心交了一个代码上去

#include<iostream>
using namespace std;
int h[100001],n=1,b[100001],b1[100001],a1=1,a2=1;
int main()
{
	while(cin>>h[n])
	{
		b[n]=1;b1[n++]=1;
	}
	n--;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
        // 只要找到前面有不小于我的,就可以把我接在这个的后面形成一个不上升子序列
		if(h[j]>=h[i])b[i]=max(b[i],b[j]+1);
	}
	a1=-1;
	for(int i=1;i<=n;i++)
	{
        // 寻找最大值
		if(b[i]>a1)a1=b[i];
	}
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<i;j++)
        // 只要找到前面有小于我的,就可以把我接在这个的后面形成一个严格上升子序列
		if(h[j]<h[i])b1[i]=max(b1[i],b1[j]+1);
	}
	a2=-1;
	for(int i=1;i<=n;i++)
	{
        // 寻找最大值
		if(b1[i]>a2)a2=b1[i];
	}
	cout<<a1<<endl<<a2;
	return 0;
}

然后你就会发现,这道题Θ(n2) 是可以拿100分的!!!

是啊满分200呢。

考虑优化

Θ(nlog2n) 单调二分优化DP

首先,对于每个点都要跑一次遍历,这里的Θ(n)是不能省的了。我们考虑每次遍历时候的Θ(n)

我们每次遍历都看了看我能不能接上去,但是就会在接不上去和接上去也亏这里浪费掉很多时间。

那我们就可以设dpi表示长度为i的LIS的最优结尾

为什么说最优结尾呢?如果长度一样,那么肯定是用3结尾比用5结尾要好。因为如果后面有4,那么3能接上去而5不能。

然后我们又阔以知道,dp数组一定是严格单调递增的。

我们设当前的两个LIS,a长度为l结尾是x(即dpl=x),b长度l+1结尾也是x(即dpl+1=x)。

所以我们就可以知道bl+1=x,bl<bl+1,bl<x

那么我们长度为l的LIS的最优结尾dpl不就应该等于bl1吗?那就小于x啦。

所以假设是不成立哒~

知道了dp是单调递增的时候,我们就可以,设当前最长LIS的长度为len,那么dp数组现在的大小应该也是len,再设现在遍历到了ai

  • 如果ai>dplen,那么就说明ai可以接在最长LIS的后面,成为下一个最优结尾,即dp[++len]=a[i]

  • 如果aidplen,我们也不能就这样舍弃掉ai,毕竟它可能可以把前面没那么优的结尾 取而代之也。

我们设ai将要取代dpj(j<len) 如果ai>dpj,那么其实这样取代是不划算的。本来人家就比你更有潜力能接更多数,如果换了你,可能你就接不了4了(上文\滑稽

那么如果有一堆都比ai大肿么办? 肯定代替掉不小于ai的最小的那个

是不是很有二份答案的感觉?

是哒我们就用二分来寻找这个最小,然后总的复杂度就是O(nlog2n)

我知道你们会有什么问题

这里dplen表示的只是长度为len的上升子序列的最优结尾,并不代表任何关于原序列的位置信息

也就是如果我现在递推到ai,那么dp表示的就是a1ai1的序列的最优结尾的信息。

至于具体的序列是什么……没办法知道。

现在推荐俩个STL

template <class ForwardIterator, class T, class Compare> 
ForwardIterator lower_bound (ForwardIterator first, ForwardIterator last, const T& val, Compare comp);

(完蛋找不到upper_bound原型了)

好了我们推荐一个STL

lower_bound(begin,end,val),是返回地址begin地址end之间的有序序列第一个大于等于 数值 val地址

比如说我有数组a,已知这个数组单调不下降,a5=a6=7,那么我们调用lower_bound(a,a+n)就会返回第一个大于等于7的a5地址

然后C++党福利就是指针是阔以相减的。

用返回值减去数组开头就是这个元素在数组中下标。

比如上面,lower_bound(a,a+n)-a=5

至于那个找不到的upper_bound(begin,end,val)是返回地址begin地址end之间的有序序列第一个严格大于 数值 val地址

其他就和lower_bound一样了

然后这个有序序列

在STL中其实就是默认升序

所以不加上cmp的sort出来是升序

但是如果我们真的需要降序

或者这个数组本来就是降序

那么我们其实可以定义比较函数的。

lower_bound比较器里面只要val比当前的中值(就是二分的中值)“小”就向左跳,否则就向右跳。

所以我们只要在处理降序序列的时候告诉lower_bound,只要(int)a>(int)b,就认为a比b“小”

我:我现在告诉你,lower_bound,5比3小

lower_bound:你现在要找的val是5,中值是3,那么寻找第一个“大于等于”5的数,就应该到左边去找。

这里“大于等于”,在傻傻的lower_bound眼中,就是不满足我们给的"小"

lower_bound:我帮你在这个有序序列中找到第一个不"小于”5的数

翻译成人话:lower_bound在这个降序序列中找到第一个小于等于5的数

所以我们就可以用lower_bound在单调上升序列dp中寻找到第一个大于等于ai的数,取而代之。因为,LIS是不允许重复的。

但是……emmm如果是不下降子序列呢?

那就找到第一个大于ai的呗~

那个我怎么也找不到的upper_bound

记得初始值dp1=a1

#include <cstdio>
#include <iostream>
#include <algorithm>
int h[100001],cnt;
int dp[100001],len;

int main()
{
	while(~scanf("%d",&h[++cnt]));
	--cnt;
	dp[++len]=h[1];
	for(register int i=2;i<=cnt;++i)
	{
        /// 最长不下降子序列
		if(h[i]<=dp[len]) dp[++len]=h[i];
		else
		{
            // 这里dp数组是单调不上升的,所以要用upper_bound
            // 又因为它不上升,那就肯定不是升序啦?
            // 就要重新定义“小”
            // 结构体std::greater<typename>的重载运算符(typename a.typename b)可以返回a>b
            // 自己写cmp函数也没可以的
			int j=std::upper_bound(dp+1,dp+1+len,h[i],std::greater<int>())-dp;
			dp[j]=h[i];
		}
	}
	printf("%d\n",len);
	dp[len=1]=h[1];
	for(register int i=2;i<=cnt;++i)
	{
        /// 最长严格上升子序列
		if(h[i]>dp[len]) dp[++len]=h[i];
		else
		{
			int j=std::lower_bound(dp+1,dp+1+len,h[i])-dp;
			dp[j]=h[i];
		}
	}
	printf("%d\n",len);
	return 0;
}

Θ(nlog2n) 树状数组优化

我们考虑LISz中fi的转移方程

fi=max(fj)+1(aj<ai)

要求一段区间的最大值……我们可以用线段树。

当然退化一下树状数组更省空间。

这次我们设(注意两个优化的dp数组表示不同含义)dpi表示以ai结尾的LIS的最长长度

再设txx结尾的LIS的最长长度

那么我们求LIS的时候就可以遍历t1tai1之间的最大值,然后再用ai接上去,最后再更新tai就可以了。

fi=max1j<i(tj)+1tj=max(fi,tj)

然后我们就可以用树状数组来维护t1tj的最大值

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
int cnt,h[100001],hsh[100001];
int lowbit(int x)
{
	return x&(-x);
}
void add(int T[],int i,int x)
{
	while(i<=cnt)
	{
		T[i]=std::max(T[i],x);
		i+=lowbit(i);
	}
}
int query(int T[],int n)
{
	int res=0;
	while(n)
	{
		res=std::max(T[n],res);
		n-=lowbit(n);
	}
	return res;
}
int T[100001],dp[100001];

int ans=0;

int main()
{
	while(~scanf("%d",&h[++cnt]));
	--cnt;
	std::memcpy(hsh,h,sizeof(h));
	std::sort(hsh+1,hsh+1+cnt);
	int m=std::unique(hsh+1,hsh+1+cnt)-hsh-1;
    // 必须离散化,否则t的下标会爆,可以不去重,原序列可以无序
	for(register int i=1;i<=cnt;++i) h[i]=std::lower_bound(hsh+1,hsh+1+m,h[i])-hsh;
	for(register int i=1;i<=cnt;++i)
	{
        /// 最长不下降子序列,那就不是升序啦?
        /// 但是树状数组的下标维护必须升序,那就考虑用相反数
        /// 又但是树状数组的下标必须大于0,所以加上m+1
		int j=query(T,m-h[i]+1);
		ans=std::max(ans,dp[i]=j+1);
		add(T,m-h[i]+1,dp[i]);
	}
	printf("%d\n",ans);
	ans=0;
	memset(T,0,sizeof(T));
	for(register int i=1;i<=cnt;++i)
	{
        /// 最长严格上升子序列,所以相同的不能算,树状数组下标要-1,而不是h[i]
		int j=query(T,h[i]-1);
		ans=std::max(ans,dp[i]=j+1);
		add(T,h[i],dp[i]);
	}
	printf("%d\n",ans);
	return 0;
}

然后你就发现dp数组没什么用……用j+1不就全解决了吗

最长公共子序列

LCS(我也不知道英文是啥)

给定两个数组a,b求他们的最长公共子序列

(就是文字意思)

dpi,j表示a1i,b1j的最长公共子序列,那么就会有

dpi,j={dpi1,j1+1ai=bjmax(dpi1,j,dpi,j1)aibj

Θ(n2),目前没有想到优化

所以对于题目

题目描述

给出 1,2,,n 的两个排列 P1P2 ,求它们的最长公共子序列。

输入格式

第一行是一个数 n

接下来两行,每行为 n 个数,为自然数 1,2,,n 的一个排列。

输出格式

一个数,即最长公共子序列的长度。

输入输出样例

输入 #1

5 
3 2 1 4 5
1 2 3 4 5

输出 #1

3

说明/提示

  • 对于 50% 的数据, n103
  • 对于 100% 的数据, n105

我们可以抱着试一试的心态用Θ(n2)的算法

#include <cstdio>
#include <iostream>

int n;
int dp[100001][100001],a[100001],b[100001];

int main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;++i) scanf("%d",a+i);
	for(register int i=1;i<=n;++i) scanf("%d",b+i);
	for(register int i=1;i<=n;++i)
	{
		for(register int j=1;j<=n;++j)
		{
			if(a[i]==b[j]) dp[i][j]=std::max(dp[i][j],dp[i-1][j-1]+1);
			else dp[i][j]=std::max(dp[i-1][j],dp[i][j-1]);
		}
	}
	printf("%d\n",dp[n][n]);
	return 0;
}

然后你就会很开心发现

g++ P1439.cpp -o P1439.exe
P1439.cpp:5:22: error: size of array 'dp' is too large
 int dp[100001][100001],a[100001],b[100001];
                      ^
P1439.cpp: In function 'int main()':
P1439.cpp:16:19: error: 'dp' was not declared in this scope
    if(a[i]==b[j]) dp[i][j]=std::max(dp[i][j],dp[i-1][j-1]+1);
                   ^
P1439.cpp:17:9: error: 'dp' was not declared in this scope
    else dp[i][j]=std::max(dp[i-1][j],dp[i][j-1]);
         ^
P1439.cpp:20:16: error: 'dp' was not declared in this scope
  printf("%d\n",dp[n][n]);

什么意思?

就是dp数组太大了

时间没炸空间先爆

好吧我们注意一下

给出 1,2,,n 的两个排列 P1P2

既然是排列

那么就因该有这两个集合相同,而且元素互异。

我们可以随便找一下样例中的公共子序列

比如3 4 5

然后从上面的3向下面的3连一条线

4也是

5还是

就发现这三条线完全没有交点

找其他公共子序列也是这样

所以我们就发现,当P2升序排列时,公共序列的元素在P1中的相对位置和在P2中的相对位置一样

然后我们把限制条件拿掉

公共序列的元素在P1中的相对位置和在P2中的相对位置一样

因为这个序列是公共的。

而且这个序列是可以确定的。

那么这个子序列的元素之间相对位置就和在原序列中的相对位置一样

即公共序列的元素在中的相对位置和在中的相对位置一样

然后我们又知道这些元素是互异的。

所以对于P1中的每一个元素,我们都可以知道他们在P2中的位置,设为pos

那么只要P1的子序列满足子序列的元素在P2中的位置也是有序的,那么这个子序列就是公共子序列

也就是我们要求pos中的LIS

我管你用什么方法反正空间上卡过去了

时间上Θ(nlog2n)就可以过

#include <cstdio>
#include <algorithm>

int n;
int a[100001],b[100001],hsh[100001],dp[100001],len;

int main()
{
	scanf("%d",&n);
	for(register int i=1;i<=n;++i) scanf("%d",a+i);
	for(register int i=1;i<=n;++i) scanf("%d",b+i);
    // 求出元素在b[i]中的位置
	for(register int i=1;i<=n;++i) hsh[b[i]]=i;
    // 映射回a数组
	for(register int i=1;i<=n;++i) a[i]=hsh[a[i]];
	dp[++len]=a[1];
	for(register int i=2;i<=n;++i)
	{
		if(a[i]>dp[len]) dp[++len]=a[i];
		else
		{
			int j=std::upper_bound(dp+1,dp+1+len,a[i])-dp;
			dp[j]=a[i];
		}
	}
	printf("%d\n",len);
	return 0;
}
posted @   IdanSuce  阅读(216)  评论(0编辑  收藏  举报
编辑推荐:
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
阅读排行:
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
点击右上角即可分享
微信分享提示