一个用SAM维护多个串的根号特技
一个用SAM维护多个串的根号特技
基本介绍
在多个串的字符串题中,往往会出现一类题需要用到某个子串是否在一些母串中出现。此时对于 \(\text{parent}\) 树的 \(\text{right}\) 集合而言,问题并不关心某个具体位置而只关心是否有某个 \(\text{endpos}\) 在指定母串中。
那么对于 \(\text{parent}\) 树上的来自同一个母串的节点而言,其对祖先的贡献都是可以替代的,并不需要重复标记其某个祖先 \(\text{right}\) 集合中是否存在一个 \(\text{endpos}\) 来自这个母串。
于是我们维护来自这个母串的所有节点对 \(\text{parent}\) 树的贡献复杂度等价于所有来自这个母串的叶子节点在 \(\text{parent}\) 树上的并的大小。(此处叶子指的是来自这个母串且其子树内不存在来自这个母串节点的节点)
分析这些叶子在树上的并的大小的具体规模,有一个显然的上界就是 \(\sum_{i=1}^{n}2len_i-1\) ,其中 \(len_i\) 是第 \(i\) 个串的长度,对其建后缀自动机的节点数上界为 \(2len_i-1\) ,对 \(n\) 个串一起建可以得到这个显然的上界。
观察这棵并起来的树的性质可以发现,这棵树上的每一个节点的 \(\text{right}\) 集合都包含来自这个母串的 \(\text{endpos}\) ,而每个节点的父节点都是该节点所代表串的一个后缀且至少长度减少 \(1\) 。那么考虑这个串每一个前缀的最大贡献就是这个前缀的长度,所以可以得到另外一个上界 \(len_i^2\) 。
假设这 \(n\) 个串的总长为 \(S\) ,那么 \(\sum_{i=1}^{n}2len_i-1\) 可以看做 \(2S\) ,所以对于每一个母串都维护贡献的复杂度是 \(\sum_{i=1}^n \min(2S, len_i^2)\) ,且 \(\sum_{i=1}^n len_i = S\) 。根据均值不等式可以得到 \(n\) 和 \(len_i\) 都取 \(\sqrt{2S}\) 时达到上界,总复杂度 \(O(S\sqrt{2S})\) 。
然而事实上后面那个下界不容易卡满,因为你需要构造一个字符串其每一个前缀都能在 \(\text{parent}\) 树上分支出最长的一条链,所以这个根号实际上跑起来比某些大常数的一个 \(log\) 做法还快。
例题
BZOJ3277 字符串
题意:给出 \(n\) 个字符串,对于每一个字符串求出其有多少个子串在至少 \(k\) 个字符串中出现过。
可以说是这个特技的模板题了,不过线段树合并可以做 \(O(nlogn)\) ,这里就只说根号做法吧。建出后缀自动机后枚举每一个串的叶子节点暴力往上跳给祖先节点出现在不同的母串的次数 \(+1\) 即可,已经被别的该串节点遍历过就跳出,复杂度就是上述的 \(O(S\sqrt{2S})\) 。
NOI2018 你的名字
题意:对于每一个询问串,求出其有多少个不同子串在母串的 \([L, R]\) 之间出现过。
将问题转化为求有多少个子串在询问串和母串的 \([L, R]\) 之间共同出现即可,暴力枚举的同时额外加一个线段树合并判是否在 \([L, R]\) 即可,复杂度 \(O(S\sqrt{2S}logS)\) 。不过套上 \(log\) 以后会因为第二个上界被卡高后被叉掉,但是 \(68pts\) 不需要线段树合并判断,可以确保通过。实际上3s内跑过了所有测试点
Sum of Squares of the Occurrence Counts 加强版
没加强之前这个特技不能做
题意:给出 \(n\) 个串,对于每一个串 \(i\) 求出其所有子串在串 \([1,i]\) 之间的出现次数的平方和。
对于所有串建后缀自动机,用线段树合并维护出 \(\text{parent}\) 树上每一个节点拥有来自哪几种母串的 \(\text{endpos}\) ,以及每一种母串对应的 \(\text{endpos}\) 数量。考虑每一个母串对所有线段树大小之和的贡献就是叶子的并,于是暴力遍历合并后的每一棵线段树的总复杂度就是 \(O(S\sqrt{2S})\) 。直接在线段树上暴力统计每一个 \(\text{parent}\) 树上节点对每一个串的答案的贡献即可,总复杂度 \(O(S\sqrt{2S}+SlogS)\)。
后记
如果你想知道会了这个根号特技有什么用,我也说不清。对于我这种字符串菜鸡来说,写简单好写的做法比套上各种数据结构好调多了,这个特技牺牲了一些时间效率,但是大大简化了思维难度和代码难度。当然如果您是神仙完全可以去秒正解。