后缀自动机,一个处理字符串问题的神器。听起来很神圣,貌似很难写。其实代码实现并不复杂,萌新估计都能学会。

以前听学长们讲过好多次也看过陈立杰的课件,都不是很明白。今天终于弄明白了,就写一个让大家都能看懂的讲解(其实主要是给自己复习用的QAQ),来填坑吧。
学习这种东西最好用笔在草稿纸上多画一画,遇到不懂得尝试举例看一看,就直观多了。

首先先明确几个定义:
·对于字符串S,他的后缀自动机为SAM。
·初始位置为init。
·如果自动机无法识别当前串,则会转移到Null(空);并且规定Null只能转移到Null。
·规定trans(s,ch)为当前状态为s,读入ch后转移到的状态
·规定Reg(A)为自动机能识别的串的集合。即使得trans(init,x)∈end的串x。
·规定Reg(s)为从s开始能识别的串的集合,即使得trans(init,sx)属于Reg(A)的x的集合。
·规定SAM(x)=1,当且仅当x是S的后缀,SAM(x)≠Null,当且仅当x是S的后缀或子串。
·ST(str)表示trans(init,str),即从初始状态转移str后达到的状态。
·规定母串S的后缀集合为Suf,连续子串集合为Fac,从位置a开始的后缀为Suffix(a)。
·规定S[l,r)表示S区间[l,r)的子串(前闭后开),下标从0开始。

然后,我们可以得到一些推论:
若串s∉Fac,则ST(s)=Null。
我们考虑ST(a)之后能接那些字符串,即Reg(ST(a)),ST(a)能识别S,当且仅当ax是S的后缀。所以,x也是s的后缀。Reg(ST(a))是后缀的一个集合。
画出来是这样的:

对于一个状态,我们只关心Reg(x)。
若字符串a在原串S的[l,r)出现,则可以识别S从r开始的的后缀。
设a在[l1,r1),[l2,r2)...[ln,rn)中出现,则Reg(ST(a))为{suffix(r1),suffix(r2)...suffi(rn)}。
设Right(a)={r1,r2...rn},则Reg(ST(a))完全由right(a)决定(这不是显然吗,但是注意right()并不决定a,之后会讲)。
那么,若right(a)=right(b),则两个状态可结束的右区间相同,那么ST(a)=ST(b)。
故一个状态s(或者叫串的集合s),由所有right(x)相同均为right的串x组成。
设r∈right(s),则在给定长度len后可以唯一确定该子串为s[ri-len,ri),其中ri为集合任意一个r(为什么左边是减,右边不动?区间左边小)。
考虑下面的串:

若让集合right(s)包含且仅包含5,11(即蓝色),那么s可为的子串为:aaaab,aaab,其长度为4,5。
若让集合right(s)包含且仅包含5,11,17,22(即蓝色和红色),那么s可为的子串为:aab,ab,其长度为2,3。
所以,对于一个集合right(s),其s的长度是一个区间,我们定义这个区间为[min(s),max(s)]。
证明:如果长度l,r合适,则l,r之间的所有长度均合适(这个显然)。如果长度为r+1,则会扩大限制,缩小集合(上面例子中第二个集合变成第一个);若长度为l-1,则会松弛限制,扩大集合(上面例子中第一个到第二个)。(感觉我讲的已经很明白了吧QAQ)

状态数为线性的证明:
点数线性:
考虑两个状态a、b,其right集合分别为Ra,Rb。
假设Ra,Rb存在交集,即有r包含于Ra且r包含于Rb(原谅我真的打不出符号了)。
a、b表示的子串不会有交集(为什么?否则他们不是两个状态。对于某个串str,ST(sta)既为a又为b,那么a、b就是同一个状态了的说)。
因为字符串无交集而r有交集,那么对于r中的位置。其表示的前置字符串长度不同。
所以,就有:

