求最長上升子序列的三種經典方案:
題型簡介:
給定一個長度為 $ 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 $ 的最大值。
$ solution\quad 1: $
先說一種最普遍的方法,因為所求為子序列,所以這道題很容易想到一種線性動態規划。我們需要求最長上升子序列,為了上升我們肯定要知道我們當前階段最后一個元素為多少,為了最長我們還要知道當前我們的序列有多長。我們可以用前者來充當第一維描述:設 $ F[i] $ 表示以 $ A[i] $ 為結尾的最長上身子序列的長度,為了保證保證元素單調遞增我們肯定只能從 $ i $ 前面的且末尾元素比 $ A[i] $ 小的狀態轉移過來:
$ 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] $ 實在是太麻煩了,所以我們換一種思維方法:對於原序列每個元素,它有一個下標和一個權值,最長上升子序列實質就是求最多有多少元素它們的下標和權值都單調遞增。
於是我們將 $ A $ 數組的每一個元素先記下他現在的下標,然后按照權值從小到大排序。接着我們按從小到大的順序枚舉 $ A $ 數組,(此時權值已經默認單調遞增了)我們的轉移也就變成從之前的標號比它小的狀態轉移過來,這個我們只需要建立一個與編號為下標維護長度的最大值的樹狀數組即可,枚舉 $ A $ 數組時按元素的序號找到它之前序號比他小的長度最大的狀態更新,然后將它也加入樹狀數組中。 期望復雜度: $ O(nlog(n)) $
$ 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;
}
關於樹狀數組求最長上升子序列的方案及方案數,這個需要結構體來實現,構建結構體數組使其中每一個元素可以包含多個信息,這樣在樹狀數組更新時可以做到順便兼顧記錄前驅,以及累計方案數(需要去重)。關於具體如何實現,可以參見我出的這場考試中的第二題:五彩棒
另外真的很抱歉,博主現在拿的平板,家里電腦壞了,不能具體解答。
$ 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;
}