manacher 算法通常用来在 时间复杂度内求解字符串最长回文子串 / 以串内一点为中心的最长回文半径问题。
一些前置定义:
- 回文串:定义一个长度为 下标从 开始的字符串 是回文的,当且仅当 。
- 回文子串:定义串 是串 的回文子串,当且仅当 为回文串,且 为 的子串。
- 最长回文子串:定义一个长度为 的字符串的最长回文子串,为其所有回文子串中长度最长的子串。
- 最长回文半径:定义一个长度为 下标从 开始的字符串 在 位置的最长回文半径 是最大的整数满足 。
- 回文中心:定义一个长度为 下标从 开始的回文串 的回文中心:若 为奇数则 串的回文中心位于 ,否则回文中心位于 和 两位置之间。
问题求解:
给定一个长度为 的字符串 ,求其最长回文子串的长度。
Data Range: 。
算法流程:
这里认为 串下标从 开始。
设 表示 位置处最长回文半径的长度。由于存在长度为偶数的回文串,此时回文中心位于两字符之间,为了方便处理,在这里对原字符串进行一次特殊的操作:即将原字符串中任意两字符之间,以及头尾两处均添加一个特殊字符(例如:$)。此时字符串长度由 变为 ,原来的每一个长度为偶数的回文串均变为由特殊字符为回文中心的回文串。例如:串 abcbaab 变为 $a$b$c$b$a$a$b$,此时串 abcba 变为 $a$b$c$b$a$,以 c 字符为回文中心;串 baab 变为 $b$a$a$b$,以两个 a 字符之间的 $ 为回文中心。
然后考虑求解 数组。首先思考怎么暴力求解。很容易想到对于字符串的每一个 都暴力向左向右扩展直到左右两侧的字符不相同为止。期望跑的很快,但是很明显可以被形如 aa....a 的字符串给卡到 。这样做很多判断的信息都被浪费掉重新又判断了一遍,因此考虑类似于 kmp,利用这些信息优化计算 数组的时间复杂度。
从左往右按顺序依次枚举每一个 。假设当前已经处理了 要计算 的值。我们记录 r 和 nowmid 两个信息分别表示以 为回文中心时,回文中心可以覆盖到的下标最大的点下标最大是多少(r)以及此时回文中心的下标是多少(nowmid)。
然后分类讨论 和 r 之间的关系:
1.
直接暴力扩展更新答案即可。
2.
考虑如何快速的计算 的值:此时已经计算得到存在一个以 nowmid 为回文中心的回文串,其回文半径覆盖的下标最大的点下标为 r,那么同样可以直接计算出覆盖的下标最小的点下标为 l。此时容易对称的计算 关于 nowmid 对称的点下标为 ,则此时已经计算出以 点为中心的回文半径长度,直接用 的值更新 即可。
但是若此时 的值超过了当前回文串半径 r,那么超过的部分由于不在回文串内不能保证回文,因此只能将 更新到 ,然后继续暴力扩展。同样的,若 的值恰好为 ,那么也不能保证为最长回文串,也需要继续暴力扩展。有一个简单的写法是更新完 的值之后直接暴力扩展(容易发现剩下不能扩展的情况暴力扩展的时间复杂度为 )。计算完 后,更新 r 和 nowmid 的值即可。
代码实现:
int main()
{
scanf("%s", s + 1);
int n = strlen(s + 1);
for (int i = 1; i <= n; ++i)
t[i * 2] = s[i], t[i * 2 + 1] = '$';
t[1] = '$';
n = n * 2 + 1;
for (int i = 1; i <= n; ++i)
s[i] = t[i];
int r = 0, nowmid = 0;
for (int i = 1; i <= n; ++i)
{
if (i < r)
p[i] = min(p[2 * nowmid - i], r - i);
else
p[i] = 1;
while (i + p[i] <= n && i - p[i] >= 1 && s[i + p[i]] == s[i - p[i]])
++p[i];
if (p[i] + i > r)
r = i + p[i], nowmid = i;
}
cout << *max_element(p + 1, p + n + 1) - 1 << '\n';
return 0;
}时间复杂度分析:
很显然除了暴力更新的部分以外时间复杂度都是 ,而暴力更新部分最多扩展 次(扩展之后 r 会变大,但是 r 不会变小,最多从 变到 ),因此总时间复杂度为 。空间复杂度也为 ,可以通过。
喜欢的话,留下你的评论吧~