Loading

LIS及其扩展

1计算长度

1.1朴素DP

我们设状态\(f_i\)为以i结束的最长上升子序列的最长长度。则有则我们从i前面找到一个元素,满足\(a_j<a_i\),枚举所有满足条件的j,则i的f值就是这所有的j对应的f值加1。代码:

	for(int i=1;i<=n;++i)
        {
		for(int j=1;j<i;++j)
			if(a[j]<a[i])
                f[i]=max(f[i],f[j]+1);
	}

1 .2优化dp

我们发现我们浪费了一些时间在找满足\(a_j<a_i\)的f的最大值上,能否优化这个过程呢?当然可以,我们只需要一颗线段树。这颗线段树满足单点修改,区间查询最大值,所以不用打懒标记。
这里需要注意的是,线段树里的每个位置是\(a_i\)这个值,而不是下标i,每次找到以i结尾的最大值时单点查询\(a_i\),把这个位置的值改为\(f_i\),到了\(a_j\)查找最大值时查找的区间就是0至\(a_{j-1}\)这样就保证了我们解得合法性。

代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 7001000
#define M 100100
using namespace std;

inline int Max(int a,int b){
	return a>b?a:b;
}

struct Xtree{// point_change square_ask->no_lazy
	int p[N];
	
	inline Xtree(){
		memset(p,0,sizeof(p));
	}
	
	inline void pushup(int k){
		p[k]=Max(p[k*2],p[k*2+1]);
	}
	
	inline void change(int k,int l,int r,int w,int x){
		if(l==r&&l==w){
			p[k]=x;
			return;
		}
		int mid=l+r>>1;
		if(w<=mid) change(k*2,l,mid,w,x);
		else change(k*2+1,mid+1,r,w,x);
		pushup(k);
	}
	
	inline int ask_max(int k,int l,int r,int z,int y){
		if(l==z&&r==y) return p[k];
//		printf("%d %d %d %d\n",l,r,z,y);
		int mid=l+r>>1;
//		printf("%d %d %d\n",l,r,mid);cin.get();
		if(mid<z) return ask_max(k*2+1,mid+1,r,z,y);
		else if(y<=mid) return ask_max(k*2,l,mid,z,y);
		else return Max(ask_max(k*2,l,mid,z,mid),ask_max(k*2+1,mid+1,r,mid+1,y));
	}
};

int n,a[M],maxx=-1;;
Xtree xt;

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	xt.change(1,0,M,a[1],1);
//	printf("enter\n");
	for(int i=2;i<=n;i++){
		int w=xt.ask_max(1,0,M,0,a[i]-1);
//		printf("%d ",w);
		maxx=Max(maxx,w+1);
		xt.change(1,0,M,a[i],w+1);
	}
	printf("%d",maxx);
	return 0;
}

1.3第二种优化

这一次我们的优化要重新设计状态。
思考这样一个问题,我们能否使得我们的合法序列为有序的,从而可以二分取寻找最优值?
我们这样来设计状态:
\(f_i\)表示以长度为i的最长上升子序列的结尾的数值(不是下标)的最小值。
首先关注一下这个f数组的有序性。
我们发现这个f数组一定是一个单调递增的序列,否则,如果存在一个\(f_i\)满足\(i<j\)\(f_i>=f_j\),以\(f_j\)结尾的长度为j的最长上升子序列,设它的第i项为k,则以k结尾的长度为i的最长上升子序列一定存在,原因就是j比i要长,并且k比\(f_i\)更优,故一定是一个单调递增序列。
那么我们怎么来利用它的单调性呢?
我们先考虑怎么用先有的序列的每一个元素去维护f数组。
我们设a数组为我们要求的最长上升子序列的那个题目给出的数组。
设此时此刻该用\(a_i\)去更新维护我们的f数组,设当前f数组的长度为len,由f数组的定义可以知道,那么当前a数组的最长上升子序列为len。
容易想到的是,如果\(f_len\)要小于\(a_i\)的话,那么我们可以另\(f_{++len}=a_i\),更新当前最长上升子序列的最优值。
接下来最重要的问题,也是这个算法的主体,就是\(a_i\)可能可以更新len以前f数组的值,容易想到,如果满足\(f_len\)要小于\(a_i\),那\(a_i\)就没法去更新其余的f值,如果不是这种情况呢?
我们思考一下,可以得出以下结论:
如果\(f_q\)\(a_i\)要小,那么\(a_i\)就可以接在它后面,去更新长度为q+1的最长上升子序列的末尾值。
但是如果\(f_{q+1}\)要比\(a_i\) 要小的话,显然,虽然\(a_i\)满足条件,但是却不能够更新\(f_{q+1}\)
那什么时候\(a_i\)才能更新呢?
当且仅当\(a_i\)\(f_q\)要大并且\(a_i\)要比\(f_{q+1}\)要小!
又因为f数组是一个单调上升的数组。
所以我们可以在f数组里二分。
这里二分推荐使用lower_bound和upper_bound,这两个函数不仅在STL里有,其余时候也可以用。只是我习惯打很多很多的头文件,因此不知道这两个函数在哪个头文件里。。。算了,无伤大雅。
顺便说一句。前者返回的是第一个大于等于的值,后者返回的是第一个大于的值。
具体用法看代码,upper_bound类似
其实也可以自己打一个二分查找,二分不算太难。
代码:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;

int a[N],f[N],n;

int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	int len=1;
	f[len]=a[1];
	for(int i=2;i<=n;i++){
		if(a[i]>f[len]) f[++len]=a[i];
		else{
			int w=lower_bound(f+1,f+len+1,a[i])-f;
			f[w]=a[i];
		}
	}
	printf("%d",len);
}

