最长公共子串(SA和SAM求法)

这里主要介绍两种求多个字符串最长公共子串的方法。
这里我们以[POI2000]公共串为例题来讲一下这两种不同的方法的区别。
前置知识:后缀数组后缀自动机
首先先讲一下SA的求法:
我们可以很容易的想明白,排名越靠近的两个字符串,他们的最长公共前缀,也就是LCP会越大,然后我们考虑将这些字符串连接起来,中间用不同的特殊字符分隔开,去求这个大的字符串的后缀数组。
例如:我们要去求下面三个字符串的最长公共子串
1.abca
2.abc
3.bcde
那么通过我们操作后形成的大字符串就是abca1abc2bcde3
然后我们求出这个大字符串的后缀数组,然后我们来思考一下,如何去计算答案。
很显然,因为经过排序后越相似的子串距离越近,那么我们就可以用类似滑动窗口的那种感觉去选择一段区间,保证选择的这段区间要满足至少包含每一个字符串的一个子串,这样我们求这一段区间的LCP,就代表公共子串的长度,我们求出所有的这些区间,然后将这些长度取 m a x max max,就可以轻松的得到答案了。
时间复杂度 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 len(当前匹配长度)。
很明显的一点是,如果当前我们可以匹配上的话,我们就让 l e n + + len++ len++,否则我们就让 p p p跳到 f a [ p ] fa[p] fa[p],并且让 l e n len len更新为跳 f a fa fa后的l e n [ p ] en[p] en[p](这里可以直接更新 l e n len len的原因是因为我们已经匹配到了 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的优点就是可以求得很大数量级的字符串的公共子串。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值