我们不妨设(钦定)minB>maxA,这样的话,a中所有的串均为b中所有的串的后缀。对于所有Right(B)中的位置r,其前面a中所有的串一定都出现了。而a对长度的限制更小,所以集合Ra一定比集合Rb大。故Rb为Ra的真子集。(这里讲的和陈立杰课件不一样,严重怀疑其幻灯片有错)
所以,我们证明了:对于两个Right集合,要么完全不相交,要么一个是另一个的真子集。
这样的话我们可以把他们画成一棵树:

(借用陈立杰课件的图)
我们称之为parent树。
这棵树上叶子节点数量为O(n)的,而非叶子节点每个节点最少两个子树(什么你说一个?为什么不合并了它),数量也是O(n)的。故节点个数为O(n)的。所以节点个数是线性的。
另外,对于下面的s,如果选的串比min(s)更短,则会跑到其父亲一层去。所以,max(fa)=min(s)-1。
边数线性:
对于一条由状态a开始,字符为c的边,如果Trans(a,c)为Null,我们可以不存储它。
而如果Trans(a,c)=b(ab为状态),那么则有一条a->b的标号为c的边。
我们先求出一个SAM的生成树,生成树的边数为O(n)的。
我们考虑一个非树边,如果有状态通过他转移,那么则有init经过“生成树状态中a + 非树边a->b + 生成树状态b”到一个ed。
这是一条init->ed的路径,为一个后缀。
我们另一个后缀对应第一个非树边,那么一个后缀最多对应一条非树边(显然),一个非树边至少被一个后缀对应(这个为什么?)。

其中所有小括号表示非树边。
那么由于c->d只会被“跨过”,那么我们可以把状态cd合并掉也没关系吧,换句话说,状态c没有存在的必要,状态c和d本来就应该是一个状态!
由于后缀数量是O(n)的,所以非树边数量也是O(n)的,总体还是O(n)的。这样也就证明了边数是线性的。

如何存储:
我们显然不能在一个状态中存储所有right集合。因为我们不知道一个位置会被存储多少次,这样会MLE。
然而一个状态的right集合一定是其后代叶子节点的并集。
按照DFS序

对于上面节点的right集合,一定是DFS序上连续的一段区间。(为什么?DFS序保证子树为连续区间的说)
对于状态s,其right集合为right(s),若有s->t,字符为c的边,则在s的right中,只有S[ri]==c的符合要求,那么t的right集合为{ri+1|r[i]==c},且max(t)>max(s)(为什么?多了一个字符c的说)。
如果s出发有字符为x的边,那么parent(s)出发的也有(为什么,因为parent(s)表示s的后缀!)。令f=parent(s),则right(trans(s,c))包含于right(trans(f,c))。

实现:
可能你感觉我说了这么多废话证明,这玩意到底怎么写呢?
好,下面讲一下怎么实现。
一开始我们有一个空的SAM,现在考虑如何增加一位,即把字符串T的SAM拓展为字符串Tx的SAM。
令p=ST(T)(说白了就是上次加入的节点QAQ),新建np=ST(Tx)。连接trans(p,x)=np。
对p无转移x的祖先v,令trans(v,x)=np。
找到p第一个有转移x的祖先vp。如果没有vp,令parent(np)=root,拓展结束,返回。
令q=trans(vp,x),若max(q)==max(vp+1),直接令parent(np)=q,拓展结束,返回(这种情况表示np为q的子集,同时直接加入不会使max(q)变得更小,所以直接连接就可以了。举个例子的话,q表示ab,np表示aab,vp表示a。)。
否则新建节点nq,使得trans(nq,*)=trans(q,*),parent(nq)=parent(q),max(nq)=max(vp)+1,然后让parent(np)=parent(q)=nq(这种情况表示nq的串比vp+1长,强行插入会使之max变小,所以加入一个更短的节点并把后继拆成两个独立的节点。例如vp表示a,q表示aaab,np表示aab,这样强行插入会使得q的max变小。而这两种状态都是状态ab的子集,所以我们新建nq表示ab即可)。

