后缀自动机简单小结

版权声明:小蒟蒻的博客转载也请注明出处哦 https://blog.csdn.net/qq_42835823/article/details/85195960

推荐学习:%%DZYO%%%

我就只贴一个模板了……【例题在后面】

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{
	int link,len;
    //link后缀链接所指向的自己的后缀
    //len当前中点等价类中长度最长的哪一个的长度
	int nxt[28];
	//后缀自动机(DAG)所指向的 
}st[N<<1];//后缀自动机兼后缀链接 
int siz,last;
//siz一共的节点数 
//last上一次加入的状态
void sa_init(){//初始化 
	siz=1;last=1;
	st[1].link=0;
	st[1].len=0;
    /*若关于不同的字符串多次建立后缀自动机,就需要执行这些代码:
    for (int i=0; i<MAXLEN*2; ++i)
        st[i].next.clear();*/
}
void sa_extend(int c){
    //令last为对应整个字符串的状态(最初last=0,在每次字符添加操作后我们都会改变last的值) 
	int cur=++siz;int p;
	st[cur].len=st[last].len+1;
    //建立一个新的状态cur,令len(cur)=len(last)+1,而link(cur)的值并不确定
	for(p=last;p&&!st[p].nxt[c];p=st[p].link)
		st[p].nxt[c]=cur;//将之前子串的后缀都加上当前字符( 添加字符c的转移) 
    //我们最初在last,如果它没有字符c的转移,那就添加字符c的转移,指向cur,
    //然后走向其后缀链接,再次检查——如果没有字符c的转移,就添加上去
    //如果在某个节点已有字符c的转移,就停止,并且令p为这个状态的编号
    //如果“某节点已有字符c的转移”这一事件从未发生,而我们来到了空状态-1(经由t_0的后缀指针前来)
    //我们简单地令link(cur)=0,跳出
	if(p==0)st[cur].link=1;
	else{
        //假设我们停在了某一状态q,是从某一个状态p经字符c的转移而来
        //现在有两种情况:len(p)+1=len(q)或不然
		int q=st[p].nxt[c];
        //如果len(p)+1=len(q),那么我们简单地令link(cur)=q,跳出
		if(st[q].len==st[p].len+1)st[cur].link=q;
        //p是last的后缀   如果q只比p多了一个字符(当前字符c)
        //说明q是cur的后缀,于是就连一条边 
		else{
            //否则,情况就变得更加复杂。必须新建一个q的“拷贝”状态:
            //建立一个新的状态clone,将q的数据拷贝给它(后缀链接,以及转移)
            //除了len的值:需要令len(clone)=len(p)+1
			int clone=++siz;
			st[clone].link=st[q].link;
			for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];//memcpy要挂 
			st[clone].len=st[p].len+1;
			for(;p&&st[p].nxt[c]==q;p=st[p].link)
				st[p].nxt[c]=clone;
            //最终,我们需要做的最后一件事情就是——从p开始沿着后缀链接走,
            //对每个状态我们都检查是否有指向q的,字符c的转移,
            //如果有就将其重定向至clone(如果没有,就终止循环)
			st[q].link=st[cur].link=clone;
            //在拷贝之后,我们将cur的后缀链接指向clone,并将q的后缀链接重定向到clone
		}
	}
	last=cur;
    //在任何情况下,无论在何处终止了这次添加操作
    //我们最后都将更新last的值,将其赋值为cur
}
//如果我们还需要知道哪些节点是终止节点而哪些不是,我们可以在构建整个字符串的后缀自动机之后找出所有终止节点。
//对此我们考虑对应整个字符串的节点(显然,就是我们储存在变量last中的节点),
//我们沿着它的后缀链接走,直到到达初始状态,并且将途径的每个节点标记为终止节点。
//很好理解,如此我们标记了字符串s所有后缀的对应状态,也就是我们想要找出的终止状态。
int s[N],lens;
int main(){
	scanf("%s",s+1);
	lens=strlen(s+1);
	sa_init();
	for(int i=1;i<=lens;i++)
		sa_extend(s[i]-'a');
	
	retrun 0;
}
  • 这个模板是从 1 1 开始用点的,因为从 0 0 开始的话, 0 0 l i n k link 会指向 1 -1 ,用数组时会越界。
  • 下面刚开始是从 0 0 开始的,就不要介意板子不一样了,改过来就好。


下面讲讲例题。

后缀自动机模板

洛谷地址
在这里插入图片描述
在这里插入图片描述

先建立后缀自动机,将所有不是克隆的点都染色,然后通过 l i n k link 树,求一下子树大小就是出现了多少次。然后再用当前节点的 l e n len (最大长度)*出现次数,取个 m a x max 就好了。

我是用 + D F S 建边+DFS 来统计的。

