zoukankan      html  css  js  c++  java
  • 后缀自动机(SAM)

    后缀自动机作为一种OI新兴的字符串处理工具,越来越...

    打住你的论文行为

    SAM的定义

    观前提示:笔者是从2015年国集论文中学习的SAM

    一个串 (S) 的后缀自动机是一个有限状态自动机(DFA)

    它能且只能接受所有 (S) 的后缀,并且拥有最少的状态与转移

    首先我们要插入SAM的串为 (S),长度为 (|S|)(S_{l,r}) 为第 (l) 个字符到第 (r) 个字符形成的字串

    对于一个字串 (t)(right_t)(S) 中所有出现 (t) 的右端点

    例如一个串 ababbab,字串 ab(right) 集合为 ({2,4,7})

    每个状态 (s) 代表了唯一的 (right) 集合

    对于一个状态 (s),设 (len_s) 为其所有代表状态中最长串的长度

    每个状态还有一个 (fa) 指针

    SAM的构造

    我们假设现在已经插入了前 (|S|-1) 个字符,现在要插入第 (|S|) 个字符,设这个字符是 #a(为了与平常的 a 区别,#a 代表一个字符)

    看我暴力

    我们很容易发现不能暴力加边转移,因为对于一个状态 (s),能从 (1) 状态到达 (s) 的串肯定是其后缀,那么我们暴力加转移边 #a,形成了个啥呢

    例如 abab,要插入 c,变成 ababc

    好,暴力,(S_{1,3}) 后面来个状态 c,形成了 abac!Wonderful Answer!

    肯定是不行的,我们此时能插入 #a 的状态肯定代表的是串 (S_{1,|S|-1}) 的后缀,因为这样加转移边 #a 之后,跑出来的才是新串的后缀

    现在介绍 (fa) 指针,从一个状态 (s) 跳到 (fa_s)(fa_s) 代表的是 (s) 的后缀

    也就是说,跳 (fa) 相当于访问 (s) 的一个后缀

    到此,我们发现了一个加边方法,从 (|S|-1) 不断跳 (fa) 然后加边,最后更新 (|S|)(fa)


    但是,我们有时候跳到的状态已经有了一个向 #a 的转移边,此时不要以为直接结束就完事了,我们需要分类讨论

    设此时的状态为 (p),沿着这条已有的转移边能走到的状态为 (q)

    • 如果 (len_q=len_p+1)

    很简单吧,此时 (q)(right) 集合依然没有什么变化,令 (fa=q) 即可

    • 如果 (len_q>len_p+1)

    此时 (q) 代表的串中,长度不超过 (len_p+1) 的串的 (right) 集合会多出来一个值 (|S|)(因为插入字符 #a 嘛,然后 (p) 有恰好有这个转移边),但是长度超过 (len_p+1) 的串的 (right) 集合却没有,一个状态不能同时代表两个不同的 (right) 集合,此时我们需要新建状态

    新建状态就很简单啦,因为只有 (right) 集合不同,所以我们除了 (right) 集合改改,剩下的原样复制

    此时你已经成功的构建了SAM

    例题

    P3804 【模板】后缀自动机 (SAM)

    题目链接

    每个 (s) 状态代表了唯一的 (right) 集合

    这题没有让我们求出子串具体是什么,所以直接对每个状态取最长的串即可

    沿着 (parent) 树连边,之后跑一遍树形dp即可

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #define N 2000001
    #define INF 1100000000
    #define Kafuu return
    #define Chino 0
    #define fx(l,n) inline l n
    #define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
    #define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
    #define R register int
    using namespace std;
    string st;
    int last=1,ndn=1,num,head[N],at[N],ans;
    struct SAM{
    	int c[26],len,fa;
    }s[N];
    struct Edge{
    	int na,np;
    }e[N<<1];
    queue<int>q;
    fx(void,add)(int f,int t){
    	e[++num].na=head[f];
    	e[num].np=t;
    	head[f]=num;
    }
    fx(void,SAMadd)(const int val){
    	int bf=last,now=++ndn;
    	at[last=now]=1;
    	s[now].len=s[bf].len+1;
    	while(bf&&!s[bf].c[val]){
    		s[bf].c[val]=now;
    		bf=s[bf].fa;
    	}
    	if(!bf) s[now].fa=1;
    	else{
    		int to=s[bf].c[val];
    		if(s[to].len==s[bf].len+1) s[now].fa=to;
    		else{
    			int nto=++ndn;
    			s[nto]=s[to];
    			s[nto].len=s[bf].len+1;
    			s[to].fa=s[now].fa=nto;
    			while(bf&&s[bf].c[val]==to){
    				s[bf].c[val]=nto;
    				bf=s[bf].fa;
    			}
    		}
    	}
    }
    fx(void,dp)(const int now){
    	for(R i=head[now];i;i=e[i].na){
    		dp(e[i].np);
    		at[now]+=at[e[i].np];
    	}
    	if(at[now]>1) ans=max(ans,at[now]*s[now].len);
    }
    signed main(){
    	cin>>st;
    	for(R i=0;i<st.length();i++) SAMadd(st[i]-'a');
    	for(R i=2;i<=ndn;i++) add(s[i].fa,i);
    	dp(1);
    	cout<<ans;
    	Kafuu Chino;
    }
    

    P2408 不同子串个数

    题目链接

    我们知道,每一个子串在SAM都可以被唯一的表示出来

    那么从根节点向每个节点跑,跑出来的即是所有的子串

    DAG上玩dp即可

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #define N 2000001
    #define INF 1100000000
    #define Kafuu return
    #define Chino 0
    #define fx(l,n) inline l n
    #define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
    #define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
    #define R register int
    #define int long long
    using namespace std;
    string st;
    int n,last=1,ndn=1,num,head[N],at[N],ans[N];
    struct SAM{
    	int c[26],len,fa;
    }s[N];
    struct Edge{
    	int na,np;
    }e[N<<1];
    queue<int>q;
    fx(void,add)(int f,int t){
    	e[++num].na=head[f];
    	e[num].np=t;
    	head[f]=num;
    }
    fx(void,SAMadd)(const int val){
    	int bf=last,now=++ndn;
    	at[last=now]=1;
    	s[now].len=s[bf].len+1;
    	for(;bf&&!s[bf].c[val];bf=s[bf].fa) s[bf].c[val]=now;
    	if(!bf) s[now].fa=1;
    	else{
    		int to=s[bf].c[val];
    		if(s[to].len==s[bf].len+1) s[now].fa=to;
    		else{
    			
    			int nto=++ndn;
    			s[nto]=s[to];
    			s[nto].len=s[bf].len+1;
    			s[to].fa=s[now].fa=nto;
    			for(;bf&&s[bf].c[val]==to;bf=s[bf].fa) s[bf].c[val]=nto;
    		}
    	}
    }
    fx(int,dfs)(const int now){
    	if(ans[now]) return ans[now];
    	for(R i=0;i<26;i++) if(s[now].c[i]) ans[now]+=dfs(s[now].c[i])+1;
    	return ans[now];
    }
    signed main(){
    	cin>>n>>st;
    	for(R i=0;i<st.length();i++) SAMadd(st[i]-'a');
    	cout<<dfs(1);
    	Kafuu Chino;
    }
    

    P4070 [SDOI2016]生成魔咒

    题目链接

    由SAM性质可知,每个状态 (s) 代表的串的长度是 ((S_{1,fa_s},S_{1,s}])

    由于SAM本来就是在线的,所以每次加点直接统计即可

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<map>
    #include<algorithm>
    #include<queue>
    #define N 1000001
    #define M 5001
    #define INF 1100000000
    #define Kafuu return
    #define Chino 0
    #define fx(l,n) inline l n
    #define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
    #define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
    #define int long long
    #define R register
    #define C const
    using namespace std;
    int last=1,now,ndn=1,top,ans,num,head[N],size[N],n,v;
    string st;
    struct SAM{
    	int len,fa;
    	map<int,int>c;
    }s[N];
    fx(int,gi)(){
    	R char c=getchar();R int s=0,f=1;
    	while(c>'9'||c<'0'){
    		if(c=='-') f=-f;
    		c=getchar();
    	}
    	while(c<='9'&&c>='0') s=(s<<3)+(s<<1)+(c-'0'),c=getchar();
    	return s*f;
    }
    fx(void,SAMadd)(C int val){
    	int bf=last,now=++ndn;last=now;
    	size[now]=1;
    	s[now].len=s[bf].len+1;
    	while(bf&&!s[bf].c[val]){
    		s[bf].c[val]=now;
    		bf=s[bf].fa;
    	}
    	if(!bf) s[now].fa=1;
    	else{
    		int to=s[bf].c[val];
    		if(s[to].len==s[bf].len+1) s[now].fa=to;
    		else{
    			int nq=++ndn;
    			s[nq]=s[to];
    			s[nq].len=s[bf].len+1;
    			s[to].fa=s[now].fa=nq;
    			while(bf&&s[bf].c[val]==to){
    				s[bf].c[val]=nq;
    				bf=s[bf].fa;
    			}
    		}
    	}
    	ans+=s[now].len-s[s[now].fa].len;
    	printf("%lld
    ",ans);
    }
    signed main(){
    	n=gi();
    	for(R int i=1;i<=n;i++) v=gi(),SAMadd(v);
    }
    

    P4248 [AHOI2013]差异

    题目链接

    [sumlimits_{1le i<jle n} ext{len}(T_i)+ ext{len}(T_j)-2 imes ext{lcp}(T_i,T_j) ]

    很显然,( ext{len}(T_i)+ ext{len}(T_j)) 是一个定值,故我们只需要求字符串两两的最长公共前缀的长度之和即可

    由于这是个后缀自动机,公共前缀不好求,但是公共后缀却可以方便的求出来

    我们将原字符串反过来建SAM即可

    容易发现两个前缀的公共后缀就在 (parent) 树上它们的LCA那个状态上

    此时本题就变成了统计一个点是多少点的LCA,然后答案累计起来

    这是个简单问题,将叶节点置 (1),树形dp即可

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<queue>
    #define N 1000001
    #define M 5001
    #define INF 1100000000
    #define Kafuu return
    #define Chino 0
    #define fx(l,n) inline l n
    #define set(l,n,ty,len) memset(l,n,sizeof(ty)*len)
    #define cpy(f,t,ty,len) memcpy(t,f,sizeof(ty)*len)
    #define int long long
    #define R register
    #define C const
    using namespace std;
    int last=1,now,ndn=1,top,ans,num,head[N],size[N],n;
    string st;
    struct SAM{
    	int c[26],len,fa;
    }s[N];
    struct Edge{
    	int na,np;
    }e[N];
    fx(void,add)(int f,int t){
    	e[++num].na=head[f];
    	e[num].np=t;
    	head[f]=num;
    }
    fx(int,gi)(){
    	R char c=getchar();R int s=0,f=1;
    	while(c>'9'||c<'0'){
    		if(c=='-') f=-f;
    		c=getchar();
    	}
    	while(c<='9'&&c>='0') s=(s<<3)+(s<<1)+(c-'0'),c=getchar();
    	return s*f;
    }
    fx(void,SAMadd)(C int val){
    	int bf=last,now=++ndn;last=now;
    	size[now]=1;
    	s[now].len=s[bf].len+1;
    	while(bf&&!s[bf].c[val]){
    		s[bf].c[val]=now;
    		bf=s[bf].fa;
    	}
    	if(!bf) s[now].fa=1;
    	else{
    		int to=s[bf].c[val];
    		if(s[to].len==s[bf].len+1) s[now].fa=to;
    		else{
    			int nq=++ndn;
    			s[nq]=s[to];
    			s[nq].len=s[bf].len+1;
    			s[to].fa=s[now].fa=nq;
    			while(bf&&s[bf].c[val]==to){
    				s[bf].c[val]=nq;
    				bf=s[bf].fa;
    			}
    		}
    	}
    }
    fx(void,tdp)(int now){
    	for(R int i=head[now];i;i=e[i].na){
    		tdp(e[i].np);
    		ans+=size[now]*size[e[i].np]*s[now].len;
    		size[now]+=size[e[i].np];
    	}
    }
    signed main(){
    	cin>>st;
    	n=st.length();
    	for(R int i=st.length()-1;~i;i--) SAMadd(st[i]-'a');
    	for(R int i=2;i<=ndn;i++) add(s[i].fa,i);
    	tdp(1);
    	printf("%lld",(n-1)*n*(n+1)/2-2*ans);
    }
    
  • 相关阅读:
    如何将DataTable转换成List<T>
    关于SqlDataAdapter的使用
    VS 2010中JS代码折叠插件
    ASP.net中的几种分页方法
    学习jquery基础教程的一些笔记
    js中innerHTML与innerText的用法与区别
    SpringBoot 中使用shiro注解使之生效
    redis分布式锁
    使用ZSetOperations(有序)操作redis
    使用SetOperations(无序)操作redis
  • 原文地址:https://www.cnblogs.com/zythonc/p/14494802.html
Copyright © 2011-2022 走看看