最后上代码:

 1 struct Node {
 2     int mx;
 3     Node *par,*nxt[26];
 4 }ns[maxn],*root,*last;
 5
 6 inline Node* NewNode(int len) {
 7     static int cnt = 0;
 8     ns[++cnt].mx = len;
 9     return &ns[cnt];
10 }
11
12 inline void extend(int x) {
13     Node *p = last;
14     Node *np = NewNode(p->mx+1);
15     while( p && !p->nxt[x] )
16         p->nxt[x] = np,
17         p = p->par;
18     if( !p )
19         np->par = root;
20     else {
21         Node* q = p->nxt[x];
22         if( q->mx == p->mx + 1 )
23             np->par = q;
24         else {
25             Node* nq =NewNode(p->mx+1);
26             memcpy(nq->nxt,q->nxt,sizeof(q->nxt));
27             nq->par = q->par;
28             q->par = np->par = nq;
29             while( p && p->nxt[x] == q )
30                 p->nxt[x] = nq,
31                 p = p->par;
32         }
33     }
34     last = np;
35 }

View Code

例题:
SPOJ NSUBSTR:
一句话题面:给定一个字符串,求每个对应的长度能产生的相同子串的最大个数。
相同子串个数就是其后缀自动机节点上right()集合的大小,并且长的字符串可以切割成短的,所以我们只用每个节点可表示的最长长度去更新即可。
我们先构建出后缀自动机,然后再parent树上反向拓扑排序跑出right大小,更新答案。然后把答案倒着扫一遍用长的更新短的即可。
代码:

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<algorithm>
 5 #include<queue>
 6 #define debug cout
 7 using namespace std;
 8 const int maxn=5e5+1e2;
 9
10 char in[maxn];
11 int f[maxn],len,cnt;
12
13 struct Node {
14     int mx;
15     Node *par,*nxt[26];
16     int right,deg;
17 }ns[maxn],*root,*last;
18
19 inline Node* NewNode(int len) {
20     ns[++cnt].mx = len;
21     return &ns[cnt];
22 }
23
24 inline void extend(int x) {
25     Node *p = last;
26     Node *np = NewNode(p->mx+1);
27     np->right = 1;
28     while( p && !p->nxt[x] )
29         p->nxt[x] = np,
30         p = p->par;
31     if( !p )
32         np->par = root;
33     else {
34         Node* q = p->nxt[x];
35         if( q->mx == p->mx + 1 )
36             np->par = q;
37         else {
38             Node* nq =NewNode(p->mx+1);
39             memcpy(nq->nxt,q->nxt,sizeof(q->nxt));
40             nq->par = q->par;
41             q->par = np->par = nq;
42             while( p && p->nxt[x] == q )
43                 p->nxt[x] = nq,
44                 p = p->par;
45         }
46     }
47     last = np;
48 }
49
50 inline void topo() {
51     queue<int> q;
52     for(int i=1;i<=cnt;i++)
53         if( &ns[i] != root )
54             ++ns[i].par->deg;
55     for(int i=1;i<=cnt;i++)
56         if( !ns[i].deg )
57             q.push(i);
58     while( q.size() ) {
59         const int pos = q.front(); q.pop();
60         if( &ns[pos] == root )
61             break;
62         ns[pos].par->right += ns[pos].right;
63         f[ns[pos].mx] = max( f[ns[pos].mx] , ns[pos].right );
64         --ns[pos].par->deg;
65         if( !ns[pos].par->deg )
66             q.push( ns[pos].par - ns );
67     }
68 }
69
70 inline void calc() {
71     for(int i=len-1;i;i--)
72         f[i] = max( f[i] , f[i+1] );
73 }
74
75 int main() {
76     scanf("%s",in+1);
77     len = strlen(in+1);
78
79     last = root = NewNode(0);
80     for(int i=1;i<=len;i++)
81         extend(in[i]-'a');
82
83     topo();
84     calc();
85
86     for(int i=1;i<=len;i++)
87         printf("%d\n",f[i]);
88
89     return 0;
90 }

View Code

