最近研究了一下Manacher算法,本人非科班出身,参考很多大牛资料,最终终于搞明白了,感觉甚是不易,遂做此笔记,整理很多次,思路感觉还算清晰,希望对大家有所帮助!!!纯手工画图,感觉不错的朋友,点个赞哟@_@
主要功能:解决最长回文子串的问题[给定一个字符串,求解其最长回文子串的长度]
1、大体思路阐述
给定一个字符串str = “abbc”,求解其最长回文子串的长度???
对原来的字符串进行预处理[加'#']—>求解辅助数组p—>求解数组p中的最大值max—>最长回文子串的长度为(max-1)
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
原str | a | b | b | c |
|
|
|
|
|
预处理str' | # | a | # | b | # | b | # | c | # |
辅助数组p | 1 | 2 | 1 | 2 | 3 | 2 | 1 | 2 | 1 |
辅助数组是如何求解的呐???
p[i]表示以字符str'[i]为中心的最长回文子串的最右(左)字符到Str[i]的距离(包括Str[i])。以p[4]=3为例说明:str'[4]自身占据一个计数1;str'[3]=str'[5]=b占据一个计数1;str'[2]=str'[6]=#占据一个计数1。所以总计p[4]=3
PS:对str数组预处理,添加‘#’目的是将原来字符串不管是奇数个还是偶数个都处理为奇数个,方便统一编码处理。
2、如何通过代码求解回文半径数组p????
明确一下引入的3个概念
(1)回文半径数组p:p[i]表示以字符str'[i]为中心的最长回文子串的最右(左)字符到Str[i]的距离(包括Str[i])。=>p[i]的值对应为以str'[i]为中心的最长回文半径
(2)回文右边界R:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
str' | # | 0 | # | 1 | # | 2 | # | 1 | # | 3 | # | 1 | # | 2 | # | 1 | # | 0 | # |
p | 1 | 2 | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 10 | 1 | 2 | 1 | 4 | 1 | 2 | 1 | 2 | 1 |
R | 1 | 3 | 3 | 5 | 5 | 9 | 9 | 9 | 9 | 19 | 19 | 19 | 19 | 19 | 19 | 19 | 19 | 19 | 19 |
由此看来:R的大小只增不减,当R[i]<(i+p[i]),则R[i+1]=(i+p[i]),否则R[i+1]=R[i]
(3)回文右边界的中心id:这个概念和回文右边界R相互联系的,id的值表示取得R值的时,对应的回文中心位置。=> id更新与否要看R更新与否。
例如:当R[0]=1时,id=0;当R[1]=3时,id=1;当R2]=3时,id=1;当R[5]=9时,id=5;当R[9]=19时,id=9;当R[14]=19时,id=9
求解数组p的几种可能性
可能性1:当前索引的位置i不在回文右边界R之内,采用暴力扩
| -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
str’ |
| # | 1 | # | 2 | # | 1 | # |
R | -1 | 0 | 2 | 2 | 6 | 6 | 6 | 6 |
根据上表阐述说明:
1)当索引的位置i=0,R[i-1]=-1时:因为i不在回文右边界的内部,所以对于i=0位置只能采用暴力扩,发现i=0时只能扩到自己,即更新回文右边界R[i]=0;
2)当索引的位置i=1,R[i-1]=0时:因为i还不在回文右边界的内部,所以对于i=1位置只能采用暴力扩,发现i=1时可以扩到下标2的位置,即更新回文右边界R[i]=2;
3)当索引的位置i=2,R[i-1]=2时:因为此时索引的位置在回文右边界之内,所以可以不采用暴力扩【manacher算法就是在这里改进的】
根据该可能性,得到如下代码
//当前索引的位置i不在回文右边界R之内:暴力扩
if(i >R)
{
p[i] = 1;//这里需要默认的,固定为1
while((i+p[i]<strlen(man_arr)) && (i-p[i]>-1))
{
if(man_arr[i+p[i]] == man_arr[i-p[i]])
p[i]++;
else
break;
}
}
可能性2:当前索引的位置i在回文右边界R之内
明确一点:i'为i以id为中心的对称点。由id = (i+i')/2,可得:i'=2*id-i
2.1 以str'[i']为中心的最长回文子串包含在L的内部,即i'的回文在L~R内部【时间复杂度:O(1)】
结论:该种情况下,p[i]=p[i'](由于此时在求解p[i]=?说明p[i']早已经求解完毕)
2.2 以str'[i']为中心的最长回文子串扩展到了L的外部,即i'的回文在L~R外部【时间复杂度:O(1)】
结论:该种情况下,p[i] = R-i
2.3 以str'[i']为中心的最长回文子串恰好和L临界,即i'的回文左边界恰好压在L边界上面(从R的右边扩)
结论:该种情况下,可以确定p[i]至少是(R-i)或者p[i'](此时二者恰好相等),p[i]还能不能再变大取决于还能不能再扩,即下一步需要采取暴力扩进一步求解p[i]。
PS:i的回文半径即p[i]=?,主要关注在L~R的外部,以i为中心的回文串是否可以再扩大【此例中就可以再扩大,但是如果将靠近右边界R的字符‘K’改为s,那么就不能再扩大了】,无需关注L~R的内部。
根据该种可能性,得到以下程序代码
//当前索引的位置i在回文右边界R之内
//该种情况下,存在3种可能性
//2.1结论为:p[i] = p[i']
//2.2结论为:p[i] = R-i;
//2.3结论为:可以保证此时p[i]的最小值为(R-i)或p[i'],此时二者相等了。
//综上考虑,仅仅需要求解当前3中可能性中的最小值,然后进行暴力扩就OK了
if(i <= R)
{
//根据中点公式,逆推可得:i‘=(2*id-i)
p[i] = p[2*id-i]>(R-i)? (R-i):p[2*id-i];
//进行暴力扩
while((i+p[i]<strlen(man_arr)) && (i-p[i]>-1))
{
if(man_arr[i+p[i]] == man_arr[i-p[i]])
p[i]++;
else
break;
}
}
分析各种情况的时间复杂度
(1)很容易得出可能性2.1和2.2的时间复杂度均为O(1)
(2)关于可能性1和可能性2.3的时间复杂度:之前已经明确回文右边界R是只增不减的,可能性1采取的方法是直接暴力扩,即最大的可能性无非就是R从0递增为2N;可能性2.3采取的方法是在原来右边界的基础上,进行暴力扩,即最大的可能性无非也就是R从某一个数递增为2N。
总的来看,可能性1和可能性2.3的时间复杂度都是O(N)。
(3)进一步得出:Manacher算法的时间复杂度为O(N)
初步得到的Manacher算法代码
//初步得到的Manacher算法代码
#include<iostream>
using namespace std;
#include <string.h>
#include <climits>
//转化为Manacher算法的字符串
void convert_manacher(char arr[],char arr_man[], int n)
{
for(int i = 0; i < n; i++)
{
arr_man[2*i+1] = arr[i];
arr_man[2*i] = '#';
}
arr_man[2*n] = '#';//扣边界
}
//Manacher算法的实现
int Manacher(char arr[], char arr_man[], int p[], int n)
{
//初始化一些引入的基本量
int i = 0;//当前索引的位置
int id = -1;//当前的回文右边界的中心
int R = -1;//当前回文右边界
int Max = INT_MIN;//存储当前p[i]中最大的一个数
//转化为manacher算法的字符串
convert_manacher(arr, arr_man, n);
for(i = 0; i < strlen(arr_man); i++)
{
//当前索引的位置i在回文右边界R之外:暴力扩
if(i > R)
{
p[i] = 1;//这里需要默认的,固定为1
while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
{
if(arr_man[i+p[i]] == arr_man[i-p[i]])
p[i]++;
else
break;
}
}
else
{
//当前索引的位置i在回文右边界R之内
//该种情况下,存在3种可能性
//2.1结论为:p[i] = p[i']
//2.2结论为:p[i] = R-i;
//2.3结论为:可以保证此时p[i]的最小值为(R-i)或p[i'],此时二者相等了。
//综上考虑,仅仅需要求解当前3中可能性中的最小值,然后进行暴力扩就OK了
//根据中点公式,逆推可得:i‘=(2*id-i)
p[i] = p[2*id-i]>(R-i)? (R-i):p[2*id-i];
//进行暴力扩
while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
{
if(arr_man[i+p[i]] == arr_man[i-p[i]])
p[i]++;
else
break;
}
}
//更新回文右边界R和回文右边界的中心id
if(p[i]+i > R)
{
R = p[i]+i;
id = i;
}
Max = Max>p[i]? Max:p[i];
}
//打印回文半径数组p
for(int i = 0; i < 2*n+1; i++)
cout << p[i] << " ";
cout << endl;
return Max-1;
}
int main()
{
char arr[] = "Tabcbakkkabcbak";
int arr_len = strlen(arr);
int arr_man_len = 2*arr_len+1;
char* arr_man = new char[arr_man_len];//存储manacher算法的字符串
int p_len = arr_man_len;
int* p = new int[p_len];//存储回文半径
cout << Manacher(arr, arr_man, p, arr_len);
delete[] arr_man;
delete[] p;
return 0;
}
对初步得到的Manacher算法代码进行优化
//对初步得到的Manacher算法代码进行优化
#include<iostream>
using namespace std;
#include <string.h>
#include <climits>
//转化为Manacher算法的字符串
void convert_manacher(char arr[],char arr_man[], int n)
{
for(int i = 0; i < n; i++)
{
arr_man[2*i+1] = arr[i];
arr_man[2*i] = '#';
}
arr_man[2*n] = '#';//扣边界
}
//Manacher算法的实现
int Manacher(char arr[], char arr_man[], int p[], int n)
{
//初始化一些引入的基本量
int i = 0;//当前索引的位置
int id = -1;//当前的回文右边界的中心
int R = -1;//当前回文右边界
int Max = INT_MIN;//存储当前p[i]中最大的一个数
//转化为manacher算法的字符串
convert_manacher(arr, arr_man, n);
for(i = 0; i < strlen(arr_man); i++)
{
//这一行代码:直接将可能性分为2中情况
p[i] = R>i? ( p[2*id-i]>(R-i)? (R-i):p[2*id-i] ):1;
//进行暴力扩
while((i+p[i]<strlen(arr_man)) && (i-p[i]>-1))
{
if(arr_man[i+p[i]] == arr_man[i-p[i]])
p[i]++;
else
break;
}
//更新回文右边界R和回文右边界的中心id
if(p[i]+i > R)
{
R = p[i]+i;
id = i;
}
Max = Max>p[i]? Max:p[i];
}
//打印回文半径数组p
for(int i = 0; i < 2*n+1; i++)
cout << p[i] << " ";
cout << endl;
return Max-1;
}
int main()
{
char arr[] = "Tabcbakkkabcbak";
int arr_len = strlen(arr);
int arr_man_len = 2*arr_len+1;
char* arr_man = new char[arr_man_len];//存储manacher算法的字符串
int p_len = arr_man_len;
int* p = new int[p_len];//存储回文半径
cout << Manacher(arr, arr_man, p, arr_len);
delete[] arr_man;
delete[] p;
return 0;
}