Manacher(马拉车)算法
引入
例题解析
题目描述
输入格式
输出格式
输入输出样例
输入:aaa 输出:3
说明/提示
\(1 \le n \le 1.1 \times 10^7\)
思路分析
思路一(Brute Force):
由题目意思我们不难想到一个最暴力的做法,我们可以以字符串中的每个字符为中点,比较左右两个字符是否相等,然后逐渐扩大半径,直至求出结果,代码如下。
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
void solve()
{
string s;
cin >> s;
int maxn = 0;
int n = s.length();
for (int i = 0; i < n; i++)
{
int r = 0;
// 以 i 为中心的奇数长度回文串
while (i - r >= 0 && i + r < n)
{
// 对两侧逐一比较,若是不同则跳出循环,相同则半径+1
if (s[i - r] != s[i + r])
break;
r++;
}
// 更新最大回文长度,注意 r 是半径且包含当前的字符,所以长度是 2*r-1
maxn = max(maxn, 2 * r - 1);
// 以 i 和 i+1 为中心的偶数长度回文串
int l = i - 1;
r = i;
while (l >= 0 && r < n)
{
if (s[l] != s[r])
break;
l--;
r++;
}
maxn = max(maxn, r - l - 1);
}
cout << maxn << endl;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
solve();
}
显然,虽然这个代码的思路没问题,但是效率太低了,我们需要遍历字符串中的每一个位置的字符,同时要以此展开再遍历,时间复杂度可以到达\(O(n^2)\),这对于题目中这个量级的数据显然会超时,所以我们需要更加巧妙的思路,下面就介绍manacher来优化。
思路二(Manacher):
由于偶数串和奇数串的讨论方法稍有不同(由上个思路也可见出),为了方便之后的算法执行,因而在此算法执行前我们首先要对数组进行一个预处理,在开头加上“%#”,并在每个字符后面都加上“#”。例如:
奇字符串 aba -> %#a#b#a#
除去用“%”表示s[0]当做哨兵外,无论奇字符串还是偶字符串都变成了奇字符串,方便后面算法执行。
那么字符串aaba在预处理之后所产生的d[i]数组能用下表表示:
i 1 2 3 4 5 6 7 8 9
s # a # a # b # a #
d[i] 1 2 3 2 1 4 1 2 1
在求解的过程中我们可以发现对于字符“#”的d[i]其实就是刚才暴力求解时回文子串长度为偶数的情况,而字母位置的d[i]便是长度为奇数的情况,此数组d记录了所有情况。同时观察d[i]的结果可以发现,这个字符串的最长子串的长度就是最大的d[i]-1。那么这个d[i]是如何求出来的呢,我们接下来就需要进行d[i]的求解。
我们维护一个区间[l,r],在这个区间内的字符串是一个回文字符串,,接下来我们遍历数组,求每个位置的d[i],在遍历时,我们这个i可以分为三种情况。
首先是i在区间外部,如下图:
对于这种情况因为我们无法根据已知情况继续推导,故采用暴力遍历的方法。
其次,i可以在区间内部,由于回文串的对称性,我们可以基于i关于区间中点的对称点l+r-i,的回文半径d[l+r-i],来判断。此种情况分为两种,其一是i+d[l+r-i]在维护区间[l,r]内,如下图:
对于这种情况,由于未超出维护区间的范围,故对于此i在半径为d[l+r-i]的范围内必然是一个回文串,在此基础上我们再进行暴力遍历排查。
另一种情况是i+d[l+r-i]的范围超出了维护区间[l,r]的范围,如下图:
对于此情况,我们只能保证在维护区间内的情况,故此时i只能保证在半径为r-i+1的情况下有一个回文串,,那么此时d[i]先取r-i+1,再进行遍历。
此时,讨论完成三种情况并做完了暴力遍历后我们便获得了i点正确的d[i]值,如果i+d[i]还在当前区间内我们无需更新,若是超出则需要更新区间使得
l=i-d[i]+1
r=i+d[i]-1
至此,我们完成了所有d[i],的求解,我们最后需要找出最长的回文子串的长度值便可,此时我们只需遍历一遍d[i],找到最大值并-1便是该字符串中最长回文子串的长度了。
以下是完整代码:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 5e5 + 5;
void solve()
{
string s;
cin >> s;
// 预处理字符串
string str = "%#";
for (auto ch : s)
{
str += ch;
str += "#";
}
int n = str.length();
vector<int> d(n, 1);
int l = 0, r = 0;
for (int i = 1; i < n - 1; i++)
{
// 如果 i 在当前回文串的右边界内,则 d[i] 至少等于 d[l + r - i] 和 r - i 中的较小值
if (i < r)
d[i] = min(d[l + r - i], r - i);
// 以 i 为中心,向两侧扩展,比较字符是否相同
while (str[i - d[i]] == str[i + d[i]])
d[i]++;
// 如果以 i 为中心的回文串扩展到了 r 的右边,则更新 l 和 r
if (i + d[i] > r)
{
l = i - d[i] + 1;
r = i + d[i] - 1;
}
}
// 计算最长回文子串的长度,d[i] 是以 i 为中心的回文串的半径,所以长度是 d[i] - 1
int maxn = 1;
for (int i = 0; i < n; i++)
{
maxn = max(maxn, d[i] - 1);
}
cout << maxn << endl;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
solve();
}
练习
除了这题之外还有其他类似的题目,比如求回文子串的个数,或者是求最长回文子串需要将子串输出,这些题目虽然看起来不同,但是其本质都是利用了Manacher算法,在理解掌握例题之后可以尝试自己解决其余类似题目。作者找到几道类似的题目可供练习。