SPOJ SUBLEX:
一句话题面:对于给出的长度不超过90000的字符串进行Q(Q <= 500)次询问, 每次询问给出一个K, 要求输出第K小的子串(0 < K < 2^31),其中相同的子串只算一次例如“aaa"的子串是"a", "aa", "aaa"
考虑如果在Trie树上怎么做,先统计size然后26分字符。在后缀自动机上也这样做即可。注意到后缀自动机沿着匹配字符边走是一个DAG,所以我们直接用记忆化搜索跑出走这个方向存在表示的字符串个数即可。(这么鬼畜的东西我是拒接写迭代的QAQ)
代码:

 1 #include<iostream>
 2 #include<cstdio>
 3 #include<cstring>
 4 #include<algorithm>
 5 #include<queue>
 6 #define lli long long int
 7 #define debug cout
 8 using namespace std;
 9 const int maxn=2e5+1e2;
10
11 char in[maxn];
12 int len,m,cnt;
13
14 struct Node {
15     Node *fa,*fm,*nxt[26];
16     int len;
17     lli sum;
18 }ns[maxn],*root,*last;
19
20 inline Node* NewNode(int ll) {
21     ns[++cnt].len = ll;
22     return &ns[cnt];
23 }
24
25 inline void extend(int x) {
26     Node* p = last;
27     Node* np = NewNode(p->len+1);
28     while( p && !p->nxt[x] )
29         p->nxt[x] = np,
30         p = p->fa;
31     if( !p )
32         np->fa = root;
33     else {
34         Node* q = p->nxt[x];
35         if( q->len == p->len + 1 )
36             np->fa = q;
37         else {
38             Node* nq = NewNode(p->len+1);
39             memcpy(nq->nxt,q->nxt,sizeof(q->nxt));
40             nq->fa = q->fa;
41             np->fa = q->fa = nq;
42             while( p && p->nxt[x] == q )
43                 p->nxt[x] = nq,
44                 p = p->fa;
45         }
46     }
47     last = np;
48 }
49
50 inline lli dfs(Node* pos) {
51     if( pos->sum )
52         return pos->sum;
53     lli ret = 0;
54     for(int i=0;i<26;i++)
55         if( pos->nxt[i] )
56             ret += dfs(pos->nxt[i]);
57     pos->sum = ret + ( pos != root );
58     return pos->sum;
59 }
60
61 inline void query(lli x) {
62     Node* pos = root;
63     while( x ) {
64         for(int i=0;i<26;i++)
65             if( pos->nxt[i] ) {
66                 if( x <= pos->nxt[i]->sum ) {
67                     putchar(i+'a');
68                     pos = pos -> nxt[i];
69                     --x;
70                     break;
71                 }
72                 else
73                     x -= pos->nxt[i]->sum;
74             }
75     }
76     putchar('\n');
77 }
78
79 int main() {
80     scanf("%s",in+1);
81     len = strlen(in+1);
82
83     last = root = NewNode(0);
84     for(int i=1;i<=len;i++)
85         extend(in[i]-'a');
86
87     dfs(root);
88
89     scanf("%d",&m);
90     while( m-- ) {
91         lli x;
92         scanf("%lld",&x);
93         query(x);
94     }
95
96     return 0;
97 }

View Code

完结撒花!!祝大家元旦快乐2333。

转载于:https://www.cnblogs.com/Cmd2001/p/8148944.html

