嗯,远古时期学过sam,然后半年没写忘掉了,只记得大致是个啥玩意,来写个笔记搞一搞
sam及后缀树构造
设原串为(S)
1.需要知道的是sam不是后缀树,这是个类似于如果中间没有状态会被压起来的一个dfa,但是sam的fail树实际上就是(S)反串的后缀树
2.先来看反串的后缀树,没被压缩过的实际上就是把所有后缀(suf_i)暴力插入(trie),这样状态数是(O(n^2))的,同时因为trie可以识别前缀,所有后缀的所有前缀即为子串
记每个非空子串为(s_j),那么每个子串都在(S)中有匹配,对应的匹配一次结束位置的集合为(endpos_j)
明显如果对于同一(S)内的(endpos_jcap endpos_k ot = 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明显是新等价类的代表,每个后缀作为一个非独立等价类(即后缀的后缀可以产生更大的等价类)。