【字符串】 manacher算法
Algorithm
Task
给定一个字符串,求其最长回文子串
Limitations
要求时空复杂度均为线性且与字符集大小无关。
Solution
考虑枚举回文串的对称轴,将其对应的最长回文子串长度 \(len\) 求出来,取最大值即为答案。
首先回文串有两种,长度为奇数的和长度为偶数的,第一种的对称轴是一个字符,第二种的对称轴在两个字符之间。
为了将两种情况统一起来,我们将原字符串的每两个相邻字符之间和首位字符前后都加上同一个不在字符集内的其他字符,例如,将 \(aaa\) 变成 \(\#a\#a\#a\#\),这样字符串的对称轴一定是一个字符了。
定义回文半径 \(r\) 为对称轴到回文串边界的字符数量,也即对称轴的下标,考虑新字符串的回文半径一定是 #
和其他字符交替出现,并以 #
结尾,因此 r
一定是奇数,而其中真正的的字符数量为 \(\frac{r - 1}{2}\),加上另一侧得字符,得到该回文串对应原字符串的回文长度为 \(\frac{r - 1}{2} \times 2~=~r - 1\)。
我们从左到右扫描新字符串,设当前扫描到了 \(i\),则 \(\forall j \in [1, ~i)\),\(len_j\) 已经被计算完毕。
设之前的所有回文子串中,右端点最大的为 \(pos\),其对应对称轴为 \(mid\)。
分两种情况讨论。
第一种情况,\(i < pos\),则 \(i\) 在以 \(pos\) 为右端点,$ mid$ 为对称轴的大回文串中。
找到 \(i\) 关于 \(mid\) 的对称点 \(j\) ,若 \(j\) 对应的回文串的左端点不在大回文串的左侧,由于回文串的对称性,对称过去以后 \(i\) 的对应回文串应该与 \(j\) 相同,于是有 \(len_i = len_j\)。
否则,在回文串内部的部分一定是对称的,对于 \(pos\) 右侧的部分,则暴力向右匹配即可。
第二种情况,\(i \geq pos\),则直接进行暴力匹配。
考虑复杂度:每次暴力匹配,\(pos\) 会自增 \(1\),而单次的复杂度是 \(O(1)\) 的,因此暴力匹配的总复杂度是 \(O(|S|)\) 的,而剩下的操作都是 \(O(1)\) 的因此总的时间复杂度是线性的。
Sample
P3805 【模板】manacher算法
Description
给定一个只由小写字母组成的回文串 \(S\),求最长回文子串长度。
Limitations
\(|S| \leq 1.1 \times 10^7\)
Solution
板板题,依然需要注意等号的位置。
在实现中,可以在字符串结尾添加另一个无关字符,这样可以保证匹配时不会越界,并且不用手动判断。
Code
#include <cstdio>
#include <algorithm>
const int maxn = 22000007;
int n, ans;
char S[maxn];
int len[maxn], mid[maxn];
void ReadStr();
int main() {
freopen("1.in", "r", stdin);
ReadStr();
for (int i = 1, pos = 0; i <= n; ++i) {
if (i >= pos) {
int l = pos = i;
while (S[l - 1] == S[pos + 1]) { --l; ++pos; }
len[i] = pos - i + 1;
mid[pos] = i;
} else {
int j = (mid[pos] << 1) - i;
if (len[j] < (pos - i + 1)) {
len[i] = len[j];
} else {
int l = (i << 1) - pos;
while (S[l - 1] == S[pos + 1]) { --l; ++pos; }
len[i] = pos - i + 1;
mid[pos] = i;
}
}
ans = std::max(ans, len[i]);
}
qw(ans - 1, '\n', true);
return 0;
}
void ReadStr() {
static char tmp[maxn];
int _len = 0;
do tmp[++_len] = IPT::GetChar(); while ((tmp[_len] >= 'a') && (tmp[_len] <= 'z'));
tmp[_len--] = 0;
for (int i = 1; i <= _len; ++i) {
S[++n] = '#';
S[++n] = tmp[i];
}
S[++n] = '#'; S[++n] = '$';
}