后缀树到后缀自动机

后缀自动机

以前学的后缀自动机

今天听了ZR 敦爷的课加上以前和zbww讨论了一下,感觉对后缀自动机和后缀数据结构有了船新认识!QWQ

后缀自动机大概分成了下面三部分:

  • $ n^2 $ 后缀树的构建
  • 后缀自动机构建及基础应用
  • 缩边并且得到 $ O(n) $ 个点的真正的后缀自动机!

后缀树

其实后缀树才是本质的后缀数据结构。


First step 后缀树构建

把单词的后缀插入一棵 Trie 就可以得到后缀树,节点数是 $ O(n^2) $ 的。注意,到现在我们还是考虑 $ O(n^2) $ 的后缀树。

后缀树上的一个节点其实就对应了一个子串。

其实后缀数组只是后缀树的 DFS 序。

后缀树上点 $ x $ 是 $ y $ 的祖先,那么 $ x $ 明显是 $ y $ 的前缀。

  • 一个串 $ x $ 出现次数就是它是几个后缀的也就是子树中有多少个后缀点。

    显然

  • 一个串在 $ L $ 处出现,等价于它是 $ suf[L] $ 的前缀,等价于它是 $ key[L] $ 的祖先。

    显然

  • 那么怎么求 $ LCP( suf(x),suf(y) ) $

    前缀关系就是它们的祖先所以就是找 $ LCA(key(x),key(y)) $

  • 本质不同的子串个数

    其实就是后缀树的节点个数

  • 最长出现超过 $ K $ 次的子串

    我们只需要计算每个节点对应出现次数。前面说过了。可以 dfs 一次算出

  • 最长不重叠出现超过 $ K$ 次

    类似后缀数组二分 L 用 $ H_i \leq L $ 割开来算。

    也就是说,换在后缀树上,如果 $ len[x] \leq L $ 我们就dfs这个子树。

    否则如果 $ len[x] \geq L $ 我们把子树那的 $ key $ 点取出来,排序,然后从左往右能选就选。

  • 求 LCS

    把 $ S , T $ 拼一起建后缀树。

    求一个子串,它在 $ S $ 中出现,且在 $ T $ 中出现。

    也就是说它是 $ S $ 某后缀前缀,也是 $ T $ 某后缀前缀。

    也就是说子树内有 $ S $ 的 \(key\) 点也有 $ T $ 的 $ key $ 点。

  • 求第 $ k $ 小的子串

    按照出边 dfs 下去就好了。


Second step 后缀自动机构建及基础应用

注意,到现在为止还是 $ O(n^2) $ 的后缀树!

首先我们拿出一棵后缀树,并且定义:

  • 一个点的 $ len $ 表示这个点代表的子串的长度

  • 一个点的父亲( \(par[x]\) )表示这个点在后缀树上的父亲。

到现在还是后缀树,后缀自动机和后缀树差的其实就是一个 “转移” 数组

  • \(son[x][c]\)在这里后缀树从 $ x $ 这个节点代表的子串是往前加字符得到的节点!往后加字符其实构造出来的是前缀树,为了和前面后缀树的拼接我们暂时用 $ son[x][c] $ 表示往前加字符的!

现在我们就有了一个基本的后缀自动机。

构建考虑增量法

我们现在有 $ S $ 的后缀自动机,考虑往前加入一个字符,怎么整出 $ cS $ 的后缀自动机?

步骤大概是:给 $ cS $ 这个串建立一个节点,然后给它找到父亲/祖先们,最后整出它的转移。

首先,它的祖先肯定是 $ cS[1...t] $ ,这个祖先也有可能不存在,也就是说整个字符串没有 $ c $ 这个字母。

我们要先找这个最近的已经存在的祖先(或者是根),再把剩下的祖先建立出来。

首先 $ cS[1...t] $ 肯定是 $ son[S[1...t]][c] $ 。

如何找到 $ S[1\dots t] $ ?很显然的是, $ S[1...t] $ 是 $ S $ 的祖先,所以我们可以从 $ S $ 向上跳,直到找到 $ son[x][c] $ 有值。

然后我们就从 $ cS[1...t] $ 往上补全,同时把中间的部分补全,这样我们就构造好了父亲。

然后考虑怎么构造 $ son$ ,这些新加进去的点肯定是没有 $ son $ 的,如果它加字符还可以连向其他的点,那么它一定是出现过的,即 $ cS[1...d] $ 是 $ S $ 的一个子串。