2扩展

2.1求最长不下降子序列。

其实求最长不下降子序列的方法类似,读者不妨自己去推一下。这里只放代码
线段树:

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 400100
#define M 100100
using namespace std;

inline int Max(int a,int b){
	return a>b?a:b;
}

struct Xtree{//point_change square_ask no_lazy
	int p[N<<3];
	
	inline void pushup(int k){
		p[k]=Max(p[k*2],p[k*2+1]);
	}
	
	inline void change(int k,int l,int r,int w,int x){
		if(l==r&&r==w){
			p[k]=x;
			return;
		}
		int mid=l+r>>1;
//		printf("%d %d %d %d\n",l,r,mid,w);cin.get();
		if(w<=mid) change(k*2,l,mid,w,x);
		else change(k*2+1,mid+1,r,w,x);
		pushup(k);
	}
	
	inline int ask_max(int k,int l,int r,int z,int y){
		if(l==z&&r==y) return p[k];
		int mid=l+r>>1;
		if(y<=mid) return ask_max(k*2,l,mid,z,y);
		else if(mid<z) return ask_max(k*2+1,mid+1,r,z,y);
		else return Max(ask_max(k*2,l,mid,z,mid),ask_max(k*2+1,mid+1,r,mid+1,y));
	}
};
Xtree xt;

int n,a[N],b[N],maxx;

int main(){
//	freopen("dp.out","r",stdin);
//	freopen("1.out","w",stdout);
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	xt.change(1,0,M<<1,a[1],1);
	for(int i=2;i<=n;i++){
		int w=xt.ask_max(1,0,M<<1,0,a[i]);
		maxx=Max(maxx,w+1);
		xt.change(1,0,M<<1,a[i],w+1);
	}
	printf("%d\n",n-maxx);
}

二分:

	f[1]=a[1];int len=1;
	for(int i=2;i<=tail;i++)
	{
		if(a[i]<=f[len]) f[++len]=a[i];
		else
		{
			int l=1,r=len;
			while(l<r)
			{
				int mid=l+r>>1;
				if(f[mid]<a[i]) r=mid;
				else l=mid+1;
			}
			f[l]=a[i];
		}
	}

这里手写了一个二分,主要是因为做导弹拦截的时候还不会lower_bound和upper_bound,那时候的码风也和现在不同。

2.2求最长公共子序列。

接下来的主要介绍将最长公共子序列转化为最长上升子序列来求解。

我们设第一个序列为\(a_1,a_2,...a_n\),第二个序列为\(b_1,b_2,...b_m\)

接下来的操作是,对a中的元素从小到大排序,变成\(a_i,a_j,...a_k\),a数组排序前后的两个不同的数组之间设置一个映射关系,即有 \(a_1\rightarrow a_i,a_2\rightarrow a_j,...a_n\rightarrow a_k\),b中属于a中的元素,则也做此类映射,同时b中不属于a的元素全部去掉,程序中实现时只需要标记一下。

这里需要再加上一个操作,映射后,如果a中有q个x元素,b中有p个,如果p比q小,那么要把着p个元素减到q个。不然结果会出错。

这样在映射后,a数组变成了一个不下降的序列,只需要对b数组跑一个最长不下降子序列即可,因为在映射后,b中任何一个不下降子序列都是与a的一个公共子序列。

洛谷上的题目:https://www.luogu.com.cn/problem/P1439

这个题目有一定的特殊性,因为都是1到n的一个全排列,所以去重操作就不用了。

\(O(nm)\) 做法

#include<iostream>
using namespace std;
int dp[1001][1001],a1[2001],a2[2001],n,m;
int main()
{
   //dp[i][j]表示两个串从头开始,直到第一个串的第i位 
   //和第二个串的第j位最多有多少个公共子元素 
   cin>>n>>m;
   for(int i=1;i<=n;i++)scanf("%d",&a1[i]);
   for(int i=1;i<=m;i++)scanf("%d",&a2[i]);
   for(int i=1;i<=n;i++)
    for(int j=1;j<=m;j++)
     {
     	dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
     	if(a1[i]==a2[j])
     	dp[i][j]=max(dp[i][j],dp[i-1][j-1]+1);
     	//因为更新,所以++; 
     }
   cout<<dp[n][m];
}

\(O(nlogn)\)做法

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
#include<sstream>
#include<queue>
#include<map>
#include<vector>
#include<set>
#include<deque>
#include<cstdlib>
#include<ctime>
#define dd double
#define ll long long
#define ull unsigned long long
#define N 100010
#define M number
using namespace std;

int n;
int a[N],b[N];
int m[N],f[N];

inline int Min(int a,int b)
{
	return a>b?b:a;
}

int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		m[a[i]]=i;
	}
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&b[i]);
		b[i]=m[b[i]];
		f[i]=0x7ffffff;
	}
	
	f[1]=b[1];int len=1;
	for(int i=2;i<=n;i++)
	{
	    int l=1,r=len;
		if(b[i]>f[len]) f[++len]=b[i];
		else
		{
			while(l<r)
	    	{
	    		int mid=l+(r-l>>1);
	    		if(f[mid]>b[i]) r=mid;
	    		else l=mid+1;
	    	}
	    	f[l]=Min(f[l],b[i]);
		}
	}
	
	printf("%d",len);
	return 0;
}

引用

  1. https://www.luogu.com.cn/blog/pks-LOVING/junior-dynamic-programming-dong-tai-gui-hua-chu-bu-ge-zhong-zi-xu-lie
posted @ 2021-02-17 07:52  hyl天梦  阅读(43)  评论(0编辑  收藏  举报