LIS(最長上升子序列)的三種經典求法

求最長上升子序列的三種經典方案:

題型簡介:

給定一個長度爲 $ N $ 的數列,求它數值單調遞增的子序列長度最大爲多少。即已知有數列 $ A $ , $ A={A_1,A_2....A_n} $ ,求 $ A $ 的任意子序列 $ B $ ( $ B={A_{k_1},A_{k_2}....A_{k_p}} $ ),使 $ B $ 知足 $ k_1<k_2<....<k_p $ 且 $ A_{k_1}<A_{k_2}<....<A_{k_p} $ 。現求 $ p $ 的最大值。html



$ solution\quad 1: $

先說一種最廣泛的方法,由於所求爲子序列,因此這道題很容易想到一種線性動態規劃。咱們須要求最長上升子序列,爲了上升咱們確定要知道咱們當前階段最後一個元素爲多少,爲了最長咱們還要知道當前咱們的序列有多長。咱們能夠用前者來充當第一維描述:設 $ F[i] $ 表示以 $ A[i] $ 爲結尾的最長上身子序列的長度,爲了保證保證元素單調遞增咱們確定只能從 $ i $ 前面的且末尾元素比 $ A[i] $ 小的狀態轉移過來:ios

$ F[i]=^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]+1\} $

初始值爲 $ F[0]=0 $ ,而答案能夠是任何階段中只要長度最長的那一個,因此咱們邊轉移邊統計答案。算法

複雜度: $ O(n^2) $數組

$ code\quad 1: $

#include<iostream>
#include<cstdio>
#define ll long long
#define rg register int
using namespace std;

int n,ans;
int a[10005];
int f[10005];

int main(){ cin>>n;
	for(rg i=1;i<=n;++i) cin>>a[i];
	for(rg i=1;i<=n;++i){
		for(rg j=1;j<i;++j) //枚舉轉移
			if(a[j]<a[i])f[i]=max(f[i],f[j]);
		++f[i]; ans=max(ans,f[i]); //更新答案
	}cout<<ans<<endl;
	return 0;
}

$ solution\quad 2: $

咱們發現上一種方法會枚舉前面較小的位置,咱們考慮可否用數據結構優化,首先將轉移方程列一下:數據結構

$ F[i]=^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]+1\} $

咱們發現大括號中的 $ 1 $ 與 $ j $ 沒有任何關係,因此咱們將它提取出來:優化

$ F[i]=1+~~^{max}_{0\leq j<i,A[j]<a[i]}\{F[j]\} $

而後咱們發現咱們只須要將比 $ i $ 小的全部的符合 $ A[j]<A[i] $ 的 $ F[j] $ 的最大值求出來,可是這個條件 $ A[j]<A[i] $ 實在是太麻煩了,因此咱們換一種思惟方法:對於原序列每一個元素,它有一個下標和一個權值,最長上升子序列實質就是求最多有多少元素它們的下標和權值都單調遞增。spa

因而咱們將 $ A $ 數組的每個元素先記下他如今的下標,而後按照權值從小到大排序。接着咱們按從小到大的順序枚舉 $ A $ 數組,(此時權值已經默認單調遞增了)咱們的轉移也就變成從以前的標號比它小的狀態轉移過來,這個咱們只須要創建一個與編號爲下標維護長度的最大值的樹狀數組便可,枚舉 $ A $ 數組時按元素的序號找到它以前序號比他小的長度最大的狀態更新,而後將它也加入樹狀數組中。 指望複雜度: $ O(nlog(n)) $code

$ code\quad 2: $

#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
#define rg register int
using namespace std;

int n;
int s[200005];

struct su{
    int v,id; //按照權值爲第一關鍵字保證算法正確性
    inline bool operator <(su x){
		if(v==x.v)return id>x.id; //按照序號從大到小能夠保證所求爲上升子序列
		return v<x.v; //(針對標號)由於相同權值的數,前面的狀態不能轉移給後面
	}                 //(針對標號)從大到小枚舉就不會出現這種狀況
}a[200005];

inline void add(int x,int y){
    for(;x<=n;x+=x&-x) s[x]=max(s[x],y);
}

inline int ask(int x){
    rg res=0;
    for(;x>=1;x-=x&-x) res=max(s[x],res);
    return res;
}

int main(){
    cin>>n;
    for(rg i=1;i<=n;++i)
        cin>>a[i].v,a[i].id=i;
    sort(a+1,a+n+1);
    for(rg i=1;i<=n;++i)
		add(a[i].id,ask(a[i].id)+1);
    cout<<ask(n)<<endl;
    return 0;
}

關於樹狀數組求最長上升子序列的方案及方案數,這個須要結構體來實現,構建結構體數組使其中每個元素能夠包含多個信息,這樣在樹狀數組更新時能夠作到順便兼顧記錄前驅,以及累計方案數(須要去重)。關於具體如何實現,能夠參見我出的這場考試中的第二題:五彩棒htm

另外真的很抱歉,博主如今拿的平板,家裏電腦壞了,不能具體解答。blog


$ solution\quad 3: $

這是最快的方法:貪心加二分查找

我以前說過:咱們確定要知道咱們當前階段最後一個元素爲多少,還有當前咱們的序列有多長。前兩種方法都是用前者作狀態,咱們爲何不能夠用後作狀態呢?:設 $ F[i] $ 表示長度爲 $ i $ 的最長上升子序列的末尾元素的最小值,咱們發現這個數組的權值必定單調不降(仔細想想,這就是咱們貪心的來由)。因而咱們按順序枚舉數組 $ A $ ,每一次對 $ F[] $ 數組二分查找,找到小於 $ A[i] $ 的最大的 $ F[j] $ ,並用它將 $ F[j+1] $ 更新。

注意:這個方法雖快,可是講實話仍是樹狀數組好一些,由於對於最長上升子序列的方案輸出和計算方案數(upd:很抱歉咕掉了,如今補一下坑,在上面第二種方法結尾),樹狀數組有不少優點!二分查找由於貪心的緣故會被限制。

指望複雜度: $ O(nlogn) $

$ code\quad 3: $

#include<iostream>
#include<cstdio>
#include<algorithm>
#define ll long long
#define rg register int
using namespace std;

int n;
int a[200005];
int f[200005];

int main(){
	cin>>n;
	for(rg i=1;i<=n;++i) cin>>a[i];
	rg ans=1; f[1]=a[1];
	for(rg i=2;i<=n;++i){
		rg l=1,r=ans,mid;
		while(l<=r){
			mid=(l+r)>>1;
			if(a[i]<=f[mid])r=mid-1;
			else l=mid+1;
		}f[l]=a[i];
		if(l>ans)++ans;
	}cout<<ans<<endl;
	return 0;
}
相關文章
相關標籤/搜索