// luogu-judger-enable-o2
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
struct node{
    int link,len;
    bool cl;
    //link后缀链接所指向的自己的后缀
    //len当前中点等价类中长度最长的哪一个的长度
    int nxt[30];
    //后缀自动机(DAG)所指向的 
}st[N<<1];//后缀自动机兼后缀链接 
int siz,last;
//siz一共的节点数 
//last上一次加入的状态
void sa_init(){//初始化 
    siz=1;last=0;
    st[0].len=0;
    st[0].link=-1;
    /*若关于不同的字符串多次建立后缀自动机,就需要执行这些代码:
    for (int i=0; i<MAXLEN*2; ++i)
        st[i].next.clear();*/
} 
void sa_extend(int c){
    //令last为对应整个字符串的状态(最初last=0,在每次字符添加操作后我们都会改变last的值) 
    int cur=siz++;int p;
    st[cur].len=st[last].len+1;
    //建立一个新的状态cur,令len(cur)=len(last)+1,而link(cur)的值并不确定
    for(p=last;p!=-1&&!st[p].nxt[c];p=st[p].link)
        st[p].nxt[c]=cur;//将之前子串的后缀都加上当前字符( 添加字符c的转移) 
    //我们最初在last,如果它没有字符c的转移,那就添加字符c的转移,指向cur,
    //然后走向其后缀链接,再次检查——如果没有字符c的转移,就添加上去
    //如果在某个节点已有字符c的转移,就停止,并且令p为这个状态的编号
    //如果“某节点已有字符c的转移”这一事件从未发生,而我们来到了空状态-1(经由t_0的后缀指针前来)
    //我们简单地令link(cur)=0,跳出
    if(p==-1)st[cur].link=0;
    else{
        //假设我们停在了某一状态q,是从某一个状态p经字符c的转移而来
        //现在有两种情况:len(p)+1=len(q)或不然
        int q=st[p].nxt[c];
        //如果len(p)+1=len(q),那么我们简单地令link(cur)=q,跳出
        if(st[q].len==st[p].len+1)st[cur].link=q;
        //p是last的后缀   如果q只比p多了一个字符(当前字符c)
        //说明q是cur的后缀,于是就连一条边 
        else{
            //否则,情况就变得更加复杂。必须新建一个q的“拷贝”状态:
            //建立一个新的状态clone,将q的数据拷贝给它(后缀链接,以及转移)
            //除了len的值:需要令len(clone)=len(p)+1
            int clone=siz++;
            st[clone].cl=true;
            st[clone].link=st[q].link;
            for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];//memcpy要挂 
            st[clone].len=st[p].len+1;
            for(;p!=-1&&st[p].nxt[c]==q;p=st[p].link)
                st[p].nxt[c]=clone;
            //最终,我们需要做的最后一件事情就是——从p开始沿着后缀链接走,
            //对每个状态我们都检查是否有指向q的,字符c的转移,
            //如果有就将其重定向至clone(如果没有,就终止循环)
            st[q].link=st[cur].link=clone;
            //在拷贝之后,我们将cur的后缀链接指向clone,并将q的后缀链接重定向到clone
        }
    }
    last=cur;
    //在任何情况下,无论在何处终止了这次添加操作
    //我们最后都将更新last的值,将其赋值为cur
}
//如果我们还需要知道哪些节点是终止节点而哪些不是,我们可以在构建整个字符串的后缀自动机之后找出所有终止节点。
//对此我们考虑对应整个字符串的节点(显然,就是我们储存在变量last中的节点),
//我们沿着它的后缀链接走,直到到达初始状态,并且将途径的每个节点标记为终止节点。
//很好理解,如此我们标记了字符串s所有后缀的对应状态,也就是我们想要找出的终止状态。
char s[N];
long long ans;
struct edge{
    int v,nxt;
}e[N<<1];
int first[N<<1],cnt=0;
void add(int u,int v){
    e[++cnt].v=v;
    e[cnt].nxt=first[u];first[u]=cnt;
}
int f[N<<1];
void dfs(int x){
    for(int i=first[x];i;i=e[i].nxt){
        dfs(e[i].v);
        f[x]+=f[e[i].v];
    }
    if(!st[x].cl)f[x]++;
    if(f[x]>1)ans=max(ans,(long long)f[x]*st[x].len);
}
int main(){
    scanf("%s",s+1);
    sa_init();
    int le=strlen(s+1);//太慢,记录一下 
    for(int i=1;i<=le;i++)
        sa_extend((int)(s[i]-'a'));
    for(int i=1;i<siz;i++)
    	add(st[i].link,i);
    dfs(0);
    printf("%lld",ans);
    return 0;
}

[TJOI2015]弦论

洛谷地址
在这里插入图片描述
在这里插入图片描述

