Manacher算法
Manacher算法使用场景
Manacher算法用于解决求一个字符串的最长回文子串长度问题,且时间复杂度为O(N)。
一般的求一个字符串的最长回文子串问题的方法
举个栗子:如下图
对于字符串str,将str重写为str’的形式,即给每个字符前后加‘#’(不一定为#,也可以为其他字符)。然后使用下标指针,从str‘[0]开始遍历,将每个字符往两边“扩”,若两边相等则len+1,“扩”至不等,则将红色箭头后移。以此类推。
以上图为例,显然str’[0]时不能扩,因此len=0,红色箭头后移;str’[1]时只能扩一次,因此len=1,红色箭头后移;依次类推,直到str’[9]时能向两边3次,故len=3。显然以后的len都没有str’[9]的了len大,因此最长回文子串为232。
Manacher算法详解
显然,经典算法的时间复杂度可以高达O(N^2),因此我们使用Manacher算法来优化经典算法,使时间复杂度达到O(N)。
在了解Manacher算法以前,先了解几个概念
回文半径,回文直径,回文半径数组,之前扩的所有位置汇总所到达的回文的有边界R,最远边界的中心点C
举个栗子来说明这几个概念:如图
则str的:
最长回文半径为:4
最长回文直径为:7
之前扩的所有位置汇总所到达的回文的有边界R:每个字符的最长回文所到达的最右边的下标
最远边界的中心点C:每个字符的最长回文的中心所在的下标
最长回文半径数组为:{-1,0,2,2,4,4,8,8,8,8,8,8}
如下图:
Manacher算法
<1> 首先按照一般的回文的方法,将字符串转化为带‘#‘的形式
<2> 然后依次遍历每个字符,按照一般方法向外扩,扩不了就往下遍历。在扩的过程中可以得到每个字符的R和C;
<3> 若遍历到的当前位置,没有在最右边的最长回文右边界中,则使用<2>的方法暴力扩
<4> 若遍历到的当前位置i,在最右边的最长回文右边界中,找到i与R对应的C的对称位置i’,如图所示:
分为下面三种情况进行分析:
case 1 :
i’的回文域完全在L,R中,如下图:
因为i与i’的回文半径是一样的,所以i位置不用扩也知道没有c位置的回文半径大,因此直接就跳过了
case2:
i‘的回文域部分在L,R中,如下图:
此时,i的回文半径为i位置到R的距离,因此i位置也不用扩了,因为没有c位置的回文半径大。
case 3:
i’的回文域的边界恰好落在c位置回文半径的界上,如图:
此时i的回文半径也刚好到边界R,因此直接跳到R+1的位置重复<3><4>过程。
Manacher算法C++代码实现
#include<iostream>
#include<string>
#include<math.h>
using namespace std;
inline int max(int a, int b){ return a > b ? a : b; }
inline int min(int a, int b){ return a > b ? b : a; }
char* changeStr(string str)//该函数间str转为带#的形式
{
char*s = (char*)str.c_str();
char*re = new char[str.length() * 2 + 1];
int index = 0;
for (int i = 0; i < str.length() * 2 + 1; ++i)
{
if (i % 2)
{
//若i为奇数
re[i] = s[index++];
}
else
{
re[i] = '#';
}
}
return re;
}
int max_LS_length(string str)//得到最长回文子串的长度
{
if (str.length() == 0)return 0;//若字符长度W为0 直接返回0
char *s = changeStr(str);//转化字符形式
int len = str.length() * 2 + 1;
int *arr = new int[len];//回文半径数组
int C = -1;//回文中心
int R = -1;//最大回文右边界+1的位置,即最大回文右边界为R-1
int Max = INT_MIN;//最大值
for (int i = 0; i < len; ++i)//遍历每个位置
{
//所有不用验的i位置的回文半径:
//当i>=R时,说明i在最大回文右边界(R-1)外,则不用验的回文半径为1,也就是i位置的字符本身
//当R>i时,说明i在最大回文右边界(R-1)内,则不用验的回文半径为R-i与arr[i']=arr[2*C-i]之中最小的那个。(这个就是case1,case2,case3中不用扩的部分)
arr[i] = R>i ? min(arr[2 * C - i], R - i) : 1;
//接下来,从不用验的部分往后走,然后扩
while (i + arr[i]<len&&i-arr[i]>-1)
{
if (s[i + arr[i]] == s[i - arr[i]])
arr[i]++;//若满足回文则往外扩
else
break;//否则跳出不扩了
}
if (i + arr[i] > R)
{
//若当前位置加上当前位置的回文半径大于R,说明最大回文半径右边界+1(R)要跟新,同时C也要跟新为i
R = i + arr[i];
C = i;
}
Max = max(Max, arr[i]);//跟新max
}
//释放堆空间
if (s != NULL)
delete[]s;
s = NULL;
if (arr != NULL)
delete[]arr;
arr = NULL;
return Max-1;//因为处理串(带#的串)回文半径的长度,与原始回文串(不带#的串)的长度关系就是就是max-1的关系
}
int main(void)
{
string str1 = "abc1234321ab";
cout << max_LS_length(str1);
system("pause");
return 0;
}