考虑什么点的 $ son $ 会连向它?其实就是 $ S $ 一步步跳到 $ S[1...t] $ 中途的点可以通过添加 $ c $ 一一对应到这些点。

如果这样的祖先不存在,说明没有 $ c $ 出现过,直接从后缀根开始构造就好了。

总结下来一句话,爬链,创链,连链。


Third step 缩边并且得到 $ O(n) $ 个点的真正的后缀自动机!

到现在我们的后缀树还是 $ O(n^2) $ 节点的,显然是成不了大事的。

这个树叶子数量只有 $ O(n) $ 的(后缀数量),也就是说它看起来是非常臃肿。

我们考虑这样的一个事实:如果一个树不存在 只有一个儿子的点 那么点数和叶子数同阶。(画个每个点都至少两个儿子的树就发现是非常显然的了,每次一个点至少合并了两个部分叶子,至少就是 $ O(2n) $ 个点)

那么也就是说如果我们可以把只有一个儿子的点压缩了,这就是个线性的树了!

但是我们希望压缩后不会损失信息,所以我们希望 ...- a - a - a -... 这样的树压缩后直接变成了 ... - aaa - ...,最终剩下的点保存的不再是一个串,而可能是几个串。

我们称最后留下来的点叫做关键点。当然叶子还是留着的,我们称之为后缀点。

到这里,发现其实直接对 $ sa $ 建虚树和我们最后希望压缩结束的树,本质上是一样的。

但是剩下的关键点,它代表的不再只是一个串了!

所以现在,原来的定义不太适用了,有了新的定义:

定义一个点代表的串的集合是 $ Z(x) $

同时,$ len(x) $ 的定义也变化了,定义现在是 $ Z(x) $ 里面最长串的长度。

然后,定义 $ fail(x) $ 就是一个点往祖先爬的第一个关键点,我们也有 $ |Z(x)| = len(x) - len(fail(x)) $。

如何保证一个点被压缩,它的信息仍然留存在关键点上?

  • 现在我们需要证明原树上如果 $ T $ 是被压缩的点,那么 $ son[c][T] $ 也是被压缩的点

    • $ cT $ 是后缀点,那么 $ T $ 也是后缀点,矛盾
    • $ cT $ 有2个儿子,$ cT $ 也会有两个儿子,矛盾
  • 但是如果 $ x $ 是一个关键点,$ son[x][c] $ 却可能是被压缩点。

    这种情况我们让 $ son[x][c] $ 连到它被压缩到的点中。

于是,我们相当于更改了 $ son $ 的定义,对于 $ Y\in Z(x) \(,\) cY \in Z(son_{new}[x][c]) $

这样的话,我们就有了一个无损压缩,既压缩了点又没丢失信息。

怎么构造呢?

回忆一下,刚刚我们构造 $ n^2 $ 后缀自动机,我们的操作是 爬链,加链,连链(改 $ son $ )

我们能不能更改这个过程,使得构造出来的节点数是 $ O(n) $ 的呢?

一样的,假设我们已经对 $ S $ 构造出了 $ O(n) $ 节点的后缀自动机,加入字符 $ c $ 得到 $ cS $,从 $ S $ 所在节点开始爬链。

假设为 $ cS $ 新建的点是 $ P $。

我们假设爬链得到的点是 $ Q $ ,$ E = son[Q][c] $,也就是说最长的 $ cS[1...t] $ 在 $ E $ 上 。

但是它可能实际上想链接的是 $ E $ 中的某个串在未压缩时所对应的某个子串。根据 $ fail $ 的定义是不能把 $ fail[P] $ 直接连向 $ E $ 的。

如果 $ len[E] = len[Q] + 1 $,想要连接的就是 $ E $ ,因为 $ cS[1\dots t] $ 就是 $ E $ 所对应的最长串长度+1,直接 $ son[Q][c] = E $ 就好了,否则如果 $ len[E] > len[Q] + 1 $ , 我们需要连接的是一个 $ Z(E) $ 中的某个串,方法就是把这个串分裂出来。

假设 $ Q $ 想要连接到的串($ Z(E) $ 中的一个串 )以及它的 / 存在于 $ E $ 的 / 前缀都拿出来建立一个点叫做 $ k $。现在 $ E $ 就相当于被切开了。

于是,$ E $ 的父亲就变成了 $ k $ 。

所以把 $ k $ 的父亲指向 $ E $ 原来的父亲,并且把 $ E $ 和 $ P $ 的父亲都设置为 $ k $。

