后缀自动机

后缀自动机是一种确定性有限状态自动机, 它可以接收字符串\(s\)的所有后缀.

构造, 性质

翻译自毛子俄罗斯神仙的博客, 讲的很好

后缀自动机详解 - DZYO的博客 - CSDN博客

下面是一些note:

定义

  • 对于字符串\(s\)的子串\(t\), \(endpos(t)\) (或者 \(right(t)\) ) 表示t在s中出现位置的右端点的集合.

    • \(endpos\)互不相交.
    • 有相同 \(endpos\) 集合的字符串构成一个等价类.
    • 对于每个等价类, 包含的字符串长度为\([len(p), maxlen(p)]\) , 是一个连续的区间.
  • 后缀自动机的节点 \(p\) 代表一个 \(endpos\) 相同的子串的集合.
  • 对于后缀自动机的节点 \(p\), \(parent(p)\) (或者 \(link(p)\) ) 表示p在不同等价类中的最长后缀.
    • \(parent\) 形成一棵树关系.
    • \(len(p) = maxlen(parent(p)) +1\)

构建 && 状态数/转移数线性证明

上面的blog已经写的很好了, 我就不重写一遍了:P

示意图

字符串 ab:

其中 * 代表终止节点, 虚箭头表示 \(fa(p)\).

字符串 abb:

字符串 bba 的后缀树 (见后), 即字符串 abb 的前缀树/后缀自动机的 parent 树:

Code

const int nsz=1e6+50,ndsz=2*nsz,csz=27;

ll n;
char s[nsz]; //sam
//p.l means maxlen(p)
struct tnd{int ch[csz],l,fa,cnt;}sam[ndsz];
#define ch(p,c) sam[p].ch[c]
#define fa(p) sam[p].fa
int ps=1,las=1;
int cnt[ndsz],c[ndsz],seq[ndsz];
void insert(int c){
int p=las;
las=++ps,sam[las].l=sam[p].l+1,cnt[las]=1;
for(;p&&ch(p,c)==0;p=fa(p))ch(p,c)=las;
if(p==0)fa(las)=1;
else{
int q=ch(p,c);
if(sam[q].l==sam[p].l+1)fa(las)=q;
else{
int q1=++ps;
sam[q1]=sam[q],sam[q1].l=sam[p].l+1,fa(q)=fa(las)=q1;
for(;p&&ch(p,c)==q;p=fa(p))ch(p,c)=q1;
}
}
}
void build(){
rep(i,1,n)insert(s[i]-'a');
}
struct te{int t,pr;}edge[ndsz];
int hd[ndsz],pe=1;
void adde(int f,int t){edge[++pe]=(te){t,hd[f]};hd[f]=pe;} void buildtr(){
rep(i,2,ps)adde(fa(i),i);
} void gettp(){ //topo sort
rep(i,1,ps)++c[sam[i].l];
rep(i,1,ps)c[i]+=c[i-1];
rep(i,1,ps)seq[c[sam[i].l]--]=i;
} void match(char *s,int n){
int cur=1,l=0;
rep(i,1,n){
if(ch(cur,s[i])){++l,cur=ch(cur,s[i]);}
else{
while(cur&&ch(cur,s[i])==0)cur=fa(cur);
if(cur==0)l=0,cur=1;
else l=sam[cur].l+1,cur=ch(cur,s[i]);
}
}
}

后缀树

后缀树是对字符串 \(S\) 的所有后缀建立的trie树, 同样可以识别 \(S\) 的所有后缀.

为了节省空间, 可以利用虚树的思想. 我们把只有一个子节点的节点压缩到它的父亲, 也就是说, 把没有分叉的一条链压缩成一条边.

显然, 这样建成的后缀 trie 只会保留每个后缀的终止节点('\0'), 和他们的lca. 这两者数量都是 \(O(n)\) 的, 因此状态总数也为 \(O(n)\) .

