最長不下降子序列LIS

最長不下降子序列問題

最長上升子序列問題是解決很多問題的根本,它能幫助你理解二分的思想。

引言

考慮一下:對於一個序列 nn ,請你查找nn中最長的子序列aa,使得任意 i<ji<ja[i]<=a[i]a[i]<=a[i].

例如一個長度爲55nn=55 33 11 22 44;
顯然,它的最長不下降子序列就是 11 22 44.
我們可以想一下自己是如何看出它的最長不下降子序列的.
首先,第一個數是55,前面沒有數,所以它可以是子序列的一部分,那麼我們可以將它放到考慮的第一位:
55 ? ? ? ?
因爲放了一個數,所以答案要加一:
ans=1ans=1
第二個數是33,有兩種選擇,一種是插入到剛插入的數的後面,第二種就是替換掉55.因爲5>35>3,所以33不可以放到55的後面去.5533大還在答案中,那我要你55有什麼用?果斷替換:
33 ? ? ? ?
ans=1ans=1
第三個數是11,與33同理,替換:
11 ? ? ? ?
ans=1ans=1
第四個數是22,比剛插入的數小,可以插入,那麼就變爲:
11 22 ? ? ?
ans=2ans=2
第五個數同理插入:
11 22 44 ? ?
ans=3ans=3
至此,我們的大腦(?)(?)處理完了這樣一個長度爲55的最長不下降子序列,當長度很小時我們能順利解決,剩下的就交給計算機啦.

二分求解

在我們模擬的時候,有這樣一個操作,當新讀入的數字小於答案數列的第ansans個數的時候,我們需要找到要用讀入數字替換掉的位置.這個時候,選擇從第11個數挨個比對到第ansans個數就很睿智 ,於是我們選擇二分求解.
看這樣一個序列:
11 22 44 66 88 1010
假設新讀入的數是3.
那麼我們取這個元素個數爲66的序列的中間數: 44
4433大,而這個序列是單調遞增的,要替換的位置一定在左側.我們取左側的序列.
11 22 44
中間數爲22, 2<32<3,那麼我們取右側的序列
44
可以判斷,序列中只有一個元素,要替換的數就決定是它啦!
這樣子尋找就省去了O(N)O(N)的複雜度轉爲OO((loglogn))了,爲我們節省了很多時間.
代碼實現

#include <cstdio>
#include <iostream>
#define maxn 100001
#define minn -99999
using namespace std;
int n,a[maxn],f[maxn],ans;//f數組就相當於上面的答案數組,a數組存的是數的值.
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    f[0]=minn;
    for(int i=1;i<=n;i++){
        if(a[i]>f[ans]){//直接向答案序列後加數
            f[ans+1]=a[i];
            ans++;
        }
        else{
            int l=0,r=ans;
            while(l<r){
                int mid=(l+r)>>1;
                if(f[mid]>a[i]){
                    r=mid;//如果中間數比要加的數大,那麼就要取左側序列
                }
                else{
                    l=mid+1;//如果中間數比要加的數小,那麼就要取右側序列
                }
            }
            f[l]=a[i];//最後替換
        }
    }
    printf("%d\n",ans);
    return 0;
}

更簡潔的寫法

你可能會問了:有沒有更簡潔的寫法來實現這樣高效的二分呢?答案是有的.在我們的c++ STL庫中就有這樣的函數,來幫助我們實現查找.
在< algorithm > 庫中,有個叫lower_bound的函數,先看他的函數定義:

template< class ForwardIt, class T >
ForwardIt lower_bound( ForwardIt first, ForwardIt last, const T& value );

它的功能是:
返回指向範圍 [first, last) 中首個不小於(即大於或等於) value 的元素的迭代器,或若找不到這種元素則返回 last 。

如果想了解更多關於lower_bound的嚴格說明,請點擊傳送門
那麼我們有了這個工具之後就可以這樣改進我們的程序,讓它更簡潔.

#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cstring>
#define maxn 100001
using namespace std;
int n,a[maxn],f[maxn],ans;
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    memset(f,10000,sizeof f);//要初始化的大,後面的比較就不會因爲值爲0出錯
    f[0]=-1;//vis[0]除外,它要設爲-1
    for(int i=1;i<=n;i++){
        int v=lower_bound(f,f+1+ans,a[i])-f;//在1到ans的區間中尋找第一個比a[i]大的.
        ans=max(ans,v);//更新答案
        f[v]=min(f[v],a[i]);//替換掉比a[i]大的
    }
    printf("%d\n",ans);
    return 0;
} 
 

這樣就將二分的過程運用STL函數省去了,提高了我們的編程效率.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章