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
相關文章
相關標籤/搜索