我轉載兩篇文章,同時給出一道poj題目的鏈接 http://poj.org/problem?id=2533
轉自:http://blog.csdn.net/linulysses/article/details/5559262
問題描述: 給定一個序列 An = a1 ,a2 , ... , an ,找出最長的子序列使得對所有 i < j ,ai < aj 。
顯然,暴力算法的時間複雜度是 O(2n ) ,因爲搜索空間呈指數級增長。對於這種問題,如果要找複雜度爲多項式時間的算法,自然而然地會想到動態規劃。首先,要找出一種方法把該問題分解成只有多項式個子問題。考慮 a n。如果最長遞增子序列不 包含 an ,則問題變成要在 An-1 中找最長遞增子序列;否則,在 An-1 之中找最長子序列,且該子序列的最大值不能超 an ,然後再加上 an 。按照這個思路,我們可以用 OPT(i,x i ) 來表示Ai 之中最大值不超 x 的最長遞增子序列的長度,則
OPT(i,x) = max { OPT(i-1,x), ai <= x ? OPT(i-1, ai )+1 : OPT(i-1, x) }
而最終的解則是 OPT(n, max{a1 , a2 , ..., an }) 。現在的問題是,到底有多少個子問題?這取決於 x 有多少個取值。觀察上述遞歸式可知, x 只可能取 An 中的值,因此最多有 n 可能的值。所以,子問題的數量爲 O(n2 ) 個。而每個子問題都可以通過 O(1) 的時間獲解,從而總的時間複雜度是 O(n2 ) 。實現這個算法的時候,爲了能夠使用一個二維的數組 S[n][n] 來存儲狀態,我們可以用數組 B 來保存排過序之後的 序列 b1 , b2 , ..., bn ,從而對所有i < j ,bi < bj 。而 S[i][j] 表示 Ai 中最大值不超過 bj 的最長遞增子序列的長度。
- copy A to B;
- sort B by increasing order;
- // initialize S[1][*]
- for j from 1 to n
- if A[1] > B[j] then S[1][j] := 0
- else S[1][j] := 1
- end
- for i from 2 to n
- for j from 1 to n
- if A[i] > B[j] then S[i][j] := S[i-1][j]
- else
- find the index k of A[i] in B[j]; // k <= j
- S[i][j] := max( S[i-1][j], S[i-1][k]+1 )
- end
- end
- end
- return S[n][n]
上述的實現中,第12行代碼,我們可以使用一個哈希表來實現。我們可以看到,這個實現並不優美,既要一個額外的數組 B 和一次排序,還要動用一個哈希表。可以說,我們對這個算法並不滿意。
那麼,有沒有更好一些的算法呢?注意到排序之後,我們得到新的序列 B,那麼,顯然,A 中最長的遞增子序列也是序列 B 的子序列。因此,我們可以應用求兩個序列的最長公共子序列的經典算法來求解,時間複雜也是 O(n2)。這樣,我們就省去了使用哈希表的“不雅之舉”。然而,最長公共子序列是一個更一般的算法,它不要求在序列的元素之間有序關係 。那麼,我們能不能利用本問題中元素之間的序關係來設計一個優美的算法呢?答案是肯定的。注意中在上面的算法中(姑且稱爲算法1吧),一共有 O(n2 ) 個子問題。事實上,我們從另外的角度看這個問題,從而獲得只有 O(n) 中子問題的算法,只不過,計算每個子問題需要 O(n) 的時間。
首先,假設 An 中最長遞增子序列 L 的最後一個元素是 at 。考慮 L 中在 at 之前的元素 ak ,則 ak <= at ,並且 L 由在 Ak 中包含 ak 的最長遞增子序列和 a t 組成。因此,用 OPT(i) 來表示 Ai 中包含 ai 的最長遞增子序列。因爲L 的最後一個元素必定在 An 中,因此,L 的長度爲 OPT(1), OPT(2), ..., OPT(n) 中的最大值。這也證明了此算法的正確性。接下的問題就是如何計算OPT(i)。事實上,
OPT(i) = max { OPT(j) | j < i 且 aj < ai }
也就是說,通過遍歷一次 已經計算好的 OPT(1), OPT(2), ..., OPT(i-1) 就可以計算出 OPT(i),其時間複雜度爲 O(i)。總的時間複雜度爲 O(1) + O(2) + ... + O(n-1) = O(n2 )。
- // Let S[i] be OPT(i)
- S[1] := 1
- L := S[1]
- for i from 2 to n
- S[i] := 1 // at least contain A[i]
- for j from 1 to i-1
- if A[j] < A[i] then S[i] := max( S[i], S[j]+1 )
- end
- L := max( L, S[i] )
- end
- return L
相比之一,這個算法的實現就很乾淨和優美,且不容易出錯。
O(n2 ) 的時間複雜度似乎已經是很不錯了。那麼,有沒有更快的算法?事實上,存在 O(n logk ) 的算法,其中 k爲最長遞增子序列的長度。爲了達到這個時間複雜度,就需要費點腦筋了。考慮 Ai = a1 , a2 , ..., ai 。記 Tail(X)爲遞增序列 X 的最後一個元素(尾元素),令 Ri,j 表示 Ai 中所有長度爲 j 的遞增子序列的集合。在所有屬於 R i,j的序列的尾元素中,必有一個最小值 ,記爲用 mi,j 。則
觀察一 : 對任何 i,mi,1 <= mi,2 <= ... <= mi,j 。
因此,如果我們想找以 ai+1 結尾的最長遞增子序列,則只要找到 k ,使得 mi,k < ai+1 <= mi,k+1 ,並且該最長遞增子序列的長度爲 k+1 。對於這個搜索過程,利用上述觀察一,可以使用二分法搜索 (binary search)。同時,我們注意到
觀察二 : mi+1,k+1 = ai+1 , 且對於所有 t 不等於 k+1 , mi+1,t = mi,t 。
同時,注意到計算 mi+1,* 只需要用到 mi,* ,因此,我們可以用 K[j] 來表示 mi,j 在 Ai 中的下標。當計算在 Ai+1 上進行時,我們只需在 K 中找到一個元素 k ,使得 mi,k < ai+1 <= mi,k+1 ,然後更新 K[k+1] ,這時,K[j] 就可以表示 mi+1,j 在 Ai+1 中的下標了。
- L := 0
- for i from 1 to n
- find j in K such that A[K[j]] < A[i] <= A[K[j+1]] by binary search; if no such j, then set j := 0
- P[i] := K[j]
- if j == L or A[i] < A[K[j+1]] then
- K[j+1] := i
- L := max( L, j+1 )
- end
- end
- return L
其中,P[k] 記錄了以 ak 結尾的最長遞增子序列的前一個元素在 An 中的下標。通過數組 P,我們就可以求出最長遞增子序列,爲
..., A[P[P[L]]], A[P[L]], A[L]。從上述僞代碼可以看出,總的時間複雜度爲 O(n logL )。
注:這是一道經典的題目。本人解這道題時,想到了第一種算法,在腦子裏寫出了大概的遞歸方程。然而,當真正在紙上寫下來並建立一個二維的狀態表時,才意識到問題沒那簡單。一開始,我的遞歸方程是 OPT(i,x) = max { OPT(i-1,x), OPT(i-1, min(x, ai ))+1 } 。這是不對的,但似乎不太容易發現,直到建立狀態時,我才發現。而且,這個算法對於有重複元素的序列也存在問題,需要進一步修正。而第二個算法則是比較普遍的動態規劃解,該算法優美,且對任何序列都不會有問題。最後一個算法則是參考了Algorithmist 上的一篇文章 。同時,該文章也給出 C/C++ 實現。以前解類似的動態規劃問題時,總只是在腦子裏把遞歸方程過一遍就覺得OK了,看來,以後要儘量把方程精確寫出來並畫畫狀態表。同時,也可以進一步思考更優美的動太規劃算法,例如第二個算法。前兩個算法都是典型的動態規劃思路,但顯然,第二個算法的狀態要少,也更好。自勉之。
轉自:http://www.wutianqi.com/?p=1850
引出:
問題描述:給出一個序列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.
分析:
這題目是經典的DP題目,也可叫作LIS(Longest Increasing Subsequence)最長上升子序列 或者 最長不下降子序列。很基礎的題目,有兩種算法,複雜度分別爲O(n*logn)和O(n^2) 。
算法1:
時間複雜度:O(n^2):
我們依次遍歷整個序列,每一次求出從第一個數到當前這個數的最長上升子序列,直至遍歷到最後一個數字爲止,然後再取dp數組裏最大的那個即爲整個序列的最長上升子序列。我們用dp[i]來存放序列1-i的最長上升子序列的長度,那麼dp[i]=max(dp[j])+1,(j∈[1, i-1]); 顯然dp[1]=1,我們從i=2開始遍歷後面的元素即可。
// Author: Tanky Woo
// Blog: www.WuTianQi.com
int dp[1000];
int LIS(int arr[1000], int n)
{
for(int i=1; i<=n; ++i)
dp[i] = 0;
int ans;
dp[1] = 1;
for(int i=2; i<=n; ++i)
{
ans = dp[i];
for(int j=1; j<i; ++j)
{
if(arr[i]>arr[j] && dp[j]>ans)
ans = dp[j];
}
dp[i] = ans+1;
}
ans = 0;
for(int i=1; i<=n; ++i)
{
if(dp[i] > ans)
ans = dp[i];
}
return ans;
}
算法2:
時間複雜度:(NlogN):
除了算法一的定義之外,增加一個數組b,b[i]用以表示長度爲i最長子序列的最後一個數最小可以是多少。易證:i<j時,b[i]<b[j]。
在二分查找時,一直更新b[]內容,設此時b的總長度爲k,
若1. arr[i] >= b[k], 則b[k+1] = arr[i];
若2. arr[i] < b[k], 則在b[1..k]中用二分搜索大於arr[i]的最小值,返回其位置pos,然後更新b[pos]=arr[i]。
// Author: Tanky Woo
// Blog: www.WuTianQi.com
// num爲要查找的數,k是範圍上限
// 二分查找大於num的最小值,並返回其位置
int bSearch(int num, int k)
{
int low=1, high=k;
while(low<=high)
{
int mid=(low+high)/2;
if(num>=b[mid])
low=mid+1;
else
high=mid-1;
}
return low;
}
int LIS()
{
int low = 1, high = n;
int k = 1;
b[1] = p[1];
for(int i=2; i<=n; ++i)
{
if(p[i]>=b[k])
b[++k] = p[i];
else
{
int pos = bSearch(p[i], k);
b[pos] = p[i];
}
}
return k;
}
以下是證明b[]的單調遞增性:
b序列是嚴格遞增的,即b[1] < b[2] < … < b[t]。
證明:
若b[i] >= b[i + 1],b[i + 1] 是長度爲i+1的遞增子序列的尾項的最小值,設此序列爲x[1]..x[i+1],x[1]..x[i]即構成長度爲i的遞增子序列,x[i] < x[i+1] = b[i+1] <= b[i],與b[i]定義不符。
最後,給出兩個有代表性的題目:
1.HDOJ 1257 最少攔截系統
題目傳送門:http://acm.hdu.edu.cn/showproblem.php?pid=1257
解題報告傳送門:http://www.wutianqi.com/?p=1841
此題用O(n^2)解法做即可。
2.HDOJ 1025 Constructing Roads In JGShining’s Kingdom
題目傳送門:http://acm.hdu.edu.cn/showproblem.php?pid=1025
解題報告傳送門:http://www.wutianqi.com/?p=1848
此題數據量較大,所以要用O(NlogN)的解法做。