今天花了很長時間終於弄懂了這個算法……畢竟找一個好的講解真的太難了,所以勵志我要自己寫一個好的講解QAQ
這篇文章是在懂了這個問題n^2解決方案的基礎上學習。
解決的問題:給定一個序列,求最長不下降子序列的長度(nlogn的算法沒法求出具體的序列是什么)
定義:a[1..n]為原始序列,d[k]表示長度為k的不下降子序列末尾元素的最小值,len表示當前已知的最長子序列的長度。
初始化:d[1]=a[1]; len=1; (0個元素的時候特判一下)
現在我們已知最長的不下降子序列長度為1,末尾元素的最小值為a[1],那么我們讓i從2到n循環,依次求出前i個元素的最長不下降子序列的長度,循環的時候我們只需要維護好d這個數組還有len就可以了。
關鍵問題就是怎么維護?
可以看出我們是要用logn的復雜度維護的。實際上利用了d數組的一個性質:單調性。(長度更長了,d[k]的值是不會減小的)
考慮新進來一個元素a[i]:
如果這個元素大於等於d[len],直接讓d[len+1]=a[i],然后len++。這個很好理解,當前最長的長度變成了len+1,而且d數組也添加了一個元素。
如果這個元素小於d[len]呢?說明它不能接在最后一個后面了。那我們就看一下它該接在誰后面。
准確的說,並不是接在誰后面。而是替換掉誰。因為它接在前面的誰后面都是沒有意義的,再接也超不過最長的len,所以是替換掉別人。那么替換掉誰呢?就是替換掉那個最該被它替換的那個。也就是在d數組中第一個大於它的。第一個意味着前面的都小於等於它。假設第一個大於它的是d[j],說明d[1..j-1]都小於等於它,那么它完全可以接上d[j-1]然后生成一個長度為j的不下降子序列,而且這個子序列比當前的d[j]這個子序列更有潛力(因為這個數比d[j]小)。所以就替換掉它就行了,也就是d[j]=a[i]。其實這個位置也是它唯一能夠替換的位置(前面的替了不滿足d[k]最小值的定義,后面替換了不滿足不下降序列)
至於第一個大於它的怎么找……STL upper_bound。每次復雜度logn。
至此,我們就神奇的解決了這個問題。按照這個思路,如果需要求嚴格遞增的子序列怎么辦?
仍然考慮新進來一個元素a[i]:
如果這個元素大於d[len],直接讓d[len+1]=a[i],然后len++。這個很好理解,當前最長的長度變成了len+1,而且d數組也添加了一個元素。
如果這個元素小於等於d[len]呢?說明它不能接在最后一個后面了。那我們就看一下它該接在誰后面。
同樣的道理,只是換成lower_bound即可。每次復雜度logn。
--------2018.4.14更新--------
很久沒看,沒想到這篇文章有這么多人閱讀了,感覺最長遞增子序列這里講的不是太清楚,因此補充一下。
最長遞增子序列和最長不下降子序列的不同之處在於,d數組的單調性更嚴格了:一定是單調遞增的。
可以用反證法來證明這一點:假設有d[i]=d[i+1],也就是長度為i+1的子序列最后一位最小是d[i+1],那假設這個子序列是a[1], a[2], ..., a[i+1],在這個子序列里面,a[i]<d[i+1]肯定成立,不然就不是最長遞增子序列了。那也就是說a[i]<d[i]了,那a[1], a[2], ..., a[i]就構成了一個長度為i的子序列,而且最后一個數比d[i]小。那d[i]肯定就不符合定義了。
那這個性質有什么意義呢?
仍然考慮新進來一個元素a[i]:
如果這個元素大於d[len],直接讓d[len+1]=a[i],然后len++。這個很好理解,當前最長的長度變成了len+1,而且d數組也添加了一個元素。
如果這個元素等於d[len],那么可以保證d[1..len-1]都是小於a[i]的(根據上面的證明),因此這個元素就沒有什么意義了,直接忽略就好,因為它無法接在任何一個元素d后面產生一個更有優勢的子序列。
如果這個元素小於d[len],那么就在d數組中找到第一個大於等於它的元素(這個元素必然存在,至少d[len]就是),把這個元素替換成a[i]即可。
實際上可以發現,小於等於的時候可以統一按照lower_bound替換的方式來處理。
這樣做肯定是對的,而鄺斌的模板上實際上是求的最長不下降子序列,而不是最長上升子序列。不信可以測試一下"1 2 3 2 3 2"這個樣例。切記,不要迷信權威,學會自己思考。
------------------------------------
下面是最長不下降子序列的代碼,注釋里面說明了如何改成最長上升子序列。
//最長不下降子序列nlogn Song #include<cstdio> #include<algorithm> using namespace std; int a[40005]; int d[40005]; int main() { int n; scanf("%d",&n); for (int i=1;i<=n;i++) scanf("%d",&a[i]); if (n==0) //0個元素特判一下 { printf("0\n"); return 0; } d[1]=a[1]; //初始化 int len=1; for (int i=2;i<=n;i++) { if (a[i]>=d[len]) d[++len]=a[i]; //如果可以接在len后面就接上,如果是最長上升子序列,這里變成> else //否則就找一個最該替換的替換掉 { int j=upper_bound(d+1,d+len+1,a[i])-d; //找到第一個大於它的d的下標,如果是最長上升子序列,這里變成lower_bound d[j]=a[i]; } } printf("%d\n",len); return 0; }
想了一晚上這個問題終於想通了。前面說的“最該替換的位置”實際上不是很精確,那個位置替換掉是它唯一能夠替換的位置,之所以要替換,就是為了維護d這個數組,讓它始終滿足最初的定義。
nlogn復雜度的最長上升子序列還有樹狀數組的寫法,可以參考我的另一篇文章:https://www.cnblogs.com/acmsong/p/7231069.html。
