1. 程式人生 > 其它 >最長上升子序列(LIS)的貪心演算法

最長上升子序列(LIS)的貪心演算法

技術標籤:演算法

我們知道通過dp的演算法可以求出一段序列的最長上升子序列,時間複雜度為o(n^2)。下面來介紹一種o(nlogn)的演算法來求LIS。

設一段序列的長度為n,我們需要的是一個輔助陣列f,長度最長為n,其實際長度是動態的,也表徵了最長上升子序列的長度。
對於f[i],其儲存的是對於已經掃描過的序列中,所有長度為i上升子序列的最後一個數的最小值。如:
有序列1 4 2 3
所有長度為2的上升子序列有1 2,1 3.
那麼f[2] = 2

這樣做,可以保證對於同樣長度的上升子序列,我們給出的是其尾元素的最小值,那麼這樣在掃描後續序列時,就必定更有可能構造出最長上升子序列。

首先可以證明整個f陣列是單調遞增的,即對於任意的索引i < j,都有 f[i] < f[j]。反證法如下:
f[i] == f[j],對於f[j]對應的長度為j的最長上升子序列,其倒數第2個元素值一定小於f[j],那麼這個倒數第2個元素值可以作為f[i],則有f[i] < f[j]
同理可證f[i] > f[j]的情形。

繼續分析:
如已掃描序列1 4 2 3,根據我們的定義,有f[3] = 3(序列1 2 3的尾元素),再往後掃描時,如果是4,即序列為1 4 2 3 4時,就可以根據f[3] = 3(3 < 4)往後延長一個LIS長度(即f長度變為4),而且使得f[4] = 4

(即1 2 3 4的尾元素)。這樣顯然是合理的,而且可以為以後LIS長度的更新以及尾元素的更新做鋪墊。

或者從反面考慮:如果我們f[i]儲存的不是長度為i的最長上升子序列的尾元素,如f[3] = 5(存1 4 5的尾元素),那麼掃描後續元素就無法最大化地延長上升序列,無法達到題目要求。

注意在思考問題時,從集合的角度考慮問題,即每個f[i]首先是關於所有長度為i的上升序列的函式,其次,f[i]存的是這些上升序列尾元素的最小值。

那麼在掃描原陣列的時候,對於第i個數,可以根據前i-1個數得到的f陣列的結果,對f陣列進行長度或內容的更新,具體表現如下:(設f陣列的實際長度為cnt
1、若a[i] > f[cnt]

,則對於從1至cntf中的每一個f[j],都有a[i] > f[j](因為前面已經證明f單調遞增)。那麼當前的a[i]可以作為長度為cnt+1的上升子序列的最小元素(因為當前只有這麼一個長度為cnt+1的上升序列),即f[cnt+1] = a[i],同時cnt++.

2、若a[i] <= f[cnt],則無法使得上升序列變長,因為f[cnt]是長度為cnt的所有上升序列尾元素的最小值。因此,它的作用是,更新對應的一個f。顯然,對於從後往前第一個小於a[i]f[j]而言,a[i]可以接到f[j]所代表的那個最優上升序列後面,從而更新f[j+1](長度為j+1的上升序列的尾元素最小值)。因為從剛才的敘述可以知道,原f[j+1] >= a[i]。故這樣a[i]的作用是直接改變f的一個恰當元素值,同時為最終cnt的更新作準備(通過將一個f的元素值變小,使得後面元素接到前面更具有可能)。

AC程式碼:

#include<stdio.h>
#include<iostream>
#include<string.h>
#include<stdlib.h>
#include<algorithm>
using namespace std;
const int N = 1e5+5;
int n;
int a[N],f[N];
int cnt = 0;
int Find(int x){
	int l = 1,r = cnt;
	while(l < r){
		int mid = l+r >> 1;
		if(f[mid] >= x)
			r = mid;
		else
			l = mid+1;
	}
	
	return l;
}
int main(){
	scanf("%d",&n);
	for(int i = 1;i <= n;i++){
		scanf("%d",&a[i]);
	}
	
	f[++cnt] = a[1];
	for(int i = 2;i <= n;i++){
		if(a[i] > f[cnt])
			f[++cnt] = a[i];
		else
			f[Find(a[i])] = a[i];
	}
	printf("%d\n",cnt);
	return 0;
}