一、前言
Manacher 算法用于求解字符串中最长回文子串的问题,可以做到O(n)的时间复杂度
我们先来看看以下几个概念:
回文串:正序和逆序相同的字符串(aabaa,abba)
最无脑做法:暴力列举所有子串,在判断是否回文(O(n3))
优化求解方法:按照该思路,我们会先遍历整个字符串,对于遍历到的每个字符,以它为中心,再依次比较该字符左右位置的字符是否相同,这样进行下去。
但这样会带来如下的问题:
对于给定的字符串abba,按照此方法其最长回文子串长度就为1,但是实际上,显而易见地,abba本身就是个回文串,其长度为4,所以,对于偶回文串,这样地方法就显得不太适用了。
解决办法:将原字符串首尾及相邻的字符之间任意插入一个相同字符,该字符可以是任意字符,包括原字符串中出现的,然后按照原求解方法求解
至于为什么插入的字符可以为任意字符,因为在插入字符的回文串中原回文串里的字符始终与原回文串里的字符比对,而插入的字符始终与插入的字符比对,所以插入的字符无论是什么都不会影响最终的结果
二、优化求法
在正式讲Manacher之前,我们通过上面谈到的思想,先用该思想来直接解决求解字符串中最长回文子串的问题:
#include<iostream>
using namespace std;
string changeString(string str) {
int len = str.size() * 2 + 1;
string ans(len, 'a');
int index = 0;
for (int i = 0; i < len; i++) {
ans[i] = (i % 2 == 0 )? '*' : str[index++];
}
return ans;
}
int process(string str) {
//将str首尾及相邻字符之间插入任意相同字符
str = changeString(str);
int len = str.size();
int max = INT_MIN;
for(int i = 0; i < len; i++){
int radium = 1;
while (i - radium > -1 && i + radium < len) {
if (str[i + radium] == str[i - radium]) {
radium++;
}
else {
break;
}
}
if (radium > max) {
max = radium;
}
}
return max - 1;
}
int main() {
string str;
cin >> str;
cout << process(str) << endl;
}
该算法的时间复杂度为O(N*N),Manacher算法能够在此基础上再次优化,做到O(N)的时间复杂度,下面我们来看看吧
三、Manacher算法思想
现在设置两个变量,变量C表示目前扩展到最右边位置的回文子串的中心下标,变量R表示最右边位置的下一个下标(R设置为最右边位置的下标也可以,按照我的方法设置可以便于计算)
那么遍历该字符串,此时遍历到的下标i与i关于C对称的i'会出现以下四种情况:
1.以i‘为中心的回文串在以C为中心的回文串的内部
在此种情况下,以i为中心的回文串长度就等于以i'为中心的回文串长度,因为此时i与i'在以C为中心的回文串的内部且关于C对称,如果以i为中心的回文串长度增加或减少,相应的,以i'为中心的回文串的长度也会增加或减少
2.以i'为中心的回文串左边界恰好与以C为中心的回文串的左边界对齐
在此种情况下,以i为中心的回文串长度大于等于以i'为中心的回文串的长度,因为我们只能确定在以C为中心的回文串内以i为中心的部分是回文的,我们还需要继续向右拓展才知道以i为中心的回文串的长度究竟是多少
3.以i'为中心的回文串的左边界在以C为中心的回文串外部
此时以i为中心的回文串的右边界恰好与以C为中心的回文串的右边界对齐,因为i与i'此时仍然关于C对称,如果i的回文串右边界继续向右扩展的话,那么显然i的向右扩展部分与i’的向左扩展部分对称,此时以C为中心的回文串的长度会增加,故i不能再向右拓展,此种情况与2情况对称
4.i在C的右边界外
此时按照常规方法求解即可
四、Manacher算法代码
#include<iostream>
using namespace std;
//进行字符串的处理 1221-->#1#2#2#1#
string manacherString(string str) {
int len = str.length();
string str2(2 * len + 1, 'a');
int index = 0;
for (int i = 0; i!=2*len+1; i++) {
str2[i] = (i&1)==0 ? '#' : str[index++];
}
return str2;
}
int manacher(string s) {
if (!s.length()) {
return 0;//若字符串长度为0,返回0
}
string str = manacherString(s);//扩增后的字符串1
int len = str.length();
int* pArr = new int[len];
int C = -1;//中心
int R = -1;//回文右边界的再往右一个位置,最右的有效区是R-1位置
int MAX = INT_MIN;
for (int i = 0; i < len; i++) {
pArr[i] = R > i ? min(pArr[2 * C - i], R - i) : 1;//代码优化,包括了列举的种种情况
while (i + pArr[i]<len && i - pArr[i]>-1)//不超出边界
{
//继续向右拓展
if (str[i + pArr[i]] == str[i - pArr[i]]) {
pArr[i]++;
}
else {
break;
}
}
//更新C和R
if (i + pArr[i] > R) {
R = i + pArr[i];
C = i;
}
//记录最大的回文子串长度
MAX = max(MAX, pArr[i]);
}
return MAX - 1;
}
int main() {
string str;
cin >> str;
cout << manacher(str) << endl;
}