求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等