这里主要介绍两种求多个字符串最长公共子串的方法。
这里我们以
[POI2000]公共串
为例题来讲一下这两种不同的方法的区别。
前置知识:
后缀数组
、
后缀自动机
首先先讲一下SA的求法:
我们可以很容易的想明白,排名越靠近的两个字符串,他们的最长公共前缀,也就是LCP会越大,然后我们考虑将这些字符串连接起来,中间用不同的特殊字符分隔开,去求这个大的字符串的后缀数组。
例如:我们要去求下面三个字符串的最长公共子串
1.
abca
2.
abc
3.
bcde
那么通过我们操作后形成的大字符串就是
abca1abc2bcde3
然后我们求出这个大字符串的后缀数组,然后我们来思考一下,如何去计算答案。
很显然,因为经过排序后越相似的子串距离越近,那么我们就可以用类似滑动窗口的那种感觉去选择一段区间,保证选择的这段区间要满足至少包含每一个字符串的一个子串,这样我们求这一段区间的LCP,就代表公共子串的长度,我们求出所有的这些区间,然后将这些长度取
m
a
x
max
ma
x
,就可以轻松的得到答案了。
时间复杂度
O
(
字符串总长度
)
O(字符串总长度)
O
(
字符串总长度
)
下面的代码里会有细节的解释
#include<bits/stdc++.h>
#define endl "\n"
#define x first
#define y second
#define Endl endl
typedef long long ll;
const int INF=0x7fffffff;
const double PI=3.14159265359;
using namespace std;
const int N=1e6+10;
string str;
int L[N],R[N],col[N],cnt[N];
int sum=0;
struct SAIS{//这是个SAIS的板子,O(n)求SA
int sa[N],rk[N],s[N<<1],op[N<<1],pos[N<<1],c1[N],c[N];
int ht[N];
#define re register
#define L(x) sa[c[s[x]]--]=x
#define R(x) sa[c[s[x]]++]=x
inline void sa_sort(int *S,int n,int m,int *s,int *op,int tn)
{
for(re int i=1;i<=n;i++) sa[i]=0;
for(re int i=1;i<=m;i++) c1[i]=0;
for(re int i=1;i<=n;i++) c1[s[i]]++;
for(re int i=2;i<=m;i++) c1[i]+=c1[i-1];
for(re int i=1;i<=m;i++) c[i]=c1[i];
for(re int i=tn;i;i--) L(S[i]);
for(re int i=1;i<=m+1;i++) c[i]=c1[i-1]+1;
for(re int i=1;i<=n;i++)
if(sa[i]>1 && op[sa[i]-1]) R(sa[i]-1);
for(re int i=1;i<=m;i++) c[i]=c1[i];
for(re int i=n;i;i--)
if(sa[i]>1 && !op[sa[i]-1]) L(sa[i]-1);
}
void SA_IS(int n,int m,int *s,int *op,int *pos)//m代表字符的范围
{
int tot=0,cnt=0;int *S=s+n;
op[n]=0;
for(re int i=n-1;i;i--) op[i]=(s[i]!=s[i+1])?s[i]>s[i+1]:op[i+1];
rk[1]=0;
for(re int i=2;i<=n;i++)
if(op[i-1]==1 && op[i]==0) pos[++tot]=i,rk[i]=tot;
else rk[i]=0;
sa_sort(pos,n,m,s,op,tot);
int u=0,p=0;
for(re int i=1;i<=n;i++)
if(rk[sa[i]])
{
u=rk[sa[i]];
if(cnt<=1 || pos[u+1]-pos[u]!=pos[p+1]-pos[p]) ++cnt;
else
{
for(re int j=0;j<=pos[u+1]-pos[u];j++)
if(s[pos[u]+j]!=s[pos[p]+j] || op[pos[u]+j]!=op[pos[p]+j]){++cnt;break;}
}
S[u]=cnt;
p=u;
}
if(tot!=cnt) SA_IS(tot,cnt,S,op+n,pos+n);
else for(re int i=1;i<=tot;i++) sa[S[i]]=i;
for(re int i=1;i<=tot;i++) S[i]=pos[sa[i]];
sa_sort(S,n,m,s,op,tot);
}
void get_ht(int n)
{
for(re int i=1;i<=n;i++) rk[sa[i]=sa[i+1]]=i;
for(re int i=1,p=0;i<=n;ht[rk[i]]=p,i++)
if(rk[i]!=1) for(p=p-!!p;sa[rk[i]-1]+p<=n && i+p<=n && s[i+p]==s[sa[rk[i]-1]+p];p++);
}
void Get_SA(int n){
for(int i=1;i<=n;i++) s[i]=str[i];
s[++n]=1;
SA_IS(n--,122,s,op,pos);//122为字符串的ASCII码范围
get_ht(n);
}
}sa;
void add(int x){//类似滑动窗口的感觉,或者说是莫队?
if(!col[x]) return;
cnt[col[x]]++;
if(cnt[col[x]]==1) sum++;
}
void del(int x){
if(!col[x]) return;
cnt[col[x]]--;
if(cnt[col[x]]==0) sum--;
}
void solve(){
int ans=0;
int n;
cin>>n;
for(int i=1;i<=n;i++){//L[i]和R[i]表示这个字符串所在的范围
L[i]=R[i-1]+1;
string temp;
cin>>temp;
str+=temp;
str+=i+'0';//通过不同的分割符,将这些字符串分隔开
R[i]=L[i]+temp.size();
}
int len=str.size();
str=" "+str;
sa.Get_SA(len);
for(int i=1;i<=n;i++){
for(int j=L[i];j<R[i];j++) col[sa.rk[j]]=i;//将他们的排名染色,便于之后统计区间是否合法
}
deque<int>q;//单调队列 求区间最值
int l=1;
add(1);
for(int r=2;r<=len;r++){
while(q.size()&&sa.ht[q.back()]>=sa.ht[r]) q.pop_back();
q.push_back(r);
add(r);
if(sum==n){
while(sum==n&&l<r) del(l),l++;
l--,add(l);
}
while(q.size()&&q.front()<=l) q.pop_front();//这里取等于的原因是求LCP的时候 是从l+1到r的ht取min得到的
if(sum==n) ans=max(ans,sa.ht[q.front()]);
}
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
// cin>>T;
T=1;
while(T--){
solve();
}
}
接下来讲一下SAM的做法
如果使用SAM去做这道题的话,我们只需要用其中的一个串建立SAM,然后用其他的串在它上面跑匹配就可以了,匹配时我们可以设置两个变量
p
p
p
(当前状态),
l
e
n
len
l
e
n
(当前匹配长度)。
很明显的一点是,如果当前我们可以匹配上的话,我们就让
l
e
n
+
+
len++
l
e
n
+
+
,否则我们就让
p
p
p
跳到
f
a
[
p
]
fa[p]
f
a
[
p
]
,并且让
l
e
n
len
l
e
n
更新为跳
f
a
fa
f
a
后的l
e
n
[
p
]
en[p]
e
n
[
p
]
(这里可以直接更新
l
e
n
len
l
e
n
的原因是因为我们已经匹配到了
p
p
p
,所以当前状态的所有后缀我们也都匹配到了,所以就可以直接更新了)。
另外我们需要记录一下每一个点被匹配到时最长长度是多少,等全部匹配完之后我们要去更新答案,答案长度是所以字符串匹配到此节点时长度的最小值,等所有的字符串都匹配完后,我们就可以计算答案了。
时间复杂度
O
(
字符串总长度
)
O(字符串总长度)
O
(
字符串总长度
)
下面的代码会给出细节的解释
#include<bits/stdc++.h>
#define endl "\n"
#define x first
#define y second
#define Endl endl
typedef long long ll;
const int INF=0x7fffffff;
const double PI=3.14159265359;
using namespace std;
const int N=1e6+10;
string s[N];
int anslen[N*2],tlen[N*2],col[N*2],cnt[N*2];
int ans=0,n;
struct SAM
{
int idx,last,len[N*2],link[N*2],nex[N*2][26];
int c[N*2],a[N*2];
int sz[N*2];
int v[N*2];
SAM()
{
idx=0;last=0;
len[0]=0;link[0]=-1;
}
void insert(int c)
{
int cur=++idx;
anslen[cur]=len[cur]=len[last]+1;
int p=last;
while(p!=-1 && !nex[p][c]) nex[p][c]=cur,p=link[p];
if(p==-1) link[cur]=0;
else
{
int q=nex[p][c];
if(len[p]+1==len[q]) link[cur]=q;
else
{
int clone=++idx;
anslen[clone]=len[clone]=len[p]+1;
link[clone]=link[q];
for(int i=0;i<26;i++) nex[clone][i]=nex[q][i];
while(p!=-1 && nex[p][c]==q) nex[p][c]=clone,p=link[p];
link[q]=link[cur]=clone;
}
}
last=cur;
}
void work(int id,int x){
int top=0;
int p=0,sum=0;//p代表现在的状态 sum代表现在的匹配长度
for(int i=0;i<s[id].size();i++){
while(p!=-1&&nex[p][s[id][i]-'a']==0){
p=link[p];
if(p==-1) break;
sum=len[p];
}
if(p==-1){
p=0;
sum=0;
}
else{
sum++;
p=nex[p][s[id][i]-'a'];
}
int k=p;
do{
v[++top]=k;
tlen[k]=max(sum,tlen[k]);//记录这个节点被匹配到时的最长长度
col[k]=x;
if(cnt[k]==x-1) cnt[k]=x;//如果该字符串在其它字符串中均出现,让它的出现次数++
k=link[k];
}while(k!=0&&col[k]!=x);
}
for(int j=top;j>=1;j--){
int temp=v[j];//答案长度取min
anslen[temp]=min(anslen[temp],tlen[temp]);
}
for(int j=top;j>=1;j--){
int temp=v[j];
tlen[temp]=0;
}
}
void getans(){
for(int i=1;i<=idx;i++){
if(cnt[i]==n){
ans=max(ans,anslen[i]);
}
}
}
}sam;
void solve(){
cin>>n;
int minnid=0,minnlen=INF;
for(int i=1;i<=n;i++){//将长度最小的字符串建立SAM
cin>>s[i];
int len=s[i].size();
if(minnlen>len){
minnlen=len,minnid=i;
}
}
for(int i=0;i<minnlen;i++){
sam.insert(s[minnid][i]-'a');
}
for(int i=1;i<=n;i++){//将所有字符串在SAM上匹配一遍
sam.work(i,i);
}
sam.getans();
cout<<ans<<endl;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int T;
// cin>>T;
T=1;
while(T--){
solve();
}
}
下面说一下两种方法的区别:
首先SA的优点就是方便好写并且容易理解
SAM的优点就是可以求得很大数量级的字符串的公共子串。