后缀自动机

后缀自动机的疑难点

代码

void sam_extend(char c) {
  int cur = sz++;
  st[cur].len = st[last].len + 1;
  int p = last;
  while (p != -1 && !st[p].next.count(c)) {
    st[p].next[c] = cur;
    p = st[p].link;
  }
  if (p == -1) {
    st[cur].link = 0;
  } else {
    int q = st[p].next[c];
    if (st[p].len + 1 == st[q].len) {
      st[cur].link = q;
    } else {
      int clone = sz++;
      st[clone].len = st[p].len + 1;
      st[clone].next = st[q].next;
      st[clone].link = st[q].link;
      while (p != -1 && st[p].next[c] == q) {
        st[p].next[c] = clone;
        p = st[p].link;
      }
      st[q].link = st[cur].link = clone;
    }
  }
  last = cur;
}

构造后缀自动机

\(case1\),\(p\)沿着后缀链接树向上跳,涉及到的每个\(p\)没有\(st[p].nxt[c]\)的转移

  • 从后缀链接树的\(last\)状态向上跳,会遍历到长度为\([0,len(last)]\)的所有区间,即这个字符串的所有后缀,若跳到了根节点也没有\(c\)的转移,那么就是整个\(SAM\)中不存在字符\(c\),那么为\(p\)的每一个\(c\)转移都指向新加入的那个点,后缀链接树上,将\(longest(last)+c\)的后缀链接指向根节点,这是独一无二的\(endpos\)

\(case2\),\(p\)沿着后缀链接树向上跳,找到一个\(p\)\(q\)的转移\(c\),并且满足\(len(p)+1=len(q)\)

  • 后缀链接树向上找,找到的每个状态都是\(last\)的后缀,如果这个状态\(p\)通过转移\(c\)正好能够走到\(q\),并且\(q\)是它所属的\(endpos\)等价类中最长的,那么显然\(q\)不会在前方相比\(p\)突出一块,会与\(p\)对齐,而\(p\)\(last\)的后缀,\(p+c\)\(q\)必然是\(last+c\)的后缀,所以\(last+c\)\(endpos\)必然是\(q\)的一个真子集,而由于我们找的是顺着后缀链接树的第一个满足条件的\(p\),它必然是最长的,又由于\(q\)也是它对应的\(endpos\)等价类中最长的,所以\(last+c\)不断地删头\(endpos\)首次发生变化必然是删到\(len(q)\)时,所以直接相连是没有问题的

  • 为什么找到一个\(p\)就停下来?因为如果我们继续向上跳,跳到的必然是\(p\)的后缀,记为\(p'\),所以它通过转移\(c\)对应的状态\(q'\)也一定是\(q\)的后缀,那在后缀链接树上,\(q'\)一定是\(q\)的父亲,由于后缀链接树上,儿子的\(endpos\)集合是父亲的真子集,所以在\(q\)的儿子接上位置\(n\)之后,由定义,它的所有的父节点的\(endpos\)集合都顺水推舟、水到渠成的拥有了位置\(n\),所以只连一次就符合了\(SAM\)的定义

\(case3\)\(len(p)+1<len(q)\) 怎么搞呢

  • 我们发现,不同于上一个情况,我们的\(q\)状态的前端和\(p\)状态相比突出了一块,而这时候我们找到的\(last\)的最大的\(endpos\)集合不同的后缀是\(p\)而不是\(q\),因此增加\(c\)之后,仅仅有\(p+c\)的状态的\(endpos\)集合增加了\(n\),我们记这个状态为\(nq\),可以发现它实际上是\(q\)状态中最短的子串,所以合着\(q\)状态中只有最小的\(nq\)\(endpos\)增加了。怎么办呢?我们考虑将q状态拆开,将到\(q\)的转移\(c\)全部指向\(nq\),与此同时,\(q\)的所有转移,\(nq\)也一定继承。这很好理解,因为\(nq\)实际就是\(q\)的后缀,在它后面加什么\(defg\cdots\) ,所指向状态的\(endpos\)都不会受影响,\(nq\)\(q\)还是一家人(一家人再怎么打骂起分歧也不会分开),所以这么连肯定也是对的

  • 下面考虑后缀链接树怎么搞,首先一个显而易见的结论,\(nq\)\(endpos\)集合仅仅只比\(q\)\(endpos\)集合多了一个元素\(n\),这里好办,让\(nq\)代替\(q\)的位置,然后把\(q\)\(last+c\)的后缀链接指向\(nq\)(为什么能取代,儿子取代父亲的位置不是恒河里吗doge),然后我们通过后缀链接树向上跳,如果发现\(st[p].nxt[c]==q\),那么就在\(SAM\)上把这个\(p\)的转移\(c\)直接连到\(q\)上("将到\(q\)的转移\(c\)全部指向\(nq\)",对应的就是上文的这句话),因为这时跳到的\(p+c\)\(endpos\)集合包含\(n\),它显然不应该指向\(q\)。如果发现\(st[p].nxt[c]\neq q\),这句话是什么意思呢?就是我们的\(q\)已经足够小了,此时跳到的\(p\)是起初\(p\)的后缀,再增加一个字符\(c\)会形成一个更大的\(endpos\)集合,增加字符后形成的\(q'\)显然也是\(nq\)的后缀,接着向上跳形成的\(q''\)也同样是\(nq\)的后缀。所以我们发现,再往上走都是\(q\)的祖先,而由于\(nq\)的存在,上面所有的状态的\(endpos\)显然都包含有\(n\),这时\(p\)再向\(q',q'',\cdots\)连边就没有一个状态两种\(endpos\)集合的情况了,所以它是对的

复杂度证明

  • 空间复杂度显然正确,因为每一次插入字符时,\(Sam\)的状态数只会增加两个,因此为\(O(n)\),上界为2n-1,在形如字符串\(abbbb\cdots\)时取到,这就是为什么\(SAM\)的空间要开两倍

  • 时间复杂度,分两种情况讨论

\((1).\) 考虑连续的转移,我们假设能连续就连续,总共就不超过\(2n-1\)个状态,那么将这些点连续地连起来最多也就\(2n-2\)条边,实际上,极限情况\(abbbb\cdots\)也达不到\(2n-2\)

\((2).\) 考虑不连续的转移,令当前不连续的转移为\((p,q)\),字符为\(c\),我们设从起始点到\(p\)的最长路径为\(u\),\(q\)到任意终止点的最长路径为\(w\),那么\(u+c+w\)这个东西显然是原串的一个后缀,而一个长为\(n\)的串最多就有\(n\)个后缀,\(SAM\)中又不会出现重复的子串,因此不连续转移的个数最多就是\(n-1\)

posted on 2023-01-30 21:12  star_road_xyz  阅读(82)  评论(0编辑  收藏  举报

导航