1. 程式人生 > 其它 >最長上升子序列 LIS【DP的O(n^2)及貪心+二分的O(nlogn)解法】

最長上升子序列 LIS【DP的O(n^2)及貪心+二分的O(nlogn)解法】

技術標籤:資料結構和演算法

最長上升子序列

給你一個長度為N的序列,求其最長上升子序列的長度。

樣例輸入:
6
1 6 2 4 3 5

樣例輸出:
4

解釋:其最長上升子序列的長度為4。可以是{1 2 4 5 }或者{1 2 3 5}

注意:子序列是不連續的。

假設我們把序列儲存在a陣列中,並且從下標1開始儲存。

動態規劃解法

定義 f[i] 表示到a[i] 為止的最長上升子序列的長度。(其中a[i]必須被選中,必須以a[i]結尾)

所以動態轉移方程是 f [ i ] = max(f [ j ] ) + 1.( 1 <= j < i 並且 a [ j ] > a [ i ] )

也就是說 對於位置i來說,只要前面的a[ j ]小於a[ i ],那麼就可以轉移過來,從中選擇一個最大的即可。

程式碼:

#include<iostream>
using namespace std;

const int maxn=10000;
int a[maxn],f[maxn];
 
int main() {
	int n;
	cin>>n;
	for (int i=1; i<=n; i++)
		cin>>a[i];
		
    for (int i=1; i<=n; i++){  //n個數都得算f[i]
        f[i]=1; //最起碼是個1
        for (int j=1; j<i; j++){ //前面的所有j
            if (a[j]<a[i])  //滿足小於
                f[i]=max(f[i],f[j]+1);  //選最大的
        }
    }
    int ans=0;
    for (int i=1; i<=n; i++) //統計答案
        if (f[i]>ans) ans=f[i];
    cout<<ans<<endl;
}

時間複雜度:雙重迴圈,所以複雜度是O(n^2).

貪心+二分解法

對上述的動態規劃演算法考慮優化,外層迴圈,也就是每個數都得計算f[i],這個肯定沒有辦法再優化了。
所以優化的重點就在於內層迴圈找 i 前面的比 a[ i] 小的最大的 f [ j ]。

說到查詢,大家第一時間都會想到二分,但是二分要求陣列得是有序的,f 陣列不滿足,那麼我們可以使得 f 陣列有序嗎?

我們先把樣例的 f 陣列寫出來,如下:

我們觀察 f 陣列劃線的兩個1,前一個表示的是以a[1]也就是1結尾的最長上升子序列長度是1,後一個表示以a[2]也就是6為結尾的最長上升子序列長度也是1。

這兩個1對於後面的轉移是有影響的,但是我們發現,只要是6能轉移的,1肯定都能轉移,所以這個6我們沒有必要儲存下來。

到這裡我們可以總結出,在相同的長度下,我們想盡量讓a[i]更小,這樣狀態更能轉移到後面去,這種優化就是貪心思想的體現。比如剛剛說的樣例,同樣是長度為1,以1結尾比以6結尾好多了,所以我們選擇1,而不用6,劃線的4和3也是同樣的情況。

所以,對於每個長度,我們只需要記錄其最小的那個a[i]。 假設我們把這個陣列稱為g。
g[i]表示長度為 i 的最長上升子序列的最小結尾是 g[ i ]。g陣列的長度就是截止當前的最長上升子序列的長度。

比如當前 g 陣列有m個。
如果 a[i] >g[m] 說明可以用 a[i] 繼續擴充套件.
如果 a[i]<g[m] 對於當前答案沒有影響,但是 a[i] 可能會優化g陣列,所以我們要在 g 陣列中找到第一個小於 a[i] 的數,去優化g陣列。因為g陣列單調的,可以使用二分查詢。

程式碼:

#include<iostream>
using namespace std;

const int maxn=10000;
int a[maxn],g[maxn];
 
int main() {
	int n;
	cin>>n;
	for (int i=1; i<=n; i++)
		cin>>a[i];
	
	int m=0;
	g[0]=-100000000; //初始化	
    for (int i=1; i<=n; i++){
        if (a[i]>g[m]){ //滿足就擴充套件
        	m++;
        	g[m]=a[i];
		}else{ //否則二分
			int l=0,r=m;
			while (l!=r){
				int mid=(l+r+1)/2;
				if (g[mid]<=a[i]) l=mid;
					else r=mid-1;		
			}
			if (g[l]!=a[i]) g[l+1]=a[i]; //特判下相等的情況
		}
    }
   	cout<<m<<endl; //最長上升子序列的長度就是g陣列的長度
}

其中,初始化g[0]為負值,目的是讓a[1]一定可以選入。當然你可以選擇先在g陣列中加入a[1].
二分完畢後,有一個判斷語句,if (g[l]!=a[i]) g[l+1]=a[i]; 因為我們求得是嚴格上升的,所以相同的情況下是不能更新g的。
答案就是g陣列的長度。

時間複雜度:O(nlogn)。