其实是在瞎口胡
SAM 的线性构造
SAM 的每个节点都表示一个 endpos 等价类,由于 SAM 和 parent tree 共用节点,所以我们要做的就是在这些节点上添加边使其能接受所有的后缀. 将一个节点的后缀边连向另一个节点表示这个节点的 endpos 等价类中所有串在后面加上一个字符会到达另一个 endpos 等价类. parent tree 上的父亲边将一个节点的 endpos 等价类划分开来,成为互不相交的新等价类,即跳父亲就是前往 endpos 等价类中以当前等价类最短串删去前面的一个字符为最长串的等价类.
我们在后缀自动机的每个节点上存储这个节点的等价类中的最长串长度 \(len_i\),parent tree 上的父亲 \(fa_i\) 和后缀边 \(ch_c\) 连向的节点.
考虑增量法构建 SAM,即在前面所有的字符 \(s_{1\sim i-1}\) 都已经加入 SAM 之后考虑新增 \(s_i\) 新增的边. 我们从上次加入后的节点 \(lst\) 开始,不难发现,由于新增的字符改变了所有原串的后缀,我们就要给新节点连上后缀边并且找到父亲,也就是说我们要找出 \(s_{1\sim i}\) 后缀的所有等价类. 事实上,沿着 \(lst\) 的父亲一直向上遍历到根,有结论:
-
记途中经过的所有等价类的最长串为 \(f_1,f_2,\cdots,f_k\),那么我们就会发现,对于所有和 \(f_j\) 在同一个等价类的后缀 \(f'_j\),\(f'_j+s_i\) 也在加入字符 \(s_i\) 后的 SAM 的同一个等价类中.
并且由于 \(f'j+s_i\) 总是出现在 \(s{x+1}=s_i\) 且 \(x\in\text{endpos}(f'_j)\) 的位置 \(x\),所以访问到的 endpos 等价类是不会遗漏的.
所以我们可以从 \(p=lst\) 开始不断向上遍历 \(fa_p\),因为后缀的特殊结构决定了上个节点一定是包含之前所有后缀的,就可以根据这个节点是否有 \(ch_{c=s_i}\) 这条边来分讨连边情况.
设新增节点为 \(nw\),根节点 \(rt=1\).
Case 1
如果 \(p\) 没有出边 \(ch_c\),说明当前的 SAM 上缺少 \(x+s_i\) 的信息,需要令出边 \(ch_c=nw\),此时需要继续遍历.
那么为什么从 \(lst\) 开始遍历 \(fa_p\) 找到的串就是我们要更新的信息呢?考虑跳 \(fa_p\) 其实是在等价类前面删字符,这样可以遍历之前所有的后缀所在的等价类,再把新增字符 \(c\) 对后缀带来的影响表示出来就好了.
当然,如果 \(p\) 已经到达根节点,就可以直接令 \(fa_{nw}=rt\) 并停止遍历,因为此时必然是第一次加入 \(c\) 这个字符.
Case 2
如果 \(p\) 存在一条出边 \(q=ch_c\),其实我们不能直接让这个等价类直接作为 \(fa_{nw}\). 因为如果 \(x+s_i\) 不是 \(q\) 的最长串,此时 \(q\) 中只有一部分串是 \(x+s_i\) 的后缀.
注意 \(p\) 和 \(q\) 是父子关系,所以如果 \(len_p+1\neq len_q\) 其中必然因为 \(c\) 的加入缺失了一部分子串信息,这些缺少的信息是无法通过修改原图增加的,因为原图保证了最小性. 我们必须新增节点把 \(q\) 的信息拆一部分出来来添加后缀边维护新的子串并且调整父子关系,由于此时的 \(p\) 和 \(q\) 不再符合父子的要求. 记这个新节点为 \(nq\),令 \(len_{nq}=len_{p}+1\),且 \(nq\) 继承所有 \(q\) 的出边,此时 \(nq\) 与 \(q\),\(nq\) 与 \(nw\) 符合父子关系,有 \(fa_q=fa_{nw}=nq\). 然后需要修改所有 \(p\) 原有 \(c\) 出边指向 \(q\) 的为 \(nq\),否则后缀边是不连续的.
如果 \(len_p+1=len_q\),说明此时所有字串信息都没有缺失,直接令 \(fa_{nw}=q\) 即可.
以上两种子情况都不需要继续遍历 \(fa_p\),因为已经确保操作后维护了所有的性质.
实现
非常的简短,非常的好写啊
cpp
struct node{int ch[26], fa, len;} t[maxn << 1];
int lst = 1, tott = 1;
void insert(int c) {
int p = lst, nw = lst = ++tott; t[nw].len = t[p].len + 1;
for(; p && !t[p].ch[c]; p = t[p].fa) t[p].ch[c] = nw;
if(!p) {t[nw].fa = 1, ans += t[nw].len - t[t[nw].fa].len; return;}
int q = t[p].ch[c]; if(t[q].len == t[p].len + 1) t[nw].fa = q;
else {
int nq = ++tott; t[nq] = t[q];
t[nq].len = t[p].len + 1; t[q].fa = t[nw].fa = nq;
for(; p && t[p].ch[c] == q; p = t[p].fa) t[p].ch[c] = nq;
}
return;
}
应用
根据 SAM 的强大性质可以解决不少串串题,但是有一些进阶应用还要结合其它数据结构.
最小表示法
发现这就是找在环上长度为 \(n\) 的字典序最小的字串,直接倍长插入到 SAM 中然后从根节点开始不断走字典序最小的字符,走 \(n\) 步得到的串即为答案.
求本质不同字串个数
根据 SAM 定义,每个节点上压缩的串个数之和就是 \(ans\). 由于 endpos 等价类中的串长度是连续的,所以每个节点 \(nw\) 的贡献就是 \(len_{nw}-len_{fa_{nw}}\). 在插入字符时在线统计一下就好了.
求字串出现次数
对于某个子串,从根节点开始走找到对应节点,那么这个节点包括子树内所有节点都出现过这个字串,且一定不重不漏. dfs 数子树大小即可.
求两个(多个)串的最长公共子串
对于两个串的情形,可以对其中一个串建 SAM,另一个串在 SAM 上匹配