后缀自动机复习笔记

嗯,远古时期学过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明显是新等价类的代表,每个后缀作为一个非独立等价类(即后缀的后缀可以产生更大的等价类)。

posted @ 2019-10-08 21:38  ComeIntoCalm  阅读(174)  评论(0编辑  收藏  举报