最长公共子序列的NlogN解法

来源:互联网 发布:杰克.格里森 知乎 编辑:程序博客网 时间:2024/05/17 01:21

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。

下面是一道题

题目链接:http://icpc.njust.edu.cn/Contest/Show/41

代码附上

[cpp] view plaincopy
  1. #include <iostream>  
  2. #include <stdio.h>  
  3. #include <memory.h>  
  4. using namespace std;  
  5.   
  6. #define LEN 100005  
  7.   
  8. int a[LEN], b[LEN];  
  9. int loc[LEN], n;  
  10.   
  11. void calLoc()  
  12. {  
  13.     int i;  
  14.       
  15.     for(i = 1; i <= n; i++)  
  16.         loc[b[i]] = i;  
  17. }  
  18.   
  19. int LIS()  
  20. {  
  21.     int i, k, l, r, mid;   
  22.       
  23.     a[1] = b[1], k = 1;  
  24.     for(i = 2; i <= n; i++)  
  25.     {  
  26.         if(a[k] < b[i]) a[++k] = b[i];  
  27.         else {  
  28.             l = 1; r = k;  
  29.             while(l <= r)  
  30.             {  
  31.                 mid = ( l + r ) / 2;  
  32.                 if(a[mid] < b[i])   
  33.                     l = mid + 1;  
  34.                 else    
  35.                     r = mid - 1;  
  36.                   
  37.             }  
  38.             a[l] = b[i];  
  39.         }  
  40.     }  
  41.     return k;  
  42. }  
  43.   
  44. int main()  
  45. {  
  46.     int i;  
  47.       
  48.     while(scanf("%d", &n) != EOF)  
  49.     {  
  50.         for(i = 1; i <= n; i++)  
  51.             scanf("%d", &a[i]);  
  52.         for(i = 1; i <= n; i++)  
  53.             scanf("%d", &b[i]);  
  54.         calLoc();  
  55.         for(i = 1; i <= n; i++)  
  56.             b[i] = loc[a[i]];  
  57.                 printf("%d\n", LIS());  
  58.     }  
  59.     return 0;  
  60. }  
0 0
原创粉丝点击