后缀自动机 (WJMZBMR讲稿的整理和注释)
链接放在这里,有点难理解,至少我个人是的。
后缀自动机是一种有限状态自动机,其功能是识别字符串是否是母串的后缀。它能解决的问题当然不仅仅是判断是不是后缀这种事,跟字符串的连续子串有关的问题都可以往这个方面考虑,毕竟字符串的每个连续字串都可以看做是两个长度不同的后缀去掉他们的公共部分得到的。
自动机由五个部分组成:alpha:字符集,state:状态集合,init:初始状态集合,end:结束状态集合,trans:状态转移函数。
定义trans(s,ch)为当前状态为s,读入字符ch后所达到的状态。若不存在此转移,则将转移的结果定义为null,表示不存在的状态。自动机A能识别的字符串就是所有使得trans(init,x)∈end的字符串,令这些字符串组成的集合为Reg(A)。另外,对于自动机中的某一状态s,从s开始能识别的字符串记为Reg(s)。
考虑字符串“aabbabd”的后缀,一共有7个,简单的想法是可以将这7个后缀构造成一个trie树(ppt里的图好像有问题,多了abbd这条路线),缺点是状态数太多,对于长度为N的字符串,其节点的规模会达到O(N^2),而后缀自动机相比起来就小多了,其状态数是线性的。
根据aabbabd构造的后缀自动机:
几个关键的概念:
ST(str):从初始状态init读入字符串str后转移到的状态。
S:母串。
Suf:S的后缀集合。
Fac:S的所有连续子串集合。
Suffix(a):从位置a开始的集合。
S[l,r):S中[l,r)这个区间构成的连续子串。
Right:对于一个在母串中出现过的连续子串,其每次出现的结束位置集合成为该子串的Right集。例如aabbabd中,子串ab出现了2次,其Right集为{3,6}。
state:后缀自动机的状态,对应一个Right集,一个状态由所有Right集相同的子串构成。对于一个字符串,其连续子串的规模是O(N^2)。在所有的连续子串中,有这样一些子串,它们的Right集合是相同的。仍然以aabbabd为例:子串aabb,abb,bb在字符串中均只出现了一次,Right集均为{4},同时Right集合为{4}的只有这3个子串,这3个子串构成一个状态,含义是只在5这里出现了一次的子串。
结论:
1.令r∈Right(s),只要给定子串的长度len就可以确定子串了。
2.对于某一个状态s,如果长度为l和r的子串都属于此状态,那么位于l和r之间的所有子串也都属于此状态。
长度在l和r之间的子串都可以看做长度为r的子串的后缀,长度为r的子串每一次出现,那么长度[l,r)也就是更短一些的子串也必定出现了。随着子串的长度从r逐渐减小,子串出现的机会越来越多,当然也可以在一定范围内保持不变。如果到l为止仍然属于同一个状态,能么可以想见[l,r]全部都属于同一个状态。
那么,状态可以这样简单地理解,如图(长方形代表字符串......):
一个状态由多个构造相同的分支组成,每个分支包括若干个字符串,这些字符串由前半部分横线阴影所代表的每各个缀加上后边斜线阴影的全部内容构成。对应上一段的内容,横线阴影左边界到斜线阴影右边界的长度相当于r,横线阴影右边界到斜线阴影右边界的长度相当于l。
令状态s的长度区间为[Min(s),Max(s)]。
3.状态数和边数是线性的。
考虑两个状态a,b。他们的Right集合分别为Ra和Rb。若Ra和Rb有交集,假设r∈Ra∩Rb。因为状态是按照Right集合来划分的,所以不同的状态手下的字符串也是不相同的。现在对于r这个位置来说,状态a的字符串和状态b的字符串都在这里出现并结束了,所以,他们至少结尾的一部分是相同的。而a和b手下的字符串是没有交集的,只有一种可能:它们开始的位置各不相同,也就是说,必然满足[Min(a),Max(a)]和[Min(b),Max(b)]是没有交集的。这说明,a和b中的某一个,它手下全部的字符串都是另一个手下任意一个字符串的后缀,哪怕是最短的那个。那么,对于Ra和Rb来说,其中一个是另一个的真子集。
总结一下两种状态之间的关系:或者不相交,或者一个是另一个的真子集,可以想见,各个状态的Right集合实际上构成了一个树形结构,不妨称为parent树。
假想一下,每个字符串结尾有一个空字符,对于只有一个空字符的字符串,它的Right集合就是{1,2,3,4......N},这个状态就是根节点。在字符串左边填入不同的字符,相当于增加对字符串的限定,也可以理解为对字符串的分类,a+str是一类,b+str是一类......每一类对应一个儿子节点,也是一个状态,继承了父节点Right集合的一个子集。当然,有的时候,在左边加一个字符并不能分类,连续加上Max(s)-Min(s)个才行,因为当前字符串左边只能加上那个字符,否则在母串中就不存在了。
那么,树形结构,叶节点的数量为N,总的节点数最多为2*N。
构造算法:
首先定义状态节点:
- struct Node {
- int step;
- Node *pre, *nxt[];
- void clear() {
- step = ;
- pre = NULL;
- memset(nxt, NULL, sizeof(nxt));
- }
- }*root, *last;
step表示此状态手下的字符串的最大长度,pre指向此状态在parent树中的父亲结点。nxt[i]数组指向由当前状态后接一个字符i转移到的状态。这里并没有保存Right集合,因为每个状态节点的Right集合是其后代所有叶子节点的Right集合的并集。root表示初始状态,last表示结束状态。
每次添加一个字符,并维护当前的数据结构,使之保持我们所希望的性质。
令当前的字符串为T,新字符为x,T的长度为L。添加了x后新增加了一些子串,他们都是Tx的后缀,而Tx的后缀就是在T的后缀后面添加了一个x。此时需要做的就是将这些字符串分发给不同的状态节点,如果需要的话,可能会新建一个状态节点。
添加了这个x后,首先新建一个状态:
- Node *np = cur++, *p = last;
- np->clear();
- np->step = p->step + ;
其含义是只在这个新的结尾出现了一次的字符串。
考虑T所有的后缀(Right集合中包含L)所在的状态v1,v2......显然这些状态是parent树中从树叶到树根的一条路径。设v1=p是其中的叶节点,也就是只包含了一个最长的后缀的状态。
考虑其中一个v的Right集合{r1,r2,......rn=L}在它的后面添加一个x形成新的状态的话,只有S[ri]=x的那些ri是符合要求的。如果从v出发没有标号为x的转移(先不看rn),那么v的Right集合内就没有满足这个要求的ri。由于v1,v2......的Right集合逐渐扩大,所以如果从vi出发有标号为x的转移,那么之后的状态全部都有这样的转移。对于出发没有标号为x的转移v,它的Right集合内只有rn是满足要求的,符合状态np的“只在新的结尾出现了一次”,所以根据转移规则,让它连一条到np标号为x的转移:
- while (p && !p->nxt[w]) {
- p->nxt[w] = np, p = p->pre;
- }
循环结束有两种可能:
1:这些后缀所在的状态中没有任何一个有标号为x的转移,这种情况直接设置np的父节点为root就可以结束了。
- if (p == NULL) {
- np->pre = root;
- }
2:循环到某个p时,该状态有标号为x的转移。此时,又有两个分支。设q为trans(p,x)得到的状态。如果Max(q)==Max(p)+1,就可以直接将np作为q的儿子。
- Node *q = p->nxt[w];
- if (q->step == p->step + ) {
- np->pre = q;
- }
但Max(q)==Max(p)+1并不一定成立,这里我想了很久才想明白。考虑这样一个字符串:
a | a | b | b | c | c | x | a | a | b | b | c | c | x | b | b | c | c | x |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
最后一个x是刚刚添加的字符。在检查到bbcc这个后缀时,其Right集为Rp={6,13,18}。显然这种状态是有标号为x的转移的,转移后的结果是Rq={7,14},但其Max为7。而最后新添加的这个x,显然并不具备aabbccx的结构,否则就成了上面的那种情况。此时不能直接插入,而是需要新建一个状态,Right集为{7,14,19},Max为5。
- Node *nq = cur++;
- memcpy(nq->nxt, q->nxt, sizeof(q->nxt));
- nq->pre = q->pre;
- nq->step = p->step + ;
- np->pre = q->pre = nq;
nq就是新建的状态,基本照搬原来的q的数据,修改的地方:Max,应该的。设置q的父节点为nq,双方都具有尾部的bbccx结构,之前的q在这个基础上多了个aa,所以q是nq的子状态,然后设置np的父节点为nq。
接下来,向p的祖先方向找,凡是满足p->nxt[w] == q的都将其的父节点改为nq。
- while (p && p->nxt[w] == q) {
- p->nxt[w] = nq, p = p->pre;
- }
最后,修改结束状态为np
- last = np;
完整代码:
- #define N 5050
- struct Node {
- int step;
- Node *pre, *nxt[];
- void clear() {
- step = ;
- pre = NULL;
- memset(nxt, NULL, sizeof(nxt));
- }
- }*root, *last;
- Node pool[N * ], *cur;
- void init() {
- cur = pool;
- root = last = cur++;
- root->clear();
- }
- void extend(int w) {
- Node *np = cur++, *p = last;
- np->clear();
- np->step = p->step + ;
- while (p && !p->nxt[w]) {
- p->nxt[w] = np, p = p->pre;
- }
- if (p == NULL) {
- np->pre = root;
- } else {
- Node *q = p->nxt[w];
- if (q->step == p->step + ) {
- np->pre = q;
- } else {
- Node *nq = cur++;
- memcpy(nq->nxt, q->nxt, sizeof(q->nxt));
- nq->pre = q->pre;
- nq->step = p->step + ;
- np->pre = q->pre = nq;
- while (p && p->nxt[w] == q) {
- p->nxt[w] = nq, p = p->pre;
- }
- }
- }
- last = np;
- }
一些相关的练习题:HDU5470 3518 4416 5343 5853 4436 4622 5558 4641 1403
对象实现版:
- struct SAM {
- int ch[maxn][], fa[maxn], maxlen[maxn], Last, sz;
- int root, nxt[maxn], size[maxn];
- void init() {
- sz = ;
- root = ++sz;
- memset(size, , sizeof(size));
- memset(ch[], , sizeof(ch[]));
- memset(nxt, , sizeof(nxt));
- }
- void add(int x) {
- int np = ++sz, p = Last;
- Last = np;
- memset(ch[np], , sizeof(ch[np]));
- maxlen[np] = maxlen[p] + ;
- while (p && !ch[p][x]) ch[p][x] = np, p = fa[p];
- if (!p) fa[np] = ;
- else {
- int q = ch[p][x];
- if (maxlen[p] + == maxlen[q]) fa[np] = q;
- else {
- int nq = ++sz;
- memcpy(ch[nq], ch[q], sizeof(ch[q]));
- size[nq] = size[q];
- nxt[nq] = nxt[q];
- maxlen[nq] = maxlen[p] + ;
- fa[nq] = fa[q];
- fa[q] = fa[np] = nq;
- while (p && ch[p][x] == q) ch[p][x] = nq, p = fa[p];
- }
- }
- for (; np; np = fa[np])
- if (nxt[np] != now) {
- size[np]++;
- nxt[np] = now;
- } else break;
- }
- };
后缀自动机 (WJMZBMR讲稿的整理和注释)的更多相关文章
- 数据结构:后缀自动机 WJMZBMR讲稿的整理和注释
链接放在这里,有点难理解,至少我个人是的. 后缀自动机是一种有限状态自动机,其功能是识别字符串是否是母串的后缀.它能解决的问题当然不仅仅是判断是不是后缀这种事,跟字符串的连续子串有关的问题都可以往这个 ...
- 【整理】如何选取后缀数组&&后缀自动机
后缀家族已知成员 后缀树 后缀数组 后缀自动机 后缀仙人掌 后缀预言 后缀Splay ? 后缀树是后缀数 ...
- 后缀自动机(SAM)
*在学习后缀自动机之前需要熟练掌握WA自动机.RE自动机与TLE自动机* 什么是后缀自动机 后缀自动机 Suffix Automaton (SAM) 是一个用 O(n) 的复杂度构造,能够接受一个字符 ...
- HDU 1403 Longest Common Substring(后缀自动机——附讲解 or 后缀数组)
Description Given two strings, you have to tell the length of the Longest Common Substring of them. ...
- 【Codeforces235C】Cyclical Quest 后缀自动机
C. Cyclical Quest time limit per test:3 seconds memory limit per test:512 megabytes input:standard i ...
- [转]后缀自动机(SAM)
原文地址:http://blog.sina.com.cn/s/blog_8fcd775901019mi4.html 感觉自己看这个终于觉得能看懂了!也能感受到后缀自动机究竟是一种怎样进行的数据结构了. ...
- Codeforces 235C Cyclical Quest - 后缀自动机
Some days ago, WJMZBMR learned how to answer the query "how many times does a string x occur in ...
- 回文树&后缀自动机&后缀数组
KMP,扩展KMP和Manacher就不写了,感觉没多大意思. 之前感觉后缀自动机简直可以解决一切,所以不怎么写后缀数组. 马拉车主要是通过对称中心解决问题,有的时候要通过回文串的边界解决问题 ...
- Bzoj2534:后缀自动机 主席树启发式合并
国际惯例的题面:考虑我们求解出字符串uvu第一个u的右端点为i,第二个u的右端点为j,我们需要满足什么性质?显然j>i+L,因为我们选择的串不能是空串.另外考虑i和j的最长公共前缀(也就是说其p ...
随机推荐
- 03--SQLtie三言两语SQLtie链接(join)
本文将从连接的理论和语法讲起,结合具体的例子,详细分析 SQL 连接. 之前对数据库的连接操作似懂非懂,大概知道是什么东西,但是面试笔试的时候被虐成渣,讲不清连接到底是什么.吃一堑,长一智.这就是所谓 ...
- Pycharm 设置
1:显示行号 打上对勾OK 2:设置作者 & 文件编码 3:选择切换Python的版本
- centos 6.10 永久修改主机名
1> 修改配置文件 vim /etc/sysconfig/network #HOSTNAME=localhost.localdomain HOSTNAME=tomcat 2> 修改ho ...
- rabbitmq基本原理(转载)
Rabbitmq基本原理(转载) MQ全称为Message Queue, 是一种分布式应用程序的的通信方法,它是消费-生产者模型的一个典型的代表,producer往消息队列中不断写入消息,而另一端co ...
- ZooKeeper伪集群的搭建(Windows)
首先下载 zookeeper 地址:https://www.apache.org/dyn/closer.cgi/zookeeper/ 1.下载完成解压后修改文件夹名字为zookeeper1,然后删除c ...
- Zookeeper 使用
转自:https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/ 安装和配置详解 本文介绍的 Zookeeper 是以 3.2. ...
- Python爬虫1-----urllib模块
1.加载urllib模块的request from urllib import request 2.相关函数: (1)urlopen函数:读取网页 webpage=request.urlopen(ur ...
- IOS与h5交互记录
博主之前做过移动端app嵌入网页,与Android和IOS有交互,一直没有时间分享过程.这里不多说Android交互啦-很简单,详细了解IOS与h5的交互吧. IOS不同语法和h5的交互所建立的JSB ...
- Project Euler 37 Truncatable primes
题意:3797有着奇特的性质.不仅它本身是一个素数,而且如果从左往右逐一截去数字,剩下的仍然都是素数:3797.797.97和7:同样地,如果从右往左逐一截去数字,剩下的也依然都是素数:3797.37 ...
- 训练1-o
给出2个N * N的矩阵M1和M2,输出2个矩阵相乘后的结果. Input 第1行:1个数N,表示矩阵的大小(2 <= N <= 100)第2 - N + 1行,每行N个数,对应M1的1行 ...