后缀自动机复习笔记
嗯,远古时期学过sam,然后半年没写忘掉了,只记得大致是个啥玩意,来写个笔记搞一搞
sam及后缀树构造
设原串为\(S\)
1.需要知道的是sam不是后缀树,这是个类似于如果中间没有状态会被压起来的一个dfa,但是sam的fail树实际上就是\(S\)反串的后缀树
2.先来看反串的后缀树,没被压缩过的实际上就是把所有后缀\(suf_i\)暴力插入\(trie\),这样状态数是\(O(n^2)\)的,同时因为trie可以识别前缀,所有后缀的所有前缀即为子串
记每个非空子串为\(s_j\),那么每个子串都在\(S\)中有匹配,对应的匹配一次结束位置的集合为\(endpos_j\)
明显如果对于同一\(S\)内的\(endpos_j\cap endpos_k\not = \empty\),一个子串一定为另一个子串的后缀
3.每个点\(u\)对应一个状态,每个状态对应多个子串\(s_j\),因为这些子串\(s_j\)的\(endpos_j\)交于\(u\),所以对于一个点\(u\),其上的
\(\forall i,j, endpos_i\)和\(endpos_j\)一定具有子集关系,因此状态数是\(O(n)\)的
4.fail边奥妙重重,考虑当前点\(u\)的\(endpos\),取一个\(endpos\)包含了当前状态的最长串的最长后缀,那么fail边连向的就是这个最
长后缀的\(endpos\)中另外一个状态点\(fail_u\),既然\(\vert s[fail_u]\vert \leq \vert s[u]\vert\),所以\(u\)的\(endpos\)一定是\(fail_u\)对应状态的\(endpos\)的子集
hhh上面是在没学透的情况下的扯皮,先来看怎么构建这玩意。
直接放代码吧,反正是给自己看的玩意。
char S[N];
int tot = 1, last = 1;
struct SAM {
int son[26], len, fail;
} t[N];
void ins(int c) {
int p = last, np = ++tot;
t[np].len = t[p].len + 1, last = np;
for ( ; p && (!t[p].son[c]); p = t[p].fail) t[p].son[c] = np;
if (!p) {
t[np].fail = 1;
} else {
int q = t[p].son[c];
if (t[q].len == t[p].len + 1) {
t[np].fail = q;
} else {
int cur = ++tot;
t[cur].fail = t[q].fail,
t[cur].len = t[p].len + 1;
for (R int o = 0; o <= 25; o++)
t[cur].son[o] = t[q].son[o];
//*t[cur].son = *t[q].son;
while (p && t[p].son[c] == q) {
t[p].son[c] = cur;
p = t[p].fail;
}
t[q].fail = t[np].fail = cur;
}
}
}
设新插入的字符节点为\(np\),初始节点为\(init\)
对于每个新插入的字符\(c\),都需要在\(last\)的基础上暴跳\(fail\) 直到暴跳到的节点\(p\)的\(son[c]\)产生了冲突或者\(p\)是\(null\)为止
同时对于路径上的每个点的\(son[c]\)都指向\(np\)
1.如果\(p\)是\(null\),那么\(fail[np] = init\)
2.如果\(p.son[c]\)有冲突
1.这个状态是连续的即\(len[p] + 1 = len[p.son[c]]\),不需要做出干涉
2.这个状态是不连续的,也就是中间隔了一段直到\(p\)前缀都相同的字符串然后\(c\)转移的指针直接指了过去,现在后缀又匹配到了\(p\),并且要求有一个真正的后缀,也就是\(p+c\),所以需要新建一个节点来替代这个\(p.son[c]\)(同时也相当于新建了一个等价类),同时复制除了\(len\)以外的所有信息,然后把原来指向\(p.son[c]\)的且具有相同后缀的点的\(son[c]\)指向这个新建的节点。这个时候我们注意到\(fail[p.son[c]]\)变成了这个新建的节点
为什么\(fail[p.son[c]]\)会变成这个新建的节点?因为这个时候p+x的后缀和p匹配,因此\(p+c\)成为了\(p+x+c\)的子串,\(p+c\)也变成了\(p+x+c\)和\(last+c\)的父亲,这个时候\(endpos\)集合一定是子集关系,联想到到\(fail\)树的本质是反串的后缀树,相当于后缀树边新增分叉。
这是\(fail[q]\)改为\(new\)节点的原因,也可以说他们有相同后缀(p+c是p+x+c的后缀)
关于后缀树
hhhh我们知道\(fail\)树实际上就是反串的后缀树,且一个点的\(endpos\)相当于其子树中叶子的\(endpos\)的并集
考虑啥时候有\(endpos\)
明显每个前缀会拥有自己的\(endpos\),每个前缀插入后的状态的fail树链上的祖先也会吃到这个\(endpos\),也就是这个前缀的所有的后缀都会吃到\(endpos\),考虑在冲突的时候新建的节点(跨越了一整个子串裂出来的那个点)为啥没有\(endpos\),这个子串明显是一个前缀的后缀,因此不应该主动产生\(endpos\),这个时候也相当于新建一个等价类。
举个栗子:
点z明显是新等价类的代表,每个后缀作为一个非独立等价类(即后缀的后缀可以产生更大的等价类)。