模板:后缀自动机(SAM)

34 篇文章 0 订阅
8 篇文章 0 订阅

所谓后缀自动机,就是通过后缀建立的自动机

(逃)
请允许我先介绍一下后缀家族:
在这里插入图片描述
(又逃)

前言

OI生涯目前为止学习的最为难以理解的算法,没有之一。
到现在也没有完全的理解。
qwq

概念

定义:

后缀 i i i :字符串 s s s i i i 结尾的后缀(前缀同理)
e n d p o s ( x ) endpos(x) endpos(x) 字符串 x x x s s s 中出现的结尾位置的集合
等价类:若 e n d p o s ( u ) = e n d p o s ( v ) endpos(u)=endpos(v) endpos(u)=endpos(v),我们就称 u u u v v v 属于同一个等价类

不难发现,对于 s s s 的一个子串 x = s l . . . r x=s_{l...r} x=sl...r,都存在一个位置 p ∈ ( l , r ] p\in (l,r] p(l,r],满足对于 l ≤ i ≤ p l\le i\le p lip x x x 的后缀 i i i 都与 x x x 属于同一个等价类,而对于 p < i ≤ r p<i\le r p<ir,后缀 i i i x x x 都不属于一个等价类。
我们称满足这个性质的 p p p l i n k ( x ) link(x) link(x)

SAM是一个字符转移边组成的DAG,每条从根结点出发的路径都唯一对应 s s s 的一个子串,在SAM中,每个结点都对应着一个等价类的集合(也就是从根结点到该结点的路径所代表的字符串的集合,这些字符串必然由一个最长的字符串和它的一些连续的后缀组成),一个结点的 l i n k link link 定义为该结点对应的最长字符串的 l i n k link link

请务必确保你理解了上面这段话

构建

现在考虑如何构建出SAM
使用增量法,当前加入一个新的字符串 c c c
设上一个加入的结点为 l s t lst lst,当前加入结点为 c u r cur cur
设一个结点代表的最长字符串的长度为 l e n len len

首先,令 l e n ( c u r ) ← l e n ( l s t ) + 1 len(cur)\gets len(lst)+1 len(cur)len(lst)+1
然后从 l s t lst lst 沿着 l i n k link link 不断往上跳,直到跳到某个有 c c c 的转移边或者跳到根为止,沿途把 c c c 的转移边全部赋值成 c u r cur cur

situation 1

若到根了还没有 c c c 的转移边:说明整个字符串还没有出现过 c c c,直接把 l i n k ( c u r ) link(cur) link(cur) 赋值成根即可

否则,设跳到了结点 p p p p p p c c c 转移边为 q q q
由于一直跳的是

situation 2

l e n ( q ) = l e n ( p ) + 1 len(q)=len(p)+1 len(q)=len(p)+1:这两个结点在原串上就是相邻的,直接令 l i n k ( c u r ) = l i n k ( q ) link(cur)=link(q) link(cur)=link(q) 即可

situation 3

l e n ( q ) ≠ l e n ( p ) + 1 len(q)\ne len(p)+1 len(q)=len(p)+1:这两个结点在原串上不是相邻的,此时若按照情况2的处理方法,会使SAM上出现不应该出现的前缀,所以我们应该分裂出一个结点 p p pp pp,继承所有 q q q 的信息, l e n ( p p ) ← l e n ( p ) + 1 len(pp)\gets len(p)+1 len(pp)len(p)+1,并把 q q q c u r cur cur l i n k link link 全指向 p p pp pp,再一路往上把本来连向 q q q 的转移连向 p p pp pp

代码

void ins(int c){
  c-='a';
  int cur=++tot,p=lst;lst=tot;
  st[cur].len=st[p].len+1;siz[cur]=1;
  for(;p&&!st[p].tr[c];p=st[p].fa) st[p].tr[c]=cur;
  if(!st[p].tr[c]) st[cur].fa=1;
  else{
    int q=st[p].tr[c];
    if(st[q].len==st[p].len+1) st[cur].fa=q;
    else{
      int pp=++tot;st[pp]=st[q];
      st[pp].len=st[p].len+1;
      st[q].fa=st[cur].fa=pp;
      for(;p&&st[p].tr[c]==q;p=st[p].fa) st[p].tr[c]=pp;
      return;
    }
  }
}

应用

求 endpos 集合大小

定义 s i z x siz_x sizx 为结点 x x x 的等价类集合中 e n d p o s endpos endpos 的数目。(也就是出现次数)
那么根据定义,有:
s i z x = ∑ s ∈ s o n x s i z s + [ x ∈ S ] siz_x=\sum_{s\in son_x} siz_s+[x\in S] sizx=ssonxsizs+[xS]
其中 S S S 是每次插入的终点集合
dfs或者拓扑实现均可

int cnt[N],id[N];
void calc(){
  for(int i=1;i<=tot;i++) ++cnt[st[i].len];
  for(int i=1;i<=n;i++) cnt[i]+=cnt[i-1];
  for(int i=tot;i>=1;i--) id[cnt[st[i].len]--]=i;
  for(int i=tot;i>=1;i--) siz[st[id[i]].fa]+=siz[id[i]];
  return;
}

求本质不同子串数

就是在自动机上的走法种类呗。
那么就有:
s u m x = ∑ s = t r x , c s u m s + 1 sum_x=\sum_{s=tr_{x,c}}sum_s+1 sumx=s=trx,csums+1

Thanks for reading!

后缀树

本身也是一个大算法,但是可以通过反串建SAM偷懒。
复杂度为 O ( n C ) O(nC) O(nC)

解析

对反串建出后缀自动机,其 f a i l fail fail 树即为所求的后缀树。
p l x pl_x plx,为 x x x 节点对应的任意一个出现位置,那么 f a i l x → x fail_x\to x failxx 这条边上对应的字符串就是 s ( p l x + l e n f a i l x , p l x + l e n x − 1 ) s(pl_x+len_{fail_x},pl_x+len_x-1) s(plx+lenfailx,plx+lenx1)
结合 f a i l fail fail 的定义应该不难得到。

后缀树的性质:

  1. 根到叶子的路径和所有后缀一一对应。
  2. 所有非根节点都至少有两个子节点。
  3. L C P ( i , j ) LCP(i,j) LCP(i,j) 就是两个位置对应节点 lca 的最长长度。

在解决字典序相关问题时较为常用。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值