同时, 字符串 \(S\) 后缀自动机的parent树等价于 \(S\) 逆序 \(S'\) 的后缀树, 可以称作前缀树. 证明见[3].

几个关键问题

在后缀自动机上走路的时间复杂度

23333

就是说对字符串 \(S\) 建立后缀自动机, 然后将字符串 \(T\) 从起点走转移边, 如果没有转移边则跳parent指针. 这样可以求出 \(S\) 与 \(T\) 的每一个公共子串.

记当前 \(S\) 与 \(T\) 的匹配长度为 \(l\). 对于每一次转移, \(l\) 会加 \(1\); 对于跳parent指针, \(l\) 会减少, 而 \(l\) 总的减少不会超过 \(|T|\). 因此总时间复杂度为 \(O(|T|)\).

事实上, 对于insert(c)的均摊时间复杂度的分析是类似的.

代码

//l : max len of current matched string
//p : current state
void match(char *s,int n){
int cur=1,l=0;
rep(i,1,n){
if(ch(cur,s[i])){++l,cur=ch(cur,s[i]);}
else{
while(cur&&ch(cur,s[i])==0)cur=fa(cur);
if(cur==0)l=0,cur=1;
else l=sam[cur].l+1,cur=ch(cur,s[i]);
}
}
}

拓扑序

from [2]

SAM 中的 DAWG 满足一个性质,如果有一条转移边 \(u \rightarrow v\) ,则一定有 \(|\max(u)| < |\max(v)|\)。类似的,如果 \(\text{next}(v) = u\),也有 \(|\max(u)| < |\max(v)|\)。所以,按照每个节点记录的 max 长度排序,可以同时得到 DAWG 和前缀树的拓扑序。

使用桶排序, 那么时间复杂度是\(O(n)\).

代码

void gettp(){ //topo sort
rep(i,1,ps)++c[sam[i].l];
rep(i,1,ps)c[i]+=c[i-1];
rep(i,1,ps)seq[c[sam[i].l]--]=i;
}

这样我们就可以在SAM上进行动态规划.

每个节点代表字符串个数

由定义可知,

节点 \(p\) 代表字符串个数 $ = maxlen(p)-len(p)+1 = maxlen(p)-maxlen(parent(p))$.

同时, 节点 \(p\) 代表字符串个数 = 起点到节点 \(p\) 路径数.

求endpos集合

记非拷贝而来的节点为实节点, 否则为虚节点.

当实节点为第 \(t\) 个字符加入时建立的时, 它的endpos集合中显然有 \(t\), 并且它是endpos集合中有 \(t\) 的节点中maxlen最大的.

那么它的parent节点显然也包含\(t\), 直接跳parent()即可.

这时我们可以O(n)的求出endpos集合的大小:

  • 对于不是拷贝的节点, cnt设为1; 拷贝而来的节点, cnt设为0.
  • 在parent树上dp, \(cnt_p+=\sum_{parent(v)=p} cnt_v\).
  • \(cnt_p\) 表示这个节点endpos集合大小, 也就是在字符串中的出现次数.

如果要求endpos集合, 需要可合并数据结构 (线段树/set/堆等). 利用可持久化线段树合并 ([模板] 线段树合并) 可以求出所有点的 endpos 集合.

最小表示法

建立\(S+S\)的后缀自动机, 从起点开始, 每次走字典序最小的转移, 并记录.

转移 \(|S|\) 次之后, 得到的字符串即为 \(S\) 的最小表示.

后缀自动机的用法

  1. 拓扑序 dp (自动机上/parent树上)
  2. 利用 len 函数和 endpos 集合 (dp, 线段树合并等)
  3. 利用 parent 树
    • 树上的技巧: lca, 倍增, 点分治, 树剖, LCT
    • dp(自上向下, 自下向上, 双重, 倍增)
  4. 利用自动机的性质 (转移等)

参考资料

  1. 后缀自动机详解 - DZYO的博客 - CSDN博客
  2. 后缀自动机学习笔记 | Menci's Blog
  3. [开新坑]对于后缀自动机的一些理解 - Shinbokuow - 不试着去思考的话,不就已经死去了吗
  4. 后缀三兄弟之三——后缀自动机(附广义后缀自动机,子序列自动机) - litble的成(tui)长(fei)史 - CSDN博客
  5. 算法学习:后缀自动机转后缀树转后缀数组 - maxtir的博客 - CSDN博客

[模板] 后缀自动机&&后缀树的更多相关文章

  1. 模板—字符串—后缀自动机(后缀自动机+线段树合并求right集合)

    模板—字符串—后缀自动机(后缀自动机+线段树合并求right集合) Code: #include <bits/stdc++.h> using namespace std; #define ...

  2. BZOJ3413: 匹配(后缀自动机 线段树合并)

    题意 题目链接 Sol 神仙题Orz 后缀自动机 + 线段树合并... 首先可以转化一下模型(想不到qwq):问题可以转化为统计\(B\)中每个前缀在\(A\)中出现的次数.(画一画就出来了) 然后直 ...

  3. cf666E. Forensic Examination(广义后缀自动机 线段树合并)

    题意 题目链接 Sol 神仙题Orz 后缀自动机 + 线段树合并 首先对所有的\(t_i\)建个广义后缀自动机,这样可以得到所有子串信息. 考虑把询问离线,然后把\(S\)拿到自动机上跑,同时维护一下 ...

  4. 洛谷P2178 [NOI2015]品酒大会(后缀自动机 线段树)

    题意 题目链接 Sol 说一个后缀自动机+线段树的无脑做法 首先建出SAM,然后对parent树进行dp,维护最大次大值,最小次小值 显然一个串能更新答案的区间是\([len_{fa_{x}} + 1 ...

  5. BZOJ1396: 识别子串(后缀自动机 线段树)

    题意 题目链接 Sol 后缀自动机+线段树 还是考虑通过每个前缀的后缀更新答案,首先出现次数只有一次,说明只有\(right\)集合大小为\(1\)的状态能对答案产生影响 设其结束位置为\(t\),代 ...

  6. [Luogu5161]WD与数列(后缀数组/后缀自动机+线段树合并)

    https://blog.csdn.net/WAautomaton/article/details/85057257 解法一:后缀数组 显然将原数组差分后答案就是所有不相交不相邻重复子串个数+n*(n ...

  7. 【BZOJ-1396&2865】识别子串&字符串识别 后缀自动机/后缀树组 + 线段树

    1396: 识别子串 Time Limit: 10 Sec  Memory Limit: 162 MBSubmit: 312  Solved: 193[Submit][Status][Discuss] ...

  8. 洛谷P4493 [HAOI2018]字串覆盖(后缀自动机+线段树+倍增)

    题面 传送门 题解 字符串就硬是要和数据结构结合在一起么--\(loj\)上\(rk1\)好像码了\(10k\)的样子-- 我们设\(L=r-l+1\) 首先可以发现对于\(T\)串一定是从左到右,能 ...

  9. luogu5212/bzoj2555 substring(后缀自动机+动态树)

    对字符串构建一个后缀自动机. 每次查询的就是在转移边上得到节点的parent树中后缀节点数量. 由于强制在线,可以用动态树维护后缀自动机parent树的子树和. 注意一个玄学的优化:每次在执行连边操作 ...

随机推荐

  1. cookie特殊字符在游览器被转义

    环境:vue2.x axios 1.如果只是前端自己用,那么可以用 encodeURIComponent(string) 存 ,用decodeURIComponent(string)取. 2.遇到一种 ...

  2. 在ubuntu16.04中初次体验.net core 2.0

    .net core运行在Linux中的例子.文章已经很多了,看了一些之后也想体验一下,顺便记录一下…… 环境:win10 1709.它内置的Linux子系统(这里安装的是Ubuntu 16.04) 一 ...

  3. 树上倍增求LCA及例题

    先瞎扯几句 树上倍增的经典应用是求两个节点的LCA 当然它的作用不仅限于求LCA,还可以维护节点的很多信息 求LCA的方法除了倍增之外,还有树链剖分.离线tarjan ,这两种日后再讲(众人:其实是你 ...

  4. #WEB安全基础 : HTML/CSS | 0x9美丽的饮料店

    我带着你,你带着钱,咱们去喝点饮料吧. 老板久仰你的大名,请你帮忙设计一个网站宣传他的饮料店 你要制定一个完美的方案还需要多学点东西 我先帮你设计一下 这是存放网站的文件夹 这是根目录   这是abo ...

  5. float与double

    对数值类型的细节了解在大学里就是一带而过,自己始终也没好好看过.这是在csdn上看到的一篇文章,挺好的,记录下来. https://blog.csdn.net/Demon__Hunter/articl ...

  6. Android为TV端助力 完全解析模拟遥控器按键

    public class VirturlKeyPadCtr { private static Instrumentation mInstrumentation; public static void ...

  7. Vue组件的is具体用法

    1.为什么要使用is 在vue的官网组件部分中,有明确的描述:当使用 DOM 作为模板时 (例如,使用 el 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因 ...

  8. LNMP时,出现502 Bad Gateway的错误提示

    因为工作需要,要在ubuntu中安装LNMP环境,在这里,php是最新版本php7.1.一切都进展得很顺利,安装完成后,在浏览器中输入http://127.0.0.1/info.php,出现了502 ...

  9. LeetCode算法题-Max Consecutive Ones(Java实现)

    这是悦乐书的第242次更新,第255篇原创 01 看题和准备 今天介绍的是LeetCode算法题中Easy级别的第109题(顺位题号是485).给定二进制数组,找到此数组中连续1的最大数量.例如: 输 ...

  10. Android Studio教程08-与其他app通信

    目录 1.向另外一个应用发送用户 1.1. 构建隐含Intent 1.2. 验证是否存在接收Intent的应用 1.3. 启动具有Intent的Activity 2. 获取Activity的结果响应 ...