后缀自动机

先上SAM builder备用链接。之前的垃圾博客,洛谷的某篇教程,饕餮传奇的题单

后缀自动机,点数是2n!

首先对着代码讲一遍三种插入。

 inline void insert(char c) { //
int f = c - 'a'; // 转移边
int p = last, np = ++top; // p 是之前的结尾节点,new p是新建的,代表全串及其若干后缀的节点
last = top; // 更新结尾节点
len[np] = len[p] + ; // 最长长度 + 1
while(p && !tr[p][f]) { // 一路上,如果某个后缀没有f的转移边,就连一条
tr[p][f] = np; // fail[p]是无法被p表示(right不同)的最长后缀们
p = fail[p]; //
} //
if(!p) { //
fail[np] = ; // 如果全都没有,插入结束
} //
else { // 此时有一个转移边,此时p是某个后缀
int Q = tr[p][f]; // Q是某个子串,跟最后若干位相同
if(len[Q] == len[p] + ) { // 如果Q仅仅表示一个串
fail[np] = Q; // 那么把new p的fail指向Q,告辞
} //
else { // 否则Q代表的不是一个串,在p的后面加入一个字符的同时,前面多了些字符
int nQ = ++top; // 此时新建new Q代表串"p+插入的字符",相当于把Q分开成两部分
len[nQ] = len[p] + ; // 长度自然是p + 1
fail[nQ] = fail[Q]; // 分出来的是Q的一个后缀,继承fail
fail[Q] = fail[np] = nQ; // Q以后就要先跳到new Q,np也是
memcpy(tr[nQ], tr[Q], sizeof(tr[Q])); // 因为是分离,继承所有转移边
while(tr[p][f] == Q) { // 此时的p没有Q长,p的f转移边其实都是到new Q的,只不过以前new Q没有单独的节点,所以给了Q
tr[p][f] = nQ; // 现在new Q收回给自己的转移边
p = fail[p]; //
} //
} //
} //
return; //
} //

还有实例帮助理解:接下来就要用串*******bca来做示范。

 inline void insert(char c) { //
int f = c - 'a'; // 此时插入了*******bc
int p = last, np = ++top; // 正在插入a
last = top; //
len[np] = len[p] + ; // p bc
while(p && !tr[p][f]) { // Q xbca
tr[p][f] = np; // np ***bca
p = fail[p]; // nQ bca
} //
if(!p) { // 这种情况,之前没有"bca"或"ca"或"a"出现,如 bcibcbca
fail[np] = ; //
} //
else { // 这种情况,之前出现过"bca",现在跳到了**bc上,出现了一个a的转移边
int Q = tr[p][f]; // 此时p是bc Q是(*)bca
if(len[Q] == len[p] + ) { // 这种情况,Q就是bca,之前出现了若干个bca而且前一个字符不同,导致Q不能表示*bca
fail[np] = Q; // 只能表示bca,例:123xbca456ybca789bc a
} // 此时把new p的fail接到Q上即可
else { // 这种情况,Q表示的是*bca,例如:123xbca456xbca789bc a
int nQ = ++top; // 此时Q代表xbca和bca两个串,他们的right集合(出现位置完全相同)
len[nQ] = len[p] + ; // 此时多出来了一个单独的bca,我们新建一个节点new Q来表示
fail[nQ] = fail[Q]; // new Q表示bca,fail指针与之前*bca的指针相同。
fail[Q] = fail[np] = nQ; // 而Q现在只表示xbca一个串了,fail指向bca
memcpy(tr[nQ], tr[Q], sizeof(tr[Q])); // new p的fail指向bca,而不是更长的*bca,是因为之前跳fail的时候停在了p,
while(tr[p][f] == Q) { // 这就表明最后的bca之前的一个字符不可能跟别的bca相同,不为x。否则p就是xbc
tr[p][f] = nQ; // new Q bca本来就是Q中的一部分,现在分离出来,就继承了所有出边
p = fail[p]; // p转移到Q,说明p比最短的Q(new Q)短。所以p和以上的所有出边都不会转移到Q,因为有最后那一个新加的bca
} // 它前方不为x,所以bc呀c呀都不会直接到xbca上去
} //
} //
return; //
} //

假装把插入搞懂了......

关于排序,我的理解是这样的。

首先搞出一个桶并统计前缀和。这样长度为i的那些点的排名就是bin[i - 1] + 1 ~ bin[i]

这些点之间是没有相互关系的,所以每次出来一个长度为i的点,就挑一个排名给它,我们挑的是bin[i]

之后bin[i]--,表示这个排名已经被用掉了,之后剩余的排名从新的bin[i]开始。

注意虽然一号点长度是0但是三个循环都是从1开始,并不会出现问题。

用一道例题加深理解。

例题A:hihocoder1465

