后缀自动机

先上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. Android八门神器(一): OkHttp框架源码解析

    HTTP是我们交换数据和媒体流的现代应用网络,有效利用HTTP可以使我们节省带宽和更快地加载数据,Square公司开源的OkHttp网络请求是有效率的HTTP客户端.之前的知识面仅限于框架API的调用 ...

  2. 缓存ABC

    缓存ABC Intro 缓存是一种比较常见的用来将提高系统性能的方式.从线程缓存.进程缓存.到内存缓存再到分布式缓存再到CDN,都是属于缓存的范畴. 缓存的本质是空间换时间以提高读的效率,牺牲一些内存 ...

  3. linux 硬盘满了如何处理(转)

    linux 硬盘满了如何处理 事件源于在服务器运行一个脚本程序… 好好的脚本突然报错,还以为脚本出现问题了.细看报错原因(具体报错信息已经忘记了),是没有可用空间.从没遇见过这个情况,怎么办呢? 一. ...

  4. Error:Cannot run program "svn" (in directory "E:demo\Hello"): CreateProcess error=2,

    file-->settings-->version controller --> subversion

  5. js 点击复制代码 window.clipboardData.setData

    var v = document.getElementById("forcopy").value; window.clipboardData.setData('text',v); ...

  6. echarts之legend-改变图例的图标为自定义图片

    legend:{ show:true, orient:'horizontal', borderColor:'#df3434', borderWidth:2, data:[ { name:'蒸发量', ...

  7. Jenkins插件安装

    默认的Jenkins平台继有的功能是有限的,插件为Jenkins提供了各种扩展功能,有了插件,Jenkins就会变得更加强大,这里推荐两个不错的插件: 点击菜单栏中“系统管理”--“插件管理”--“A ...

  8. python小白——进阶之路——day4天-———流程控制while if循环

    # ### 代码块: 以冒号作为开始,用缩进来划分作用域,这个整体叫做代码块 if 5 == 5: print(1) print(2) # 注意点: 要么全部使用4个空格,要么全部使用1个缩进 ,这样 ...

  9. C#后台发布

    测试环境:... 生产环境:发布--文件系统--Release--本地文件--成功copy服务器上:(第一次发布vue项目前后端copy顺序,避免一些bug)

  10. php中一些容易混淆的函数总结

    在我们日常PHP开发中,经常会使用一些函数完成相关操作,但是有些函数功能相近,很容易混淆,再次总结一下 1. __DIR__  &&  getcwd() 看官方解释: getcwd: ...