最长公共子串问题的后缀数组解法

求N个串的最长公共子串,可以转化为求一些后缀的最长公共前缀的最大值,这些后缀应分属于N个串。具体方法如下:

设N个串分别为S1,S2,S3,...,SN,首先建立一个串S,把这N个串用不同的分隔符连接起来。S=S1[P1]S2[P2]S3...SN-1[PN-1]SN,P1,P2,...PN-1应为不同的N-1个不在字符集中的字符,作为分隔符(后面会解释为什么)。

接下来,求出字符串S的后缀数组和Height数组,可以用倍增算法,或DC3算法

二分枚举答案A,假设N个串可以有长度为A的公共字串,并对A的可行性进行验证。如果验证A可行,A'(A'<A)也一定可行,尝试增大A,反之尝试缩小A。最终可以取得A的最大可行值,就是这N个串的最长公共子串的长度。可以证明,尝试次数是O(logL)的。

于是问题就集中到了,如何验证给定的长度A是否为可行解。方法是,找出在Height数组中找出连续的一段Height[i..j],使得i<=k<=j均满足Height[k]>=A,并且i-1<=k<=j中,SA[k]分属于原有N个串S1..SN。如果能找到这样的一段,那么A就是可行解,否则A不是可行解。

具体查找i..j时,可以先从前到后枚举i的位置,如果发现Height[i]>=A,则开始从i向后枚举j的位置,直到找到了Height[j+1]<A,判断[i..j]这个区间内SA是否分属于S1..SN。如果满足,则A为可行解,然后直接返回,否则令i=j+1继续向后枚举。S中每个字符被访问了O(1)次,S的长度为NL+N-1,所以验证的时间复杂度为O(NL)。

到这里,我们就可以理解为什么分隔符P1..PN-1必须是不同的N-1个不在字符集中的字符了,因为这样才能保证S的后缀的公共前缀不会跨出一个原有串的范围。

例题POJ 3050:

//      whn6325689
//		Mr.Phoebe
//		http://blog.csdn.net/u013007900
#include <algorithm>
#include <iostream>
#include <iomanip>
#include <cstring>
#include <climits>
#include <complex>
#include <fstream>
#include <cassert>
#include <cstdio>
#include <bitset>
#include <vector>
#include <deque>
#include <queue>
#include <stack>
#include <ctime>
#include <set>
#include <map>
#include <cmath>
#include <functional>
#include <numeric>
#pragma comment(linker, "/STACK:1024000000,1024000000")


using namespace std;

typedef long long ll;
typedef long double ld;
typedef pair<ll, ll> pll;
typedef complex<ld> point;
typedef pair<int, int> pii;
typedef pair<pii, int> piii;
typedef vector<int> vi;

#define CLR(x,y) memset(x,y,sizeof(x))
#define mp(x,y) make_pair(x,y)
#define pb(x) push_back(x)
#define lowbit(x) (x&(-x))
#define MID(x,y) (x+((y-x)>>1))
#define eps 1e-9
#define PI acos(-1.0)
#define INF 0x3f3f3f3f
#define LLINF 1LL<<62

template<class T>
inline bool read(T &n)
{
    T x = 0, tmp = 1;
    char c = getchar();
    while((c < '0' || c > '9') && c != '-' && c != EOF) c = getchar();
    if(c == EOF) return false;
    if(c == '-') c = getchar(), tmp = -1;
    while(c >= '0' && c <= '9') x *= 10, x += (c - '0'),c = getchar();
    n = x*tmp;
    return true;
}
template <class T>
inline void write(T n)
{
    if(n < 0)
    {
        putchar('-');
        n = -n;
    }
    int len = 0,data[20];
    while(n)
    {
        data[len++] = n%10;
        n /= 10;
    }
    if(!len) data[len++] = 0;
    while(len--) putchar(data[len]+48);
}
//-----------------------------------

const int MAXN = 200001;

int  num[MAXN];
int sa[MAXN], rank[MAXN], height[MAXN];
int wa[MAXN], wb[MAXN], wv[MAXN], wd[MAXN];

int t1[MAXN],t2[MAXN],c[MAXN];

bool cmp(int *r,int a,int b,int l)
{
    return r[a]==r[b] && r[a+l]==r[b+l];
}