题意:给定s,多次询问t的所有循环同构串在s中出现的次数。

解:对s建立sam。循环同构的处理方法是把串复制一遍,有点像环形区间DP。

在sam上面跑tt,如果长度比t长了,就跳fail。当前长度等于t时统计答案。每个节点只会被加一次,所以用vis数组表示。

注意,转移的时候长度+1,跳fail的时候长度变为len。

 #include <cstdio>
#include <algorithm>
#include <cstring> typedef long long LL;
const int N = ; int tr[N][], len[N], fail[N], bin[N], topo[N], cnt[N];
int last, top;
char s[N], pp[N];
bool vis[N]; inline void init() {
top = last = ;
return;
} inline void insert(char c) {
int f = c - 'a';
int p = last, np = ++top;
last = np;
cnt[np] = ;
len[np] = len[p] + ;
while(p && !tr[p][f]) {
tr[p][f] = np;
p = fail[p];
}
if(!p) {
fail[np] = ;
}
else {
int Q = tr[p][f];
if(len[Q] == len[p] + ) {
fail[np] = Q;
}
else {
int nQ = ++top;
len[nQ] = len[p] + ;
fail[nQ] = fail[Q];
fail[Q] = fail[np] = nQ;
memcpy(tr[nQ], tr[Q], sizeof(tr[Q]));
while(tr[p][f] == Q) {
tr[p][f] = nQ;
p = fail[p];
}
}
}
return;
} inline void sort() {
for(int i = ; i <= top; i++) {
bin[len[i]]++;
}
for(int i = ; i <= top; i++) {
bin[i] += bin[i - ];
}
for(int i = ; i <= top; i++) {
topo[bin[len[i]]--] = i;
}
return;
} inline void count() {
for(int a = top; a >= ; a--) {
int x = topo[a];
cnt[fail[x]] += cnt[x];
}
return;
} inline void solve() {
scanf("%s", pp + );
int n = strlen(pp + );
for(int i = ; i <= n; i++) {
pp[n + i] = pp[i];
}
LL ans = ;
int now = , p = ;
for(int i = ; i <= n * ; i++) {
int f = pp[i] - 'a';
while(p && !tr[p][f]) {
p = fail[p];
now = len[p];
}
if(tr[p][f]) {
p = tr[p][f];
now++;
}
else {
p = ;
}
while(len[fail[p]] >= n) {
p = fail[p];
now = len[p];
}
//printf("i = %d \n", i);
if(now >= n && !vis[p]) {
ans += cnt[p];
vis[p] = ;
//printf("ans += %d \n", cnt[p]);
}
}
printf("%lld\n", ans);
return;
} int main() {
scanf("%s", s + );
init();
int n = strlen(s + );
for(int i = ; i <= n; i++) {
insert(s[i]);
}
sort();
count();
int T;
scanf("%d", &T);
while(T--) {
solve();
if(T) {
memset(vis, , sizeof(vis));
}
} return ;
}

AC代码

各种例题:

弦论  生成魔咒  品酒大会  差异  优秀的拆分


广义后缀自动机:

对trie构建后缀自动机。参考资料  资料B

对多个串,常见的两种方法是每次last归一和添加分隔符。

正确的方法是每次last归一,然后把insert魔改一下。

大概长这样:

 inline int split(int p, int f) {
int Q = tr[p][f], nQ = ++tot;
len[nQ] = len[p] + ;
fail[nQ] = fail[Q];
fail[Q] = nQ; // 这里不用管fail[np]
memcpy(tr[nQ], tr[Q], sizeof(tr[Q]));
while(tr[p][f] == Q) {
tr[p][f] = nQ;
p = fail[p];
}
return nQ;
} inline int insert(int p, char c) { // 直接传入p,返回值是last,下一次当p用。
int f = c - 'a';
if(tr[p][f]) { //如果有转移边了(别的串上有)
int Q = tr[p][f];
if(len[Q] == len[p] + ) { // 判断是否表示这一个,否则新建节点。
return Q;
}
return split(p, f); // split,分离出这个串。
}
int np = ++tot;
len[np] = len[p] + ;
while(p && !tr[p][f]) {
tr[p][f] = np;
p = fail[p];
}
if(!p) {
fail[np] = ;
}
else {
int Q = tr[p][f];
if(len[Q] == len[p] + ) {
fail[np] = Q;
}
else {
fail[np] = split(p, f); // 这里直接调用分离函数即可。
}
}
return np;
}

例题:

字符串  bzoj2780  找相同字符  bzoj5137  你的名字

