引入
给定字符串 $S$ 与模式串 $P$。问 $P$ 的最长的是 $S$ 的子串的前缀的长度。
我会暴力!
$O(nm)$,非常显然。不讲。
喜欢哈希的小朋友们你们好啊!
注意到答案具备单调性。于是我们使用二分,外加哈希来检验。
复杂度 $O(n\log m)$,其中 $n$ 是 $S$ 的长度,$m$ 是 $P$ 的长度。
已经很优秀了,但是还不够,毒瘤出题人出了 $n=m=3\times 10^7$ 的数据范围!
等价重写
Hash 的算法相当于二分匹配前缀长度,然后枚举前缀匹配起始点。
重写为另一种等价的形式:枚举前缀匹配的起始点,然后二分产生的匹配的长度。
现在,Hash 的问题在于每次重新二分一看就非常不优。
当然我不是让大家写分散层叠(分散层叠也完全用不上)。但是我们获得了一些优化的可能。
包的
假设现在我们在一个匹配起始点 $i$,有一个长度为 $l$ 的匹配(意味着匹配的结束点在 $e = i + l - 1$)。
考虑一个起始点在 $i$ 之后的匹配:假设起始点为 $j$。
有两种可能:
- $j \leq e$
- $j > e$
我们先处理所有第一种情况,然后我们重置 $i = e + 1$ 就可以继续处理第二种情况。
注意到第一种情况具备非常神奇的特性:$[j, i + l)$ 这段下标区间内 $S$ 的子串,等于 $P$ 长度为 $i + l - j$ 的前缀,而同时,当前起始点在 $i$ 的匹配中,与这部分对齐的一段后缀也等于这段子串!
(图示:)
| |
相当于,$P$ 的长度为 $l$ 的前缀有一个长度为 $i+l-j$ 的公共前后缀。
反过来,我们得到了一个第一类匹配存在的必要条件:
$P$ 的长度为 $l$ 的前缀有一个长度为 $i+l-j$ 的公共前后缀。
我们的目的是穷尽所有的前缀匹配(的开始位置)以计算最大前缀匹配长度。那么不妨在想象中将匹配开头升序排序。在找到一个可能开头 $i$ 之后,我们随即检查它后面的第一个的可能开头 $j$。
假设它是第一类的。如果不是,容易处理。
注意到 $j$ 最小意味着 $i+l-j$ 最大,也就是说这段公共前后缀的长度是最大的。
那么就引出了我们的主角:border(包的)——字符串最长公共严格前后缀。
注意:是“严格”的。因为整个前缀本身作为平凡公共前后缀会导致算出 $j=i$,即原地不动。
如何匹配?
非常简单。每次在位置 $i$ 找到一个匹配之后,直接跳到 border 位置($e-\mathrm{border} + 1$)找下一个匹配,如此反复。
如果找不到(border 长度为 0),那么暴力指针加一,找下一个匹配。
注意到这一定是对的。因为我们每次都找“未处理集合中的第一个”。
如何预处理?
border 很好,怎么算?
我们需要计算模式串每个前缀的 border。那么不难想到每个前缀利用上一个前缀的结果。
对于长度为 $i+1$ 的前缀,假设它有一个公共前后缀,那么这个后缀必然包含 $i+1$。
注意到前缀去掉 $i+1$ 对应的字符的部分和后缀去掉 $i+1$ 得到的东西是相等的,这是长度为 $i$ 的前缀的一个公共前后缀。同样的,必须是严格的(因为我们要算出 $i+1$ 长度的前缀的 严格 公共前后缀)。
于是有重要结论:长度为 $i+1$ 的前缀的 border 长度最多是长度为 $i$ 的前缀的 border 的长度加一。
得到了这个约束之后,我们肯定想到枚举这个长度为 $i$ 的前缀的公共前后缀。并且由于要最大化长度,这个公共前后缀也越长越好。
那么我们肯定优先考虑 border。如果 border 不行怎么办呢?
答案是取 border 的 border。然后这个公共前后缀的 border……如此循环往复,直到没有 border(border 长度为 0)。
看起来似乎是一个贪心。为什么是对的?
因为这样可以取遍所有的公共前后缀。注意到对于两个不同长度的公共前后缀 $b, t$($b$ 更长),$t$ 在前缀的那一段是 $b$ 的前缀,在后缀的那一段是 $b$ 的后缀,所以 $t$ 也是 $b$ 的公共前后缀。
所以除了 border 外的所有公共前后缀,都是 border 的公共前后缀,那么自然我们每次取“最长的”就能取遍所有。
复杂度?
看上去枚举所有公共前后缀简直暴力完了。复杂度凭什么是对的?
还是那句话。关键结论:
长度为 $i+1$ 的前缀的 border 长度最多是长度为 $i$ 的前缀的 border 的长度加一。
预计算阶段,我们每次计算一个新的前缀最多会回退旧前缀的 border 长度次。然后 border 长度减少至少这么多。
这里有点类似单调栈的复杂度分析:我们的每个新字符贡献 border 长度的一次增长,这次增长之后贡献常数大小的计算量。
匹配阶段,同理:每个主串中字符匹配时最多使得匹配前缀大小加一,而失配时回退不会超过当前前缀的大小次。均摊复杂度依然是对的。
所以总复杂度就是全线性。
小拓展
注意到这个东西能够自然拓展到全模式匹配上:只需要考虑前缀匹配的长度等于模式串长度的那些匹配就行了。