从零开始の后缀自动机相关推荐

  1. 【POJ1509】Glass Beads 【后缀自动机】

    题意 给出一个字符串,求它的最小表示法. 分析 这个题当然可以用最小表示法做啦!但是我是为了学后缀自动机鸭! 我们把这个字符串长度乘二,然后建SAM,然后在SAM上每次跑最小的那个字母,找出长度为n的 ...

  2. bzoj 2946 [Poi2000]公共串——后缀自动机

    题目:https://www.lydsy.com/JudgeOnline/problem.php?id=2946 对每个串都建一个后缀自动机,然后 dfs 其中一个自动机,记录同步的话在别的自动机上走 ...

  3. 后缀自动机 ---- P3804 【模板】后缀自动机(求每个等价类最长串的出现次数)

    后缀自动机一些关键点 首先后缀自动机上面每个节点都是一个等价类并且是最长的字符串的结尾 后缀自动机上的fail链反建就是parent tree,下面是SAM和Parent tree的构造 对于这道模板 ...

  4. 洛谷P3966 [TJOI2013]单词(后缀自动机)

    传送门 统计单词出现次数--为啥大家都是写AC自动机的嘞--明明后缀自动机也能做的说-- 统计出现次数这个就直接按长度排序然后做个dp就好,这是SAM的板子的要求啊,不提了 然后考虑怎么让所有串之间隔 ...

  5. hihocoder 后缀自动机专题

    一.后缀自动机基本概念的理解 1.首先后缀自动机的状态是由子串的endpos来决定的 子串的endpos是指一个子串可以在原字符串的哪些位置进行匹配, endpos构成的不同集合划分成不同的状态 关于 ...

  6. BZOJ3998: [TJOI2015]弦论(后缀自动机,Parent树)

    Description 对于一个给定长度为N的字符串,求它的第K小子串是什么. Input 第一行是一个仅由小写英文字母构成的字符串S 第二行为两个整数T和K,T为0则表示不同位置的相同子串算作一个. ...

  7. 【BZOJ3926】[Zjoi2015]诸神眷顾的幻想乡 广义后缀自动机

    [BZOJ3926][Zjoi2015]诸神眷顾的幻想乡 Description 幽香是全幻想乡里最受人欢迎的萌妹子,这天,是幽香的2600岁生日,无数幽香的粉丝到了幽香家门前的太阳花田上来为幽香庆祝 ...

  8. 【洛谷 P3975】 [TJOI2015]弦论(后缀自动机)

    题目链接 建出后缀自动机. T=0,每个子串算一次,否则每个子串算该子串的\(endpos\)集合大小次. 用\(f[i]\)表示结点\(i\)表示的\(endpos\)集合大小,则\(f[i]\)为 ...

  9. BZOJ3277 串 【广义后缀自动机】

    Description 字符串是oi界常考的问题.现在给定你n个字符串,询问每个字符串有多少子串(不包括空串)是所有n个字符串中 至少k个字符串的子串(注意包括本身). Input 第一行两个整数n, ...

最新文章

  1. Nanopore sequencing technology and tools for genome assembly: computational analysis of the current
  2. Radware:当前,CDN安全远远不足
  3. AI Boot Camp 分享之 ML.NET 机器学习指南
  4. 编程体系结构(07):JavaEE之Web开发
  5. 关于nginx,你不可不知的几大特色功能
  6. mysql怎么初始化自增值_MySQL 重置自增值
  7. 设计模式12——中介者模式
  8. 深度学习框架排行榜:找工作学TensorFlow,PyTorch搜索量逼近Keras
  9. thinkphp ajax 无刷新分页效果的实现
  10. 用pyinstaller把python脚本编译成可执行程序(windows系统)
  11. sklearn 特征降维利器 —— PCA TSNE
  12. 学习c++一点一滴----读取注册表
  13. Arch Linux下 让MPlayer用上CoreAVC1.7.0.0解码器
  14. web安全与渗透测试培训全套视频
  15. 剑指Offer(三十一):整数中1出现的次数(从1到n整数中1出现的次数)
  16. 比特精灵是计算机病毒吗,[病毒防治]靠,我居然也中木马?
  17. 神经网络可以解决的问题,人工神经网络通过调整
  18. 20200722-Java面经-被血虐-面试问题及总结
  19. 3.1 SQL是什么?它能做什么
  20. 职业生涯发展理论(精)

热门文章

  1. Vim替换小技巧(兼浅谈Vim哲学)
  2. word2vec 中的数学原理具体解释(四)基于 Hierarchical Softmax 的模型
  3. 利用Cache,asp.net 简单实现定时执行任务
  4. 冒号课堂§4.3:汇总范式
  5. 1.2.3 TCP/IP参考模型和五层参考模型
  6. SPI 的主模式和从模式
  7. 实用技巧:Excel中的常见问题以及解决方法
  8. 浏览器和迅雷简单拿到动漫妹子图
  9. Django实战之美化页面
  10. 2017.4.24 js 中的iscroll