标题来自原神
算法概述
Maracher 算法
用途:寻找回文串,最板子的情况下用于字符串的回文子串计数
给定一个字符串 \(S\),求出它全部的回文子串
容易想到一种暴力的 \(n^{2}\) 做法,即枚举全部中心点,开双指针向两边扩展,每扩展一次就提供 \(1\) 的贡献.
事实上,对于这样的算法来说,判断偶回文串会不太方便:因为它没有中心点.
因此,我们考虑在原字符串里加入一些莫名其妙的字符,如下:
cthoiissb
c#t#h#o#i#i#s#s#b
这样做,我们发现,当枚举原字符串内的点作为中心点时,就相当于在枚举奇回文子串,当枚举特殊字符作为中心点时,就相当于在枚举偶回文子串. 我们通过上述方法完成了对奇偶子串的统一.
下面我们来考虑对进行的转移进行优化
首先我们引入一些概念:
定义回文半径表示回文串的长度除以二的值(显然,在处理之后,回文串全都变成了奇回文串,因此直接向下取整即可),我们这样定义是因为处理后的回文半径长等于处理前的回文串长度减一,如下:
cthtc length=5
c#t#h#t#c length=4
类似地,我们还可以求出该回文串在原串中的位置:
oicthtc
o#i#c#t#h#t#c length=4[8]
可以发现,cthtc
的对称中心 h
在原字符串中的位置为 \(5\)(以下默认下标从 \(0\) 开始),在处理后的字符串中的位置为 \(8\),而回文半径长为 \(4\),考虑到 \(8+1-4=5\),发现再多举几组也是如此的性质,即 “原字符串位置等于处理后字符串位置加一减去回文半径”,考虑到这样有点麻烦,因此在处理后的字符串前插入一个不同的字符(也是为了防止在 \(p=0\) 时访问负数下标),实际上如果保守一点的话,末尾也是需要插入特殊字符,只不过因为末尾有一个 \0
,因此不需要插入. 需要注意的是,如果真的要插入的话,首位字符不能相同,否则直接就把他们两个匹配上了,会影响答案.
到现在我们的算法还是 \(n^{2}\) 的,下面我们来考虑优化这种转移.
图源
图例中的 \(T\) 表示了一种可能的字符串,下方的每一位的 \(P\) 表示了以当前位为中心的最长回文半径. 一般来说,这个 \(P\) 数组需要我们从头到尾扫一遍来求,这一点我们无从优化,我们来考虑如何才能从之前的状态跳到当前的状态.
如图,我们已经求出了一部分 \(P\) 的值,注意到图中有一个很大的 \(P_{11}=9\),我们考虑利用一下它,因此用虚线标出它的中心与左右边界. 现在我们的目标是求出 \(P_{13}\)
假如 \(P_{11}\) 左右两边对称的话,可以想到我们只需要找到需要求的点在另一边的对称点,那么对称点的 \(P\) 值一定就也是当前点的 \(P\) 值:因为回文的性质,既然对称点是回文,对称过来也一定是回文,并且因为之前求的是最大值,因此也不存在一个更大的值了,所以直接转移即可.
下面我们再来考虑另一种情况:
现在的目标是求 \(P_{15}\),按照刚才的思路,现在我们应该去找对称点 \(P_{7}\),但是我们发现此时无法直接进行转移.
刚才我们转移是建立在一个很大的回文区间 \(P_{11}=9\) 上的,因为两边的回文区间全部都在这个大的回文区间内,因此我们才能保证两边的字符是相等的. 现在区间超出了大的对称区间的范围,因此不能保证超出范围的部分是相等的,也就不能直接转移了.
不过这样的情况还是有利用价值的,考虑可以先把能保证相等的转移了,即转移到大区间的边界,剩下的没有办法,可以直接进行暴力转移,为下一次转移助力. 可以证明这样做的复杂度是 \(O(n)\) 的.
习题
P3805 模板
To be added