SAM & 广义 SAM & SA 学习笔记
SAM
定理
SAM 由 parent 树与一张 DAG 构成,他们共用点集。
\(endpos(s)\) 表示 \(s\) 出现的所有位置上最后一个字符所处位置的集合。
SAM 中 DAG 上每条路径对应原串上的一个子串,一个子串也与其对应。
在 SAM 的 DAG 上到达一个点的所有子串的 endpos 相同。
一个节点上储存的最长的串为 \(len_i\)。
在 parent 树上点 \(u\) 的父亲节点是与 \(u\) endpos 不相同的 \(len_{u}\) 的最长真后缀。
不难发现点 \(u\) 的 endpos 包含所有满足 \(v \in son_{u}\) 的 \(v\) 的 endpos,不难发现一个点 \(u\) 存储的串就是所有长度小于等于自己并大于其父节点的所有后缀。
构建
增量法
已知串 \(S\) 的 SAM,如何构造 \(S+c\) 的 SAM?
找到一个节点 \(u\) 使得 \(len_{u} = |S|\)。
考虑在 parent 树上 \(u\) 的所有祖先包含 \(S\) 的所有真后缀。
在这条路径上显然应当是满足所有深度大于 \(t\) 的点没有 \(c\) 这条出边。
新建一个点 \(p\) 使得其 \(len_{p} = len_{u} + 1\) 等于说是包括了 \(c\)。
让所有没有 \(c\) 这条出边的点向 \(p\) 连一条 \(c\) 的边。
到此为止 DAG 的维护就完成了。
接下来考虑维护 parent 树与 endpos。
大力分讨。
-
假设所有点都向 \(p\) 连边,就说明 \(c\) 根本就没出现过,那么其所有后缀的 endpos 都是 \(len_{p}\) 那么将 \(p\) 在 parent 树上的父亲改为根即可。
-
假设存在点向 \(p\) 连边,找到第一个存在向 \(c\) 的边的点 \(x\),假设其向 \(c\) 的边指向 \(q\),假若 \(len_{x} + 1 = len_{q}\),直接令 \(fa_{p} = q\)。因为 \(p\) 最长只能跳这来。
-
假若 \(len_{x} + 1 \not = len_{q}\) 那就有点麻烦,那么此时需要分裂 \(q\),假设你分裂出了新点 \(nq\),直接令 \(SAM_{nq} = SAM_{q}\),然后手动将 \(SAM_{nq,len} = SAM_{x,len} + 1\),然后让 \(SAM_{q,fa} = nq\),然后让 \(SAM_{p,fa} = nq\)。但是此时 DAG 的性质又似了,我们需要把所有 \(x\) 的祖先当中原本指向 \(q\) 的点全部指向 \(nq\)。
至此你发现 parent 树和 endpos 全部维护好了。
复杂度是神奇的线性乘字符集。
不难发现一个点的最长串一定都这个点上不会被分类,那么每次插入字符新建的点一定代表一个前缀,跳 parent 树即可跳出子串。
广义 SAM
其实差不多。
有点小区别,当你求所有串的公共子串时,需要把每个子串的公共子串都遍历一遍,自己遍历过的点自己不再遍历但是其他点仍需要遍历,复杂度是 \(O(len \sqrt len)\) 的。
考虑一个串的遍历复杂度是 \(O(len^2,\sum |S|)\) 那么均摊下来一个字符的复杂度是 \(O(\min(len,\frac{\sum |S|}{len}) \leq O(\sqrt{\sum |S|})\),所以总复杂度是 \(O(\sum |S| \sqrt{\sum |S|})\)。
SA
后缀数组 \(rk_{i}\) 表示字典序第 \(i\) 的后缀为 \([rk_{i},n]\)。
考虑怎么构造。
有一个无脑的 \(O(n \log^2 n)\) 做法,不提了。
考虑倍增。
考虑把每个 \([i,i+2^k]\) 拿出来排序,因为上一次已经排好了,所以用两个关键字做基数排序即可。
height 数组
令 \(lcp(i,j)\) 表示 \([rk_i,n]\) 与 \([rk_j,n]\) 的最长公共前缀,有 \(lca(i,j) = \min(lca(i,j),lca(j,k))\),假若我们处理出 \(height_{i} = lcp(i,i-1)\) 就能把 \(lca(i,j)\) 变成 RMQ 问题。
考虑求出 \(height\) 数组。这里给一个新数组 \(h\) 数组。
考虑 \(height_{i} = h_{rk{i}}\)。
有 \(h_{i+1} \geq h_{i} - 1\) 然后考虑去暴力扩展。
大不了全部扩展完,是 \(O(n)\) 的。
板子
广义 SAM
struct SAM{
int len,fa;
int son[26];
}nd[maxn];
int tot,lst;
void ins(char c){
if(nd[lst].son[c-'a']!=0){
int q=nd[lst].son[c-'a'],v=lst;
if(nd[q].len==nd[v].len+1){
lst=q;
return ;
}
int nq=++tot;
lst=nq;
nd[nq]=nd[q];
nd[nq].len=nd[v].len+1;
nd[q].fa=nq;
while(v!=0&&nd[v].son[c-'a']==q) nd[v].son[c-'a']=nq,v=nd[v].fa;
return ;
}
int u=++tot,v=lst;
nd[u].len=nd[lst].len+1;
lst=u;
while(v!=0&&nd[v].son[c-'a']==0) nd[v].son[c-'a']=u,v=nd[v].fa;
if(v==0){
nd[u].fa=1;
return ;
}else{
int q=nd[v].son[c-'a'];
if(nd[v].len+1==nd[q].len){
nd[u].fa=q;
return ;
}else{
int nq=++tot;
nd[nq]=nd[q];
nd[nq].len=nd[v].len+1;
nd[u].fa=nq;
nd[q].fa=nq;
while(v!=0&&nd[v].son[c-'a']==q) nd[v].son[c-'a']=nq,v=nd[v].fa;
return ;
}
}
}
int rt;
void insert(string s){
lst=rt;
for(char x:s) ins(x);
}