介紹
最長上升子序列問題,也就是Longest increasing subsequence縮寫爲LIS。是指在一個序列中求長度最長的一個上升子序列的問題。
問題描述:
給出一個序列a1,a2,a3,a4,a5,a6,a7….an,求它的一個子序列(設爲s1,s2,…sn),使得這個子序列滿足這樣的性質,s1 <s2 <s3 <… <sn並且這個子序列的長度最長。輸出這個最長的長度(爲了簡化該類問題,我們將諸如最長下降子序列及最長不上升子序列等問題都看成同一個問題,其實仔細思考就會發現,這其實只是 <符號定義上的問題,並不影響問題的實質)
例如有一個序列:1 7 3 5 9 4 8,它的最長上升子序列就是 1 3 4 8 長度爲4。最長上升子序列問題有三種解法,第一種就是暴力破除,這種解法時間複雜度爲在這裏就不介紹了;一個是動態規劃的解法,時間複雜度爲O(n^2);最後介紹一種非常nice的解法,時間複雜度爲O(nlogn)。
動態規劃
dp[i表示以i結尾的子序列中LIS的長度。然後我用 dp[j](0 ≤j<i)來表示在i之前的LIS的長度。然後我們可以看到,只有當 a[i] >a[j] 的時候,我們需要進行判斷,是否將a[i]加入到dp[j]當中。爲了保證我們每次加入都是得到一個最優的LIS,有兩點需要注意:
第一,每一次,a[i]都應當加入最大的那個dp[j],保證局部性質最優,也就是我們需要找到 max(dp[j](0 ≤j<i));
第二,每一次加入之後,我們都應當更新dp[j]的值,顯然, dp[i]=dp[j]+1 。如果寫成遞推公式,我們可以得到 dp[i]=max(dp[j](0<= j<i))+(a[i]>a[j]?1:0)。
public static int dpLIS(int[] a){
int[] dp=new int[a.length];
int max;
dp[0]=1;
for(int i=1;i<a.length;i++){
for(int j=0;j<i;j++){
if(a[i]>a[j]&&dp[j]+1>dp[i]){
dp[i]=dp[j]+1;
}
}
}
for(int i=max=0;i<dp.length;i++){
if(dp[i]>max)
max=dp[i];
}
return max;
}
O(nlogn)的方法
設 A[t]表示序列中的第t個數,F[t]表示從1到t這一段中以t結尾的最長上升子序列的長度,初始時設F [t] = 0(t = 1, 2, …, len(A))。則有動態規劃方程:F[t] = max{1, F[j] + 1} (j = 1, 2, …, t - 1, 且A[j] <A[t])。現在,我們仔細考慮計算F[t]時的情況。假設有兩個元素A[x]和A[y],滿足
(1)x < y < t
(2)A[x] <A[y] <A[t]
(3)F[x] = F[y]
此時,選擇F[x]和選擇F[y]都可以得到同樣的F[t]值,那麼,在最長上升子序列的這個位置中,應該選擇A[x]還是應該選擇A[y]呢?
很明顯,選擇A[x]比選擇A[y]要好。因爲由於條件(2),在A[x+1] … A[t-1]這一段中,如果存在A[z],A[x] < A[z] < a[y],則與選擇A[y]相比,將會得到更長的上升子序列。
再根據條件(3),我們會得到一個啓示:根據F[]的值進行分類。對於F[]的每一個取值k,我們只需要保留滿足F[t] =k的所有A[t]中的最小值。設D[k]記錄這個值,即D[k] = min{A[t]} (F[t] = k)。
注意到D[]的兩個特點:
(1) D[k]的值是在整個計算過程中是單調上升的。
(2) D[]的值是有序的,即D[1] < D[2] < D[3] < …<D[n]。
利用D[],我們可以得到另外一種計算最長上升子序列長度的方法。設當前已經求出的最長上升子序列長度爲 len。先判斷A[t]與D[len]。若A [t] > D[len],則將A[t]接在D[len]後將得到一個更長的上升子序列,len =len + 1,D[len] = A [t];否則,在D[1]..D[len]中,找到最大的j,滿足D[j] < A[t]。令k = j + 1,則有A[t]≤D[k],將A[t]接在D[j]後將得到一個更長的上升子序列,更新D[k] = A[t]。最後,len即爲所要求的最長上升子序列的度。
在上述算法中,若使用樸素的順序查找在D[1]..D[len]查找,由於共有O(n)個元素需要計
算,每次計算時的復度 是O(n),則整個算法的 時間複雜度爲O(n^2),與原來的算法相比沒有任何進步。但是由於D[]的特點(2),我們在D[]中查找時,可以使用二分查找高效地完成,則整個算法 的時間複雜度下降爲O(nlogn),有了非常顯著的提高。需要注意的是,D[]在算法結束後記錄的並不是一個符合題意的最長上升子序列。
於是我們就能夠得到O(nlogn)方法的實現:
public static int binLIS(int[] arr){
int[] maxV=new int[arr.length];
maxV[0] = arr[0]; /* 初始化 */
int len = 1;
for(int i = 1; i < arr.length; ++i) /* 尋找arr[i]屬於哪個長度LIS的最大元素 */
{
if(arr[i] > maxV[len-1]) /* 大於最大的自然無需查找,否則二分查其位置 */
{
maxV[len++] = arr[i];
}else
{
int pos = binSeach(maxV,len,arr[i]);
maxV[pos] = arr[i];
}
}
return len;
}
public static int binSeach(int[] a,int len,int value){
int left=0,right=len-1,mid;
while(left<=right){
mid=left+(right-left)/2;
if(value>a[mid])
left=mid+1;
else if(a[mid]>value)
right=mid-1;
else {
return mid;
}
}
return left;
}
}