九度OJ 1533 最長上升子序列 (基於貪心和二分查詢)
- 題目描述:
-
給定一個整型陣列, 求這個陣列的最長嚴格遞增子序列的長度。 譬如序列1 2 2 4 3 的最長嚴格遞增子序列為1,2,4或1,2,3.他們的長度為3。
- 輸入:
-
輸入可能包含多個測試案例。
對於每個測試案例,輸入的第一行為一個整數n(1<=n<=100000):代表將要輸入的序列長度
輸入的第二行包括n個整數,代表這個陣列中的數字。整數均在int範圍內。
- 輸出:
-
對於每個測試案例,輸出其最長嚴格遞增子序列長度。
- 樣例輸入:
-
4 4 2 1 3 5 1 1 1 1 1
- 樣例輸出:
-
2 1
【思路分析】
求LIS的經典問題,用到的不是DP裡面O(n*n)的方法,而是用了基於貪心、二分查詢的O(nlogn)方法(為什麼沒有命名成XXX演算法 = =)
設a[n]為原序列,d[n]為長度為n的上升子序列的最後一個元素,當有多個長度為n的上升子序列時,取這些子序列中末尾最小的元素作為d[n]的值。為什麼選最小的呢?因為最小的最有“潛力”(貪心)。舉個例子,假設原序列為:2,1,8,3,7,5,6,對於d[3],長度為3的上升子序列有1,3,7和1,3,5兩個,那麼d[3]取值為5,即末尾最小的元素。因為在這個子序列後可能存在x滿足 5 < x < 7(即x == 6),因此要是子序列確定為1,3,7的話,則顯然不如1,3,5,6長。可見,d陣列中的元素是單調遞增的。
有了上述的貪心策略,便可以進行下面的操作了。首先,令len = 1,d[1] = a[1],當a[i] > d[len]時,有d[++len] = a[i],即加入新的元素來擴充上升子序列;否則,從d[1]到d[len - 1]找到一個j,使得a[i]滿足: d[j - 1] < a[i] < d[j],這時有d[j] = a[i],即用a[i]來替換d[j](也就是a[i]比d[j]更有“潛力”)。又由於d陣列是單調遞增的,因此可以用二分查詢O(logn)很快找到j的值。
最後用a = {2,1,5,3,6,4,8,9}這個序列來過一遍上述的流程。首先,len = 1,d[1] = a[1] = 2。則a[i],len,d[len]的值的變化見下表:
程式碼如下:
#include <iostream> #include <cstdio> #include <cstdlib> #include <cstring> #include <cmath> #include <algorithm> using namespace std; const int maxn = 100005; int n; int a[maxn]; int d[maxn];//記錄長度為i的上升子序列最後一個元素的值 int binSearch(int key,int left,int right) { while(left <= right) { int mid = (left + right) >> 1; if(key > d[mid] && key <= d[mid + 1])//貪心策略 { return mid; } else if(key > d[mid]) { left = mid + 1; } else { right = mid - 1; } } return 0; } int LIS(int n) { d[1] = a[1]; int len = 1; int j = 0; for(int i = 2;i <= n;i++) { if(d[len] < a[i]) { j = ++len;//直接向後插入 } else { j = binSearch(a[i],1,len) + 1;//找到替換位置 } d[j] = a[i]; } return len; } void init() { for(int i = 1;i <= n;i++) { scanf("%d",&a[i]); } } void solve() { printf("%d\n",LIS(n)); } int main() { while(scanf("%d",&n) != EOF) { init(); solve(); } return 0; }