void da(int str[],int sa[],int rank[],int height[],int n,int m)
{
    n++;
    int i,j,p,*x=t1,*y=t2;
    for(i=0; i<m; i++)c[i]=0;
    for(i=0; i<n; i++)c[x[i]=str[i]]++;
    for(i=1; i<m; i++)c[i]+=c[i-1];
    for(i=n-1; i>=0; i--)sa[--c[x[i]]]=i;
    for(int j=1; j<=n; j<<=1)
    {
        p=0;
        for(i=n-j; i<n; i++)y[p++]=i;
        for(i=0; i<n; i++)if(sa[i]>=j)y[p++]=sa[i]-j;
        for(i=0; i<m; i++)c[i]=0;
        for(i=0; i<n; i++)c[x[y[i]]]++;
        for(i=1; i<m; i++)c[i]+=c[i-1];
        for(i=n-1; i>=0; i--)sa[--c[x[y[i]]]]=y[i];

        swap(x,y);
        p=1;
        x[sa[0]]=0;
        for(i=1; i<n; i++)
            x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;
        if(p>=n)break;
        m=p;
    }
    int k=0;
    n--;
    for(i=0; i<=n; i++)rank[sa[i]]=i;
    for(i=0; i<n; i++)
    {
        if(k)k--;
        j=sa[rank[i]-1];
        while(str[i+k]==str[j+k])k++;
        height[rank[i]]=k;
    }
}

int loc[MAXN],m;
char str[MAXN],res[MAXN];

bool check(int A,int N)
{
    int i,j,k;
    bool ba[MAXN];
    for (i=1;i<=N;i++)
    {
        if (height[i]>=A)
        {
            for (j=i;height[j]>=A && j<=N;j++);
            j--;
            CLR(ba,0);
            for (k=i-1;k<=j;k++)
                ba[loc[sa[k]]]=true;
            for (k=1;ba[k] && k<=m;k++);
            if (k==m+1)
            {
                for(j=0; j<A ;j++)
                {
                    res[j]=num[sa[i]+j]+'a'-1;
                }
                res[A]='\0';
                return true;
            }
            i=j;
        }
    }
    return false;
}

int main()
{
    int n,k,i,j,a,b,sp,ans;
    while(scanf("%d",&m)&&m)
    {
        sp=29;    //分隔符
        n=0;
        ans=0;
        for(i=1; i<=m; i++)
        {
            scanf("%s",str);
            for(j=0; str[j]; j++)
            {
                loc[n]=i;
                num[n++]=str[j]-'a'+1;
            }
            loc[n]=sp;
            num[n++]=sp++;
        }
        num[n]=0;
        da(num,sa,rank,height,n+1,sp);
        int left=0,right=strlen(str),mid;//开始二分
        while(right>=left)
        {
            mid=(right+left)/2;
            if(check(mid,n))          //判断长度为mid的串是否是所有字符串的公共子串
            {
                left=mid+1;
                ans=mid;
            }
            else
            {
                right=mid-1;
            }
        }
        if(ans!=0)
        {
            printf("%s\n",res);
        }
        else
        {
            printf("IDENTITY LOST\n");
        }
    }
    return 0;
}


优化后的check

当lcp(i-1,i)>=mid的时候,则说明前缀满足条件

为什么要vis[sa[i-1]]呢,因为一方面最初i=2开始,因此i=1时未计算,另一方面每次height数组不满足的时候,更新前缀位置时清空vis数组,那么最初的那个也会被跳过

这个是O(N)的算法

bool vis[1004];
bool check(int mid,int len)
{
    int i,j,tot=0;
    tot=0;
    CLR(vis,0);
    for(i=2; i<=len; i++)
    {
        if(height[i]<mid)
        {
            CLR(vis,0);
            tot=0;
        }
        else
        {
            if(!vis[loc[sa[i-1]]])
            {
                vis[loc[sa[i-1]]]=1;
                tot++;
            }
            if(!vis[loc[sa[i]]])
            {
                vis[loc[sa[i]]]=1;
                tot++;
            }
            if(tot==m)
            {
                for(j=0; j<mid; j++)
                {
                    res[j]=num[sa[i]+j]+'a'-1;
                }
                res[mid]='\0';
                return 1;
            }
        }
    }
    return 0;
}


类似的题目还有POJ 3080,POI 2000等


你可能感兴趣的:(最长公共子串问题的后缀数组解法)