ULR #2 C. 霸占排行榜
题意
给定一棵共 \(l\) 个节点的 Trie 树结构,其中每一个非根节点 \(i\) 表示一个字符串 \(u_i\)(容易得出这些串互不相同且非空)。给定一个长度为 \(n\) 的原始字符串序列 \(\{s_i\}\),通过这些原始字符串构造一个大字符串 \(T = s_{a_1} + s_{a_2} + \dots + s_{a_m}\)。其中 \(\{a_i\}\) 是给定的正整数序列。现在对于所有 \(i\),询问 \(u_i\) 在 \(T\) 中的匹配次数。
数据范围:\(2 \le l \le 10^5, 1 \le n \le 200, 1 \le m \le 10^6, 1 \le \sum |s_i| \le 3\times10^6, 1 \le |\Sigma| \le 4\times10^6\)。
省流:字符集巨大,字符串都很长,\(m\) 很大,\(n\) 比较小。
题解
首先,建立 AC 自动机。注意这里不能采用完整自动机结构,因为字符集很大;也不能采用类似 KMP 的单纯 \(fail\) 链结构,因为叶子节点深度和(即对应模式串长度和)是没有保证的,也不方便后续处理。需要使用可持久化数组维护每个节点的出边。建立时首先继承 \(fail_{par_i}\) 的数组,然后对于当前节点存在的转移边,到可持久化数组上进行修改。时空都是 \(\Theta(l\log |\Sigma|)\)。
接下来,考虑每个字符串 \(s_i\) 在自动机上的匹配过程,也就是记 \(tr[p, i]\) 表示从节点 \(p\) 开始,匹配 \(s_i\),最终走到的节点。这样,结合一些路径信息,就能快速进行 \(T\) 的匹配。如何预处理呢?首先,考虑不跳 \(fail\) 指针到达的节点 \(base[p, i]\)。假设已经预处理出这一部分,那么后续计算 \(tr[p, i]\) 是容易的:跳 \(fail\) 指针后节点的深度会变浅,那么讨论一下新节点在 Trie 上的深度,要么所有来自 \(u_p\) 的字符都已经失配,可以继承 \(s_i\) 在根节点的匹配结果;要么可以找到一个节点 \(p'\) 使得它的深度比 \(p\) 更浅,同时 \(s_i\) 从 \(p'\) 的匹配长度至少有从 \(p\) 开始那么长,继承它的匹配结果即可。
这里的 \(p'\) 是 \(fail_{base[p, i]}\) 在 Trie 树上的祖先,所以可以用 \(\Theta(1)\) 树上祖先之类的技术做到不带 \(\log\),但是这是不必要的。注意到 \(p'\) 同时还是 \(p\) 在 \(fail\) 树上的祖先,而虽然我们不能控制访问 \(fail_{base[p, i]}\) 的顺序,却可以控制访问 \(p\) 的顺序,所以只需要按照 \(fail\) 树的 DFS 序访问 \(p\),用一个数组记录深度为 \(d\) 的当前节点的 \(fail\) 树祖先(唯一)即可。
现在一个重要问题是求出 \(base[p, i]\),这一步显然需要利用 Trie 树的树结构,因为不会跳 \(fail\) 链。一个朴素的暴力是基于字符串哈希,具体复杂度可能是双 \(\log\) 或者一个?总之不能通过全部数据。首先考虑如果 Trie 树是链你会怎么做这个问题:考虑建立后缀数组,然后在后缀数组上跑字符串匹配,复杂度是 \(\Theta(l\log l + |\Sigma| + \sum |s_i|\cdot \log l)\) 很可以接受。考虑怎么把这个东西搬到树上。你发现树链剖分一下你复杂度直接变成两个 \(\log\),废了。可不可以什么树上后缀排序或者广义后缀自动机去做呢?是不行的,为什么呢?
以下部分感谢 @unputdownable 的支持。
利用后缀数据结构对文本串进行匹配的过程是有区别的。基于后缀数组的匹配本质上是基于后缀 Trie,后者的一种压缩形式是后缀树,而后缀数组是其另一种压缩形态——即只维护了所有叶子的一种 DFS 序。在后缀数组上二分哈希不断缩小范围的过程等价于后缀 Trie 上走子树。本题中需要维护的信息是从一个位置开始往后继续匹配的结果,那么很容易体现为对后缀树上当前点的子树内所有叶子赋值——可以用后缀数组上区间结构表示。基于后缀自动机的匹配是不同的。后缀自动机将信息记录在一个字串的末尾,基于 endpos 等价类维护信息,一个串的 SAM 可以很轻松地得到它的反串的后缀树/后缀数组。树上后缀排序,获得的也是反串集合的后缀数组的并集。这点微妙的区别导致了后缀数据结构的维护方法在这道题中是不能上树的。
所以你必须考虑基于对模式串维护的数据结构。考虑 KMP 的匹配过程,实际上是维护“文本串的一个前缀,它的最长后缀使得这个后缀是模式串的一个前缀”。其实这个过程和 SAM 匹配过程更相似,只是主客对换了。信息还是保存在匹配到的子串的右端点上。所以这道题你不能这样做。有没有什么结构,能将匹配过程维护到左端点,同时不用处理文本串呢?有,Z 函数的匹配就是这样,而且它并不带 \(\log\),是均摊线性的。所以基于 Z 函数的做法就很有前途。
事实上这个信息就是【模板】Z 函数,所以你做过这个题目的话一眼就秒了,但是我讲这么多废话是想对字符串匹配的不同视角有一个更好的理解。考虑对 \(s_i\) 建立 Z 函数数组,接下来,进行轻重链剖分,在每个重链上很容易得到从每个点出发最长能匹配多长使得依然在这条重链上,于是转化为了 \(n\cdot l\) 个询问形如在某个节点 \(p\) 的子树里面匹配某个字符串的一个后缀 \(s_i[j \dots]\)。这个东西怎么做呢,首先你把所有字符串做后缀排序,那么你可以在某个节点上给它的所有询问按照 \(s_i[j \dots ]\) 的大小排序,你发现你把它们最终走到的节点写下来,这样排序以后就是某种 DFS 序,所以你可以直接暴力像建立虚树一样去跑,每条边只会经过一次。这样不是复杂度是子树大小和吗?但是因为先前已经做了轻重链剖分,这里所有的 \(p\) 都是轻子树根,轻子树大小和是 \(l\log l\) 级别,所以复杂度很对。注意这一步的 Trie 树上移动是不需要在可持久化数组上做的,因为都是按照顺序做的,所以你可以直接用链表维护所有非空
已经获得了 \(base[p, i]\) 和 \(tr[p, i]\) 以后,只需要考虑怎么维护路径信息。继续沿用刚刚求 \(tr[p, i]\) 时候的思路。首先对于每个串 \(i\),把从某个点出发的次数统计一下。然后对于一次从 \(p\) 出发的询问,先走到 \(base[p, i]\),这一段可以用树上差分维护。接下来,分两种情况:如果跳 \(fail\) 链已经把 \(u_p\) 的字符全都删除完了,那么等价于从 \(1\) 开始匹配得到的串的某一个区间加,直接用差分即可维护;如果还有剩下的节点,那么等价于从 \(p'\) 开始匹配,其中 \(p'\) 的意义如上,是 \(fail\) 树上 \(p\) 的祖先,也是原树上 \(fail_{base[p, i]}\) 的祖先。那么你可以按照 \(fail\) 树上的深度倒序做,每次把当前节点的询问处理完以后扔到它的某个祖先上,而重复计算的贡献可以扣除掉。最后就做完了。
代码
你觉得我会写吗?