最長公共子序列的NlogN解法
http://www.douban.com/note/276277441/
最長公共子序列問題:
給定2個字串,求其最長公共子串。如abcde和dbada的最長公共字串為bd。
動態規劃:dp[i][j]表示A串前i個和B串前j個的最長公共子串的長度。
則
若A[i] == B[j] , dp[i][j] = dp[i-1][j-1] + 1;
否則 dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
時間複雜度O(N*M)。
dp[i][j]僅在A[i]==B[j]處才增加,對於不相等的地方對最終值是沒有影響的。
故列舉相等點處可以對其進行優化。
則對於dp[i][j](這裡只計算A[i]==B[j]的i和j),取最大的dp[p][q],滿足(p<i,q<j),通過二叉搜尋樹可以再logn的時間裡獲取到最大的dp[p][q],區間在[0,j)。
這裡也可將其轉化為最長遞增子序列問題。
舉例說明:
A:abdba
B:dbaaba
則1:先順序掃描A串,取其在B串的所有位置:
2:a(2,3,5) b(1,4) d(0)。
3:用每個字母的反序列替換,則最終的最長嚴格遞增子序列的長度即為解。
替換結果:532 41 0 41 532
最大長度為3.
簡單說明:上面的序列和最長公共子串是等價的。
對於一個滿足最長嚴格遞增子序列的序列,該序列必對應一個匹配的子串。
反序是為了在遞增子串中,每個字母對應的序列最多隻有一個被選出。
反證法可知不存在更大的公共子串,因為如果存在,則求得的最長遞增子序列不是最長的,矛盾。
最長遞增子序列可在O(NLogN)的時間內算出。
dp[i] = max(dp[j]+1) ( 滿足 a[i] > a[j] && i > j )
顯然對於同樣的如dp[k] = 3,假定k有多個,記為看k1,k2,.....,km 設k1 < k2 < .... < km
在計算dp[i]的時候,k2,k3,....,km顯然對結果沒有幫助,取當前最小的k,
滿足ans[k] = p (最小的p使得dp[p]=k) ,每次二分,更新ans[dp[i] = min(ans[dp[i],i).
ps:LCS在最終的時間複雜度上不是嚴格的O(nlogn),不知均攤上是不是。
舉個退化的例子:
如A:aaa
B:aaaa
則序列321032103210
長度變成了n*m ,最終時間複雜度O(n*m*(lognm)) > O(n*m)。
這種情況不知有沒有很好的解決辦法。
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <iostream>
#include <string>
#include <math.h>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
const int maxn = 1501 ;
vector<int> location[26] ;
int c[maxn*maxn] , d[maxn*maxn] ;
inline int get_max(int a,int b) { return a > b ? a : b ; }
//nlogn 求lcs
int lcs(char a[],char b[])
{
int i , j , k , w , ans , l , r , mid ;
for( i = 0 ; i < 26 ; i++) location[i].clear() ;
for( i = strlen(b)-1 ; i >= 0 ; i--) location[b[i]-'a'].push_back(i) ;
for( i = k = 0 ; a[i] ; i++)
{
for( j = 0 ; j < location[w=a[i]-'a'].size() ; j++,k++) c[k] = location[w][j] ;
}
d[1] = c[0] ; d[0] = -1 ;
for( i = ans = 1 ; i < k ; i++)
{
l = 0 ; r = ans ;
while( l <= r )
{
mid = ( l + r ) >> 1 ;
if( d[mid] >= c[i] ) r = mid - 1 ;
else l = mid + 1 ;
}
if( r == ans ) ans++,d[r+1] = c[i] ;
else if( d[r+1] > c[i] ) d[r+1] = c[i] ;
}
return ans ;
}
int main()
{
char a[maxn] , b[maxn] ;
while (~scanf("%s%s",a,b))
{
printf("%d/n",lcs(a,b));
}
}
最長公共子序列 的 nlogn 的演算法本質是 將該問題轉化成 最長增序列(LIS),因為 LIS 可以用nlogn實現,所以求LCS的時間複雜度降低為 nlogn。
1. 轉化:將LCS問題轉化成LIS問題。
假設有兩個序列 s1[ 1~6 ] = { a, b, c , a, d, c }, s2[ 1~7 ] = { c, a, b, e, d, a, b }。
記錄s1中每個元素在s2中出現的位置, 再將位置按降序排列, 則上面的例子可表示為:
loc( a)= { 6, 2 }, loc( b ) = { 7, 3 }, loc( c ) = { 1 }, loc( d ) = { 5 }。
將s1中每個元素的位置按s1中元素的順序排列成一個序列s3 = { 6, 2, 7, 3, 1, 6, 2, 5, 1 }。
在對s3求LIS得到的值即為求LCS的答案。(這點我也只是大致理解,讀者可以自己理解甚至證明。)
2.求LIS的 nlogn 的演算法:
參考上面給出連結中的pdf,由於是英文的,我也只是做一些翻譯,譯得不準請見諒及指正。
覆蓋:是序列s的幾個不相交的降序列,它們包含了s中的所有元素,降序列的個數為c。
最小覆蓋:c值最小的覆蓋。
定理:序列s的最長增序列等於最小覆蓋。
於是:求s的最長增序列轉化成求s的最小覆蓋。
3.求最小覆蓋的 nlogn 的演算法。
上圖來自連結中的pdf,其中 (i, j)表示序列中第 j 個降序列的最後一個元素是 i 。可用以為陣列a實現這個記錄。
初始化,a[ 1 ] = s[ 1 ]。
對序列s中第i個元素進行處理時,都儘量將這個元素加到之前的降序列中最後一個元素最小的那個降序列的後面(類似貪心的思想),可保證求得的是最小覆蓋,由圖可知之前的降序列的最後一個元素是升序排列的,此時可以用二分搜尋最後一個元素最小的且大於等於元素i的降序列,將元素i加到這個序列後面。
當然,若沒有這樣的序列,就再建一個降序列,目前的最後一個元素為元素i。
下面是一道題
程式碼附上:
- #include <iostream>
- #include <stdio.h>
- #include <memory.h>
- usingnamespace std;
- #define LEN 100005
- int a[LEN], b[LEN];
- int loc[LEN], n;
- void calLoc()
- {
- int i;
- for(i = 1; i <= n; i++)
- loc[b[i]] = i;
- }
- int LIS()
- {
- int i, k, l, r, mid;
- a[1] = b[1], k = 1;
- for(i = 2; i <= n; i++)
- {
- if(a[k] < b[i]) a[++k] = b[i];
- else {
- l = 1; r = k;
- while(l <= r)
- {
- mid = ( l + r ) / 2;
- if(a[mid] < b[i])
- l = mid + 1;
- else
- r = mid - 1;
- }
- a[l] = b[i];
- }
- }
- return k;
- }
- int main()
- {
- int i;
- while(scanf("%d", &n) != EOF)
- {
- for(i = 1; i <= n; i++)
- scanf("%d", &a[i]);
- for(i = 1; i <= n; i++)
- scanf("%d", &b[i]);
- calLoc();
- for(i = 1; i <= n; i++)
- b[i] = loc[a[i]];
- printf("%d\n", LIS());
- }
- return 0;
- }