[CF587F] Duff is Mad

fail 数上的子树更改或查询是易于维护的,而对一个字符串在 AC 自动机上经过的路径进行更改或查询只能暴力做。

所以如果直接把 CF547E 的做法用到这里来,区间询问拆成前缀,对每个前缀按顺序处理,fail 树上子树加,每个询问在 AC 自动机上暴力求和,时间复杂度为 O(A|si|+B|sti|)O(A|si|+B|sti|)。其中 titi 为第 ii 次询问的文本串,AA 为区间加的时间复杂度,BB 为单点查询的时间复杂度。注意式子中第一个 是有约束的(不超过 105105),而第二个没有。这样我们就获得了一个与文本串串长相关的暴力做法。

注意到第一个做法的问题在于遍历文本串的次数过多。如果选择每个文本串只进行一次遍历,对经过的所有点做 O(1)O(1) 子树加(树上前缀和),然后依此处理该文本串相关的所有询问,这个做法的时间复杂度为 O(|sti|+|t||si|)O(|sti|+|t||si|)tt 为询问的文本串序列。

于是我们根号分治:对于长度不超过 |si||si| 的串使用第一种算法,使用 A=O(n),B=O(1)A=O(n),B=O(1) 分块进行区间加单点查;否则使用第二种算法,此时 |t|=O(|si|)|t|=O(|si|)。总时间复杂度为 O((|si|)1.5)O((|si|)1.5)

[POI2000] 病毒

对所有串建出 AC 自动机,如果能够找到一个不经过终止状态的环,则存在一条无限长的不包含所有模式串的 01 串。

[NOI2011] 阿狸的打字机

给定多个字符串,多次询问 xx 串在 yy 串中出现了多少次。

xxyy 中的每次出现都恰对应 AC 自动机上的一个节点,使得这个节点为 yy 的前缀(在 Trie 树上为 yy 的祖先),并且这个节点存在后缀 xx(在失配树上为 xx 的后代),这个点即结尾于该次出现的右端点的前缀。

也即我们需要查询 Trie 树上从根到 yy 这一条路径上,有多少在失配树上以 xx 为根的子树内。于是用树状数组根据 dfs 序维护子树和,在 Trie 树上 dfs,动态维护当前所在的点到根这一条路径上所有点,在树状数组上做单点加减,区间求和。

AC 自动机

做法

核心在于维护失配指针,即每个节点指向 Trie 中存在的最长真后缀。

先建出 Trie 树。将根节点的后继节点加入作为起始节点 bfs。

bfs 的过程中,对于当前节点遍历字符集中的每个字符 cc

  • 如果 uu 存在转移 cc,那么这条转移的终点的失配指针应当指向 uu 的失配指针指向的节点向 cc 转移的结果,然后将这条转移的终点加入 bfs 队列。
  • 如果 uu 不存在转移 cc,那么新建一条向 cc 转移至 uu 的失配指针指向的节点向 cc 转移的结果。

不难发现从一个节点走向它失配指针指向的位置,它所代表的字符串的长度单调减少。因此从一个节点一直这样走,最后一定能够走到 Trie 的根节点。于是失配指针的关系构成了有根一棵树,以每个节点为根的子树即以这个节点为后缀的所有节点。