hihoCoder #1783 又一个重复计数

题目大意

给定一个长度为 \(n\) 的字符串 \(S\),定义函数 \(f(S)\) 表示 \(S\) 的不同回文子串的个数。对于 \(1\le l \le r \le n\),定义 \(S[l,r]\) 为字符串 \(S\) 的第 \(l\) 个字符到第 \(r\) 个字符组成的字符串。

求 $\sum_{l= 1}^{n} \sum_{r=l}^{n} (r - l + 1) f(S[l, r]) $ 。

分析

前置知识

  1. 长度为 \(n\) 的字符串至多有 \(n\) 个不同的回文子串。
    提示:可以从 Manacher 算法的过程与性质着手去证明。

  2. 长为 \(n\) 的序列共有 \(\binom{n+1}{2}\) 个区间,所有区间的长度之和是 \(\binom{n+2}{3}\)
    证明:只证后一部分,前一部分的证明是类似的。考虑从 \(n+2\) 个物品中选出三个物品的某个具体结果。设所选出的三个物品的编号从小到大依次为 \(a, b, c\) 。若 \(c \le n\),把此结果与区间 \([a,c]\) 相对应;若 $ c > n$ 且 \(b \le n\),把此结果与区间 \([a, b]\) 对应;若 \(b > n\)\(c > n\),把此结果与区间 \([a,a]\) 相对应。不难验证:(1)每个组合结果都与唯一的区间相对应;(2)每个长度为 \(l\) 的区间都有且仅有 \(l\) 个组合结果与之对应。证毕。

解法核心

一个回文子串 \(P\) 对答案的贡献为「包含 \(P\) 的区间」的长度之和。我们考虑「包含 \(P\) 的区间」的长度之和 \(N_P\),则 \(P\) 对答案的贡献为 \(\binom{n+2}{3} - N_P\)

下面考虑如何计算 \(N_P\)

将「不包含 \(P\) 的区间」按照其右端点 \(r\) 分类。设 \(P\)\(S\) 中的「出现位置」从左到右依次是 \([l_1, r_1], [l_2, r_2], \dots, [l_k, r_k]\),它们都满足 \(r_i - l_i + 1 = |P|\)\(|P|\) 表示 \(P\) 的长度。

对于 \(1\le r \le r_1\) 的情况,我们有 \(1 \le l \le r\),这些区间的长度之和(即它们对 \(N_P\) 的贡献)亦即「\([1, r_1 -1]\) 的子区间长度之和」为 \(\binom{r_1 + 1}{3}\)

对于 \(r_i \le r < r_{i+1}\) 的情况(不妨定义 \(r_{k+1} = n+1\)),我们有 \(l_i < l \le r\),这些区间的长度之和为 \(\binom{r_i+1 - r_i + |P|}{3} - \binom{|P|}{3}\)
推导:「区间 \([l_{i}+1, r_{i+1} -1]\) 的子区间长度之和」 - 「区间 \([l_i + 1, r_i-1]\) 的子区间长度之和」;亦即 \(\binom{r_{i+1} - l_{i} + 1}{3} - \binom{r_i - l_i + 1}{3}\),将 \(l_i = r_i - |P| + 1\) 代入即得 $\binom{r_{i+1} -r_i + |P|}{3} - \binom{|P|}{3} $,是关于 \(P\)\(r_{i+1} - r_{i}\) 的三次多项式。为了书写简便,令 $x = r_{i+1} - r_{i} $,
\begin{aligned}
\binom{x+ |P|}{3} - \binom{|P|}{3} &= \frac16 [(x+|P|) (x + |P| -1) (x+ |P| - 2) - |P| (|P| - 1) (|P| - 2) ] \\
&= \frac16 [x^3 + (3|P| - 3 ) x ^ 2 + (3|P|^2 - 6|P| + 2) x]
\end{aligned}

我们可以使用 Manacher 算法配合 Hash 或者直接用 eertree 找出所有回文子串,使用后缀数组或者后缀自动机配合倍增算法识别出每个回文子串出现位置的右端点,通过启发式合并或者可持久化线段树合并或者其他数据结构维护 \((r_{i+1} - r_i)^e\) 的和,其中 \(e = 1, 2, 3\) 。时间复杂度为 \(n\log n\)

上面引自出题人发布的题解,我补了一些细节。

这道题我是照着题解的思路做出来的。我的做法是用 Manacher 算法找出所有回文子串,用字符串哈希给每个不同的回文子串赋一个编号,编号从 \(0\) 开始。题解中最后一段红字那句话我没读懂(后缀数组我学过,但不熟悉;后缀自动机我不会;我也不懂其中提到的「倍增算法」是什么),于是我转而去维护一个回文子串 \(P\) 出现位置的中心 \(c_1, c_2, \dots, c_k\)

子串 \(S[l, r]\) 的中心定义为 \(\lceil (l+r)/2 \rceil\),亦即,长度为奇数的串中心为中间那个位置,长度为偶数的串的中心为后半部分开头那个位置。注意到,\(r_{i+1} - r_{i}\)\(c_{i+1} - c_{i}\)

注意到回文子串的中心构成一种树形结构:较长的回文子串的中心也是它所包含的较短的回文子串的中心,例如 aba 的中心亦是 b 的中心,abba 的中心亦是 bb 的中心。我们建一棵树:每个节点代表一个回文串,回文子串 \(P\) 的父亲是去掉 \(P\) 两端的字符所得的回文串。每个节点都对应着一个集合 \(C_P\)——这个节点所代表的回文串 \(P\)\(S\) 出现位置的中心的集合。

我们自底(叶子)向上(根)启发式合并各个节点对应的集合,得到 \(C_P\) 之后就可以算出 \(N_P\)

实现细节

  • 用 Manacher + 哈希时,一般都是把回文串的一半(通常是右半边)进行哈希编码,因此这道题宜将长度为奇数和长度为偶数的回文串分开计算,否则 ab 既有可能是 aba 的半边,也有可能是 abba 的半边,难以区分。

  • 按照「节点 \(v\) 的编号一定比其父节点 \(p\) 的编号大」的原则给节点(即回文子串)编号。记录下每个节点的父亲的编号,按编号从大到小合并就相当于在树上自底向上合并。

  • 要注意哈希冲突的问题,如果你用的哈希被卡了,不妨换一个底数或模数试试。我以 257 为底数,\(10^9+7\) 为模数,被卡了;把底数换成 131 就过了。

总结

现在来考虑 Manacher 算法和字符串哈希在这个方法里起到的作用。

  1. 找到所有的回文子串,并给不同的回文子串编号。
  2. 支持 \(O(1)\) 查询回文子串 \(P\) 的父亲的编号。

如果有一种自动机能支持上述两种操作,那么就可以替代 Manacher + 哈希。

posted @ 2018-08-08 14:04  Pat  阅读(197)  评论(0编辑  收藏  举报