后缀自动机(SAM)小记
介绍
简单来说,就是使用一个 \(DAG\) 以及一棵树维护一个字符串所有子串(压缩的)信息。
其中 \(DAG\) 的点称为状态。
endpos
个人认为 \(SAM\) 的核心在于 \(endpos\)。
子串(终点)在原串出现的下标集合称为 \(endpos\) 集合。
例如,对于原串 \(\texttt{aabaaba}\),\(endpos(\texttt{ab}) = \{3, 6\}\)。
性质
\(endpos\) 有如下性质:
-
对于任意子串 \(a, b\),二者 \(endpos\) 的关系必为:
-
\(endpos(a) \subseteq endpos(b)\)
-
\(endpos(a)\cap endpos(b) = \empty\)
之一。
-
-
\(endpos\) 相同的两个子串属于一个等价类。(这意味着:\(SAM\) 中每个状态会对应 \(endpos\) 相同的若干个子串)
-
原串中 \(endpos\) 对应的等价类数量级为 \(O(N)\)。
-
对于一个状态 \(st\) 及任意的 \(longest(st)\) 的后缀 \(s\) ,若有:$|shortest(st)|≤|s|≤|longsest(st)| $,则 \(s\in substrings(st)\) 。
记 \(|shortest(st)|\) 为 \(minlen(st)\),\(|longest(st)|\) 为 \(len(st)\)。
后缀链接(link)
由上面的 \(endpos\) 性质二可知,每个 \(endpos\) 集合可以被划分为若干个集合(每个集合当然可以同样地划分下去),这样我们就可以按照划分的过程得到一个树形的结构,这被称为 \(link\) 树。
对于一个状态 \(st\),我们有:\(minlen(st) = len(link(st)) + 1\)。
后缀自动机的建立
鉴于会涉及代码,先放模板题链接:
https://www.luogu.com.cn/problem/P3804
这是一个在线的过程,就是按照每次加一个点(对应字符串中加一个字符)然后相应地连边构造的。
建立过程见代码注释部分。
每个状态维护了:
- \(link\) 树的父节点 \(fa\)
- 该状态对应的等价类中最长的子串长 \(len\)。
- \(DAG\) 中连接的字符集所到的状态 \(ch[]\)
性质
- 起点开始沿蓝边任意路径所到的状态为该状态的子串。
- \(fa(st)\) 的任意子串一定是 \(st\) 任意子串的后缀。
更多性质见下图:
(这是 \(\texttt{aabbabd}\) 的后缀自动机)
参考代码
树上计数。
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+5, M=N<<1;
#define int long long
struct Edge{
int to, next;
}e[M];
int h[N], idx;
void add(int u, int v){
e[idx].to=v, e[idx].next=h[u], h[u]=idx++;
}
char str[N];
struct Node{
int len, fa; // longest length of state, link
int ch[26];
}node[N];
int tot=1, last=1; // 初始的根
int f[N];
void ins(int c){
int p=last, np=last=++tot; // np 代表新插入的点(状态)
f[tot]=1; // 新状态的大小(即子串个数)
node[np].len=node[p].len+1; // 新插入字符后对应的最长子串当然比前一个状态的长 1。
for(; p && !node[p].ch[c]; p=node[p].fa) node[p].ch[c]=np; // 跳 link,没有 c 儿子的状态连边。
if(!p) node[np].fa=1; // 跳到根则将 np 的 link 指向根(因为 c 没有出现过)
else{
int q=node[p].ch[c]; // q 为第一个有 c 儿子的状态的 c 儿子
if(node[q].len==node[p].len+1) node[np].fa=q; // 找到正好接受新串后缀的状态
else{
int nq=++tot; // 开新点
node[nq]=node[q], node[nq].len=node[p].len+1;
node[q].fa=node[np].fa=nq;
for(; p && node[p].ch[c]==q; p=node[p].fa) node[p].ch[c]=nq;
}
}
}
int res=0;
void dfs(int u){
for(int i=h[u]; ~i; i=e[i].next){
int go=e[i].to;
dfs(go);
f[u]+=f[go];
}
if(f[u]>1) res=max(res, f[u]*node[u].len);
}
signed main(){
scanf("%s", str);
for(int i=0; str[i]; i++) ins(str[i]-'a');
memset(h, -1, sizeof h);
for(int i=2; i<=tot; i++) add(node[i].fa, i);
dfs(1);
cout<<res<<endl;
return 0;
}