还是先建完后缀自动机,如果 t = = 0 t==0 ,就把每个点的权值赋值为 1 1 。如果 t = = 1 t==1 就向上一题那样记录出现了几次。不过,这次用的是桶排
用后缀自动机的 D A G DAG 我们可以记录每一个节点有多少可以到达的子串,然后从后缀自动机的 D A G DAG 的初始节点向下跑,每次判断一下 k k ,就 O K OK 了。

#include<bits/stdc++.h>
using namespace std;
const int N=5e5+5;
struct node{
    int link,len,nxt[28]; 
}st[N<<1];
int siz,last;
void sa_init(){
    siz=1;last=1;
    st[1].link=0;
    st[1].len=0;
}
int t,k,app[N<<1],sum[N<<1];//出现次数
void sa_extend(int c){
    int cur=++siz;int p;
    app[cur]=1;//
    st[cur].len=st[last].len+1;
    for(p=last;p&&!st[p].nxt[c];p=st[p].link)//
        st[p].nxt[c]=cur;
    if(p==0)st[cur].link=1;
    else{
        int q=st[p].nxt[c];
        if(st[q].len==st[p].len+1)st[cur].link=q;
        else{
            int clone=++siz;
            st[clone].link=st[q].link;
            for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
            st[clone].len=st[p].len+1;
            for(;p&&st[p].nxt[c]==q;p=st[p].link)//
                st[p].nxt[c]=clone;
            st[q].link=st[cur].link=clone;
        }
    }
    last=cur;
}
char s[N];
int c[N<<1],rk[N<<1];
void work(){
    for(int i=1;i<=siz;i++)c[st[i].len]++;
    for(int i=1;i<=siz;i++)c[i]+=c[i-1];
    for(int i=1;i<=siz;i++)rk[c[st[i].len]--]=i;//桶排
    
    for(int i=siz;i>0;i--){
        if(t)app[st[rk[i]].link]+=app[rk[i]];//st[0].link=-1越界 
        else app[rk[i]]=1;
    }
    
    app[1]=0;
    for(int i=siz;i>0;i--){
        sum[rk[i]]=app[rk[i]];
        for(int j=0;j<26;j++)
            if(st[rk[i]].nxt[j])
                sum[rk[i]]+=sum[st[rk[i]].nxt[j]];
//		cout<<i<<" "<<sum[i]<<endl;
    }
}
void solve(){
    if(k>sum[1]){
        printf("-1");
        return;
    }
    int now=1;
    k-=app[1];sum[1]=0;
    while(k>0){
        int p=0;
        while(k>sum[st[now].nxt[p]]){
            k-=sum[st[now].nxt[p]];
            p++;
        }
        now=st[now].nxt[p];
        printf("%c",'a'+p);
        k-=app[now];
    }
}
int main(){
    scanf("%s",s+1);
    int lens=strlen(s+1);
    sa_init();
    for(int i=1;i<=lens;i++)
        sa_extend((int)(s[i]-'a'));
    scanf("%d%d",&t,&k);
    work();
    solve();
    return 0;
}

[AHOI2013]差异

洛谷地址
在这里插入图片描述
在这里插入图片描述

先说一句:为什么大家都是正向建的图啊?不是反向建,求两两前缀的最长公共后缀吗?
[反正我是倒着建的,而且过了]

题目说是求两两后缀的最长公共前缀,不久是后缀数组么。但后缀数组太难了,打不来,就只能用后缀自动机做了。
反向建后缀自动机,求成了两两前缀的最长公共后缀。
然而
在这里插入图片描述
是可以转化为一个定值的。
在这里插入图片描述
那么如何求两两前缀的最长公共后缀呢?

提示一下:

  • l i n k link 树上两个点的 l c a lca 就是这两个子串的最长公共后缀。
    [证明显而易见,每个点的 f a t h e r father 都是自己的后缀]

对于一个点,我们把它当做 l c a lca ,算出他的贡献就好了。
把它的子树两两的 s i z e size 乘一下再乘上自己就是贡献了,注意别重复。