现在爬链不变,加链就变成了拆点,分裂出一个点,从加链变成了加点。

最后还需要更新一下 $ Q $ 祖先(包括 $ Q $ )往 $ c $ 的转移,即如果有往 $ c $ 的转移并且转移向 $ E $,就把它改成 $ k $ 就好了。

基本操作

其实往前加字符和往后加效果是一样的,后面我们就考虑往后加字符了,这样后缀树就变成了前缀树,理解应用的时候更轻松。

再回头看一下 $ go $ 数组,可以发现它拥有一个有用的性质:

  • 如果 $ S \in Z(x) $ 那么 $ Sc \in Z(go[x][c]) $

也就是说,要找到了 $ T $ 的节点,其实就是从根出发,按照 $ T $ 的字符走 $ go[x][c] $ 就好了。

  • 本质不同的子串个数

    后缀树的节点个数,就是每个点的 $ len[x] - len[fail[x]] $ 的和

  • 一个串的出现次数

    其实就是找 $ key[L] $ 的祖先,这些祖先含有 $ [len[fail[x]] +1, len[x]] $ 个本质不同的子串

  • 最小循环串

    找 $ SS $ 长为 $ |S| $ 的最小子串。

    从根开始走了 $ k $ 步必然就是一个长度为 $ k $ 的子串。

    既然找最小,每次走最小的边,走 $ |S| $ 次。

    如果走到了尽头,那么我们必然走到了一个后缀,但是一个后缀在 $ SS $ 中,如果长度不超过 $ |S| $ 那么必然可以继续走,所以如果真的走到了死胡同那么当前的大小肯定大于了 $ |S| $ 不符合题设。

  • $ S[L...R] $ 的定位

    $ S $ 必然是 $ key[R] $ 的祖先,直接倍增就好了。

  • 求长度为 $ k $ 的字典序最小的子串

    一个 Trivial 的做法是,走 $ k $ 步,每次走最小,这样很容易走到死胡同。

    可以求一个 $ f[i] $ 表示从 $ i $ 开始最长可以走多少步。就是求一个类似拓扑图深度的东西。

  • 求 $ S,T $ 有多少对子串相等

    $ \sum_{x} (cnt_{S,x})(cnt_{T,y}) $

    可以把这两个串拼起来然后搞后缀树,答案就是对于某个点它子树里面 $ S $ 的后缀个数和 $ T $ 的后缀个数。

    $ \sum (len[x]-len[fail[x]]) S(x) T(x) \(,\) S(x) $ 表示 $ x $ 子树中 $ S $ 后缀个数,$ T $ 同理。

    本质不同乘的是 $ [S(x)>0] $

  • 然后 LCS

    还是拼起来,搞后缀树,

  • 最长至少出现了 $ k $ 次的子串

    对于 $ x $ ,求出 $ x $ 出现了 $ S(x) $ 次,如果 $ \geq k $ 就统计就好了

  • 求第 $ K $ 小的子串,要求 $ O(Qn) $

    从小到达遍历子串,$ F[x] $ 表示从 $ x $ 出发可以走到多少个不同的子串。

  • 品酒大会

  • 设 $ f(i)$ 为长度为 $ f(i) $ 的子串的出现次数最大值,求 所有 $ f $ ,要求线性。

    出现次数可以方便地在后缀树上 dp

    它可以让长度为 $ len[fail[x]] + 1 ... len[i] $ 做贡献。

    其实可以看成对前缀取max,因为没有

  • 毒瘤:每次往后加 $ a $ ,求 $ T $ 在 $ S $ 出现次数

    离线很好做,就是这个题的弱化版 DQS的trie

  • 加一个叶子,把一条边裂开

    链加,加叶子,裂边,LCT

  • 给定 $ n $ 个字符串,求有几个字符串至少是 $ K $ 个串的子串

    $ \sum |S| \leq 10^5,n,k \leq 100 $

    对于一个点,我们需要求出它的子树中有无 $ S_i $ 的后缀。

    这个东西我以前的做法复杂度不可证明,现在老师给了种复杂度正确的做法,

    为了保证每个点被一个串抓到,可以这样做,对于每个点到根+1,按照dfs序排序后将两两相邻LCA到根-1。

    这样类似虚树地做,假设一个点有 $ k $ 个关键点儿子,它会被加 $ k - (k-1) $ 次。

posted @ 2020-02-04 16:52  yijan  阅读(391)  评论(0编辑  收藏  举报