LIS及其扩展

1计算长度

1.1朴素DP

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

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}\)这样就保证了咱们解得合法性。c++

代码:算法

#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,那时候的码风也和如今不一样。ui

2.2求最长公共子序列。

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

咱们设第一个序列为\(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的元素所有去掉,程序中实现时只须要标记一下。code

这里须要再加上一个操做,映射后,若是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
相关文章
相关标签/搜索