当然不必这么麻烦,思想还是一样,代码可以优化一下,详见代码哦。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
inline int read(){
    int x=0,f=1;char c=getchar();
    while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return f==1?x:-x;
}
const int N=7e5+5;
struct node{
    int link,len,nxt[30],f;
}st[N<<1];
int siz,last;
void sa_init(){
    siz=last=1;
    st[1].len=0;
    st[1].link=0;
}
void sa_extend(int c){
    int cur=++siz;int p;
    st[cur].len=st[last].len+1;
    st[cur].f=1;
    for(p=last;p&&!st[p].nxt[c];p=st[p].link)
        st[p].nxt[c]=cur;
    if(p==0)st[cur].link=1;
    else{
        int q=st[p].nxt[c];
        if(st[q].len==st[p].len+1)st[cur].link=q;
        else{
            int clone=++siz;
            st[clone].link=st[q].link;
            for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
            st[clone].len=st[p].len+1;
            for(;p&&st[p].nxt[c]==q;p=st[p].link)
                st[p].nxt[c]=clone;
            st[q].link=st[cur].link=clone;
        }
    }
    last=cur;
}
int lens;
int c[N<<1],rk[N<<1];
char s[N];
ll count(){
    for(int i=1;i<=siz;i++)c[st[i].len]++;
    for(int i=1;i<=lens;i++)c[i]+=c[i-1];//
    for(int i=1;i<=siz;i++)rk[c[st[i].len]--]=i;//排在第几位的是i 
    ll ans=0;
    for(int i=siz;i>0;i--){
        int x=rk[i];
        ans+=(ll)st[x].f*st[st[x].link].f*st[st[x].link].len;
        //现在父亲存储的大小还没有算自己,也就是自己兄弟的大小和 
        st[st[x].link].f+=st[x].f;
    }
    return ans;
}
int main(){
    scanf("%s",s+1);
    lens=strlen(s+1);
    sa_init();
    for(int i=lens;i>0;i--)
        sa_extend(s[i]-'a');
    printf("%lld",(ll)(lens-1)*lens*(lens+1)/2-2*count());
    return 0;
}

[APIO2014]回文串

洛谷地址
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

说实话,这道题做完了,还是好懵B。

还是一样的,先建后缀自动机,再算一算每一个子串出现的次数。然后再把反串放在后缀自动机跳跳就好了。操作就看代码吧。[有注释的]

推荐:题解
虽然图片挂了,就将就吧……

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
#define ll long long
struct node{
	int link,len,nxt[28],maxend;
}st[N<<1];;
int cnt,last;
void sa_init(){
	cnt=last=1;
	st[1].link=st[1].len=0;
}
int f[N<<1];
void sa_extend(int c,int pos){
	int cur=++cnt;int p;
	st[cur].len=st[last].len+1;
	st[cur].maxend=pos;
	f[cur]=1;
	for(p=last;p&&!st[p].nxt[c];p=st[p].link)//
		st[p].nxt[c]=cur;
	if(!p)st[cur].link=1;
	else{
		int q=st[p].nxt[c];
		if(st[q].len==st[p].len+1)st[cur].link=q;
		else{
			int clone=++cnt;
			st[clone].link=st[q].link;
			st[clone].maxend=st[q].maxend;
			for(int i=0;i<26;i++)st[clone].nxt[i]=st[q].nxt[i];
			st[clone].len=st[p].len+1;
			for(;p&&st[p].nxt[c]==q;p=st[p].link)
				st[p].nxt[c]=clone;
			st[q].link=st[cur].link=clone;
		}
	}
	last=cur;
}
char s[N];
int c[N],lens,rk[N<<1],vis[N<<1];//
ll ans=0;
void work(){//反串在后缀自动机上跑 
	int now=1,l=1;
	for(int i=lens;i>0;i--){
		while(now>1&&!st[now].nxt[s[i]-'a']){//往上跳,找到一个有这个字符转移的[找回文边缘] 
			now=st[now].link;
			l=st[now].len;
		}
		if(st[now].nxt[s[i]-'a']){
			now=st[now].nxt[s[i]-'a'];//转移
			l++;
		}
		if(st[now].maxend<i+l){//这个串在maxendpos右边 
			//如果是左边就没有贡献 ——————————————————— 
			//其他的endpos都没有maxendpos优 ——————————————————
			if(i<=st[now].maxend)//这个串与maxendpos有交集 
				ans=max(ans,(ll)(st[now].maxend-i+1)*f[now]);
			for(int p=st[now].link;p&&!vis[p];p=st[p].link){
				vis[p]=1;//打标记,节省时间 
				if(i<=st[p].maxend&&st[p].maxend<=i+st[p].len-1)//有交集 
					ans=max(ans,(ll)(st[p].maxend-i+1)*f[p]);
			}
		}
	}
}
int main(){
	scanf("%s",s+1);
	lens=strlen(s+1);
	sa_init();
	for(int i=1;i<=lens;i++)
		sa_extend(s[i]-'a',i);
	for(int i=1;i<=cnt;i++)c[st[i].len]++;
	for(int i=1;i<=lens;i++)c[i]+=c[i-1];
	for(int i=1;i<=cnt;i++)rk[c[st[i].len]--]=i;
	for(int i=cnt;i>0;i--){
		int x=rk[i];
		st[st[x].link].maxend=max(st[st[x].link].maxend,st[x].maxend);
		f[st[x].link]+=f[x];
	}
	work();
	printf("%lld",ans);
	return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_42835823/article/details/85195960