后缀自动机 (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 ...
随机推荐
- 【Linux】SecureCRT中按退格键出现^H
分两步: ①SecureCRT上部的“选项”→“会话选项”→终端→仿真→映射键→其他映射→Backspace发送delete(B) 勾选中,确定 ②SecureCRT上部的“选项”→全局选项→常规→默 ...
- 【Oracle】闪回drop后的表
本文介绍的闪回方式只适用于:删除表的表空间非system,drop语句中没有purge关键字(以上两种情况的误删除操作只能通过日志找回): 1.删除表后直接从回收站闪回 SCOTT@LGR> d ...
- redis的跳跃表
跳跃表是一种插入.查询.删除的平均时间复杂度为O(nlogn)的数据结构,在最差情况下是O(n),当然这几乎很难出现. 和红黑树相比较 最差时间复杂度要差很多,红黑树是O(nlogn),而跳跃表是O( ...
- SIFT算法总结:用于图像搜索
原始文章链接:http://bubblexc.com/y2011/163/ 原文链接:http://blog.csdn.net/cserchen/article/details/5606859 关于三 ...
- jquery 星级评价插件jquery Raty的使用
需要引入的js <script type="text/javascript" src="<%=basePath%>resources/js/jquery ...
- html img加载不同大小图像速度
最近要想法提高网页的性能,在查看图片加载时,产生了试验的想法.一直以来都没有太去深究,还是挖掘一下的好. 很简单的试验,<img>加载两个图像,一个2.3MB,5000*5000,一个22 ...
- idea中git的运用
创建本地 Git 仓库 安装 Git 插件 将代码添加到 Git 的本地仓库 在 GitHub 中创建仓库
- JDBC连接MySQL数据库(一)——数据库的基本连接
JDBC的概念在使用之前我们先了解一下JDBC的概念, JDBC的全称是数据库连接(Java Database Connectivity),它是一套用于执行SQL语句时的API,应用程序可以通过这套A ...
- css实现三栏水平布局双飞翼与圣杯布局
作为布局的入门级选手,网上也查看了很多信息和资源 双飞翼的html结构 <div class="container"> <div class="main ...
- [noip2011]计算系数+二项式定理证明
大水题,二项式定理即可(忘得差不多了) 对于一个二项式,\((a+b)^n\)的结果为 \(\sum_{k=0}^{k<=n}C_{n}^{k}a^{n-k}b^k\) 证明: 由数学归纳法,当 ...