广义后缀自动机-GSAM
学习 广义SAM 之前,请保证对 字典树、SAM 有一定了解
基础知识
伪广义后缀自动机
之所以是“伪”,因为它的时间复杂度并无法保证(虽然大部分情况下依旧是线性的,也够用),而且部分信息可能无法维护
常见的有以下两种:
-
- 将多个字符串直接连接,相邻两个字符串用特殊符号分隔
-
- 每将一个字符串全部加入到 SAM 后,将
last
指针清零
- 每将一个字符串全部加入到 SAM 后,将
(标准)广义后缀自动机
我们都知道,字典树可以维护多个字符串的信息
如果我们直接在字典树上魔改,让它成为一个后缀自动机,它有什么优秀性质?
-
- 对于一个结点 \(u\),\(len_u\) 等于 \(u\) 在字典树上的深度
-
- 将字典树进行拓扑排序(BFS),得到的就是一个 \(len\) 单调不递减的序列
再回想建立 SAM 的过程,我们插入的结点的 \(len\) 单调递增的,且前后差值为 \(1\),last
指针时上一个加入的状态
而广义 SAM 中,当我们加入一个结点时,last
指针就是它在字典树上的父亲
因此我们只需要在原字典树进行修改(如维护后缀链接、最长长度等)
过程
-
- 建立一棵字典树
-
- 对字典树进行 BFS,并记录每个结点的父亲
-
- BFS 的同时,在原字典树构建 SAM
BFS 部分:
#define pii std::pair<int, int>
std::queue<pii> q;
FOR(i, 0, 25) if(nxt[0][i]) q.push(pii(i, 0));
while(!q.empty())
{
pii now = q.front(); q.pop();
int la = SAM_expand(now.first, now.second);
FOR(i, 0, 25) if(nxt[la][i]) q.push(pii(i, la));
}
构建 SAM 部分:
inline int SAM_expand(int c, int la)
{
int now = nxt[la][c], p = lk[la];
len[now] = len[la] + 1;
for(; ~p && !nxt[p][c]; p = lk[p])
nxt[p][c] = now;
if(p == -1) lk[now] = 0;
else
{
int q = nxt[p][c];
if(len[p] + 1 == len[q]) lk[now] = q;
else
{
int nq = ++tot; lk[nq] = lk[q];
FOR(i, 0, 25) if(len[nxt[q][i]])
nxt[nq][i] = nxt[q][i];
len[nq] = len[p] + 1;
for(; ~p && nxt[p][c] == q; p = lk[p])
nxt[p][c] = nq;
lk[q] = lk[now] = nq;
}
}
return now;
}
细节:
int now = nxt[la][c];
与之前的 now = ++tot
不同,因为字典树上已经有当前状态了,直接拿来用就行
int p = lk[la];
因为 la
已经指向过了 now
,因此我们要从 lk[la]
进行遍历
FOR(i, 0, 25) if(len[nxt[q][i]])
nxt[nq][i] = nxt[q][i];
因为 len
大于当前转态的结点还没加入到 SAM 中,不能将它复制过来
模板题
建好 GSAM 后,我们统计每个结点 \(i\) 的 \(len[i]-len[lk[i]]\) 即可