Suffix Automaton的更多相关文章

  1. 【文文殿下】后缀自动机(Suffix Automaton,SAM)学习笔记

    前言 后缀自动机是一个强大的数据结构,能够解决很多字符串相关的(String-related)问题. 例如:他可以查询一个字符串在另一个字符串中出现的所有子串,以及查询一个字符串中本质不同的字符串的个 ...

  2. JDOJ 2939: Suffix Automaton 广义后缀自动机_统计子串

    建立广义后缀自动机,对每个节点都建立各自的 $Parent$ 数组. 这样方便统计,不会出现统计错误. 考虑新加入一个字符. 1 这条转移边已经存在,显然对答案没有贡献. 2 这条转移边不存在,贡献即 ...

  3. cf448B Suffix Structures

    B. Suffix Structures time limit per test 1 second memory limit per test 256 megabytes input standard ...

  4. Codeforces Round #256 (Div. 2) B Suffix Structures

    Description Bizon the Champion isn't just a bison. He also is a favorite of the "Bizons" t ...

  5. Codeforces Round #256 (Div. 2) B. Suffix Structures(模拟)

    题目链接:http://codeforces.com/contest/448/problem/B --------------------------------------------------- ...

  6. SAM初探

    SAM,即Suffix Automaton,后缀自动机. 关于字符串有很多玩法,有很多算法都是围绕字符串展开的.为什么?我的理解是:相较于数字组成的序列,字母组成的序列中每个单位上元素的个数是有限的. ...

  7. hihocoder SAM基础概念

    后缀自动机一·基本概念 时间限制:10000ms 单点时限:1000ms 内存限制:256MB 描述 小Hi:今天我们来学习一个强大的字符串处理工具:后缀自动机(Suffix Automaton,简称 ...

  8. ZJOI 游记

    在备战YZ提前招生考时去ZJOI玩了趟,ZJ果然人才辈出= =神犇讲课各种神听不懂啊orz day 0 Mon. 上午在AB班愉快地玩耍,下午就去HZ了. HZ真热啊... 学军也是节约= =空调都不 ...

  9. 后缀自动机(SAM)

    *在学习后缀自动机之前需要熟练掌握WA自动机.RE自动机与TLE自动机* 什么是后缀自动机 后缀自动机 Suffix Automaton (SAM) 是一个用 O(n) 的复杂度构造,能够接受一个字符 ...

随机推荐

  1. Error occurred during initialization of VM Could not reserve enough space for 2097152KB object heap

    ionic build Android后的报错问题 ionic 升级了splashscreen和statusbar的插件后,执行ionic build android会一直报打包错误.原因是过低的An ...

  2. Javascript数组系列五之增删改和强大的 splice()

    今天是我们介绍数组系列文章的第五篇,也是我们数组系列的最后一篇文章,只是数据系列的结束,所以大家不用担心,我们会持续的更新干货文章. 生命不息,更新不止! 今天我们就不那么多废话了,直接干货开始. 我 ...

  3. java8 快速实现List转map 、分组、过滤等操作

    利用java8新特性,可以用简洁高效的代码来实现一些数据处理. 定义1个Apple对象: public class Apple { private Integer id; private String ...

  4. Python爬虫之正则表达式(3)

    # re.sub # 替换字符串中每一个匹配的子串后返回替换后的字符串 import re content = 'Extra strings Hello 1234567 World_This is a ...

  5. DRF 序列化器-Serializer (2)

    作用 1. 序列化,序列化器会把模型对象转换成字典,经过response以后变成json字符串 2. 完成数据校验功能 3. 反序列化,把客户端发送过来的数据,经过request以后变成字典,序列化器 ...

  6. web框架开发-Django模型层(1)之ORM简介和单表操作

    ORM简介 不需要使用pymysql的硬编码方式,在py文件中写sql语句,提供更简便,更上层的接口,数据迁移方便(有转换的引擎,方便迁移到不同的数据库平台)…(很多优点),缺点,因为多了转换环节,效 ...

  7. centos查看系统信息命令

    1.cd - :返回上次所在的目录 2.查看系统版本 cat /etc/redhat-release 3.查看linux内核版本1)cat /proc/version 2) uname -a3) un ...

  8. koa2源码解读及实现一个简单的koa2框架

    阅读目录 一:封装node http server. 创建koa类构造函数. 二:构造request.response.及 context 对象. 三:中间件机制的实现. 四:错误捕获和错误处理. k ...

  9. Linux内存都去哪了:(1)分析memblock在启动过程中对内存的影响

    关键词:memblock.totalram_pages.meminfo.MemTotal.CMA等. 最近在做低成本方案,需要研究一整块RAM都用在哪里了? 最直观的的就是通过/proc/meminf ...

  10. .Net Core应用框架Util介绍(一)

    距离上次发文,已经过去了三年半,这几年技术更新节奏异常迅猛,.Net进入了跨平台时代,前端也被革命性的颠覆. 回顾 2015年,正当我还沉迷于JQuery + EasyUi的封装时,突然意识到技术已经 ...