零、预备知识
Manacher用于在一个字符串中找到最长的回文子串。
回文串:正着念和反着念一样,例如aabbaa,anna等。
注意子串与子序列的区别:
子串必须是在原字符中可以找到的。比如 " I am a student"。am是子串(当然也是子序列),但是aa就不是子串了(是子序列)。
一、算法原理
Manacher算法通常人称马拉车算法,用于在一个字符串中找到最长的回文子串。
1.首先因为奇偶个数的不同,所以判断回文的方式不一样,因此manacher算法通过在字符串两边和每个字符之间插入一个相同的字符来字符串转换为奇数个的字符串。插入的字符可以是任意字符,但是必须保证插入的字符都相同,一般常用‘#’作为插入字符。例如,原始字符串aaabbbaaa,插入后的字符串则为#a#a#a#b#b#b#a#a#a#。
2.manacher算法使用了3个辅助变量:
1) int pArr[ ] 存储以每个位置i为中心的最长回文串的最右边界right到位置i 一共有多少个字符,即 【i,right】,即 回文半径(单个字符的回文半径为1),pArr[i]=right-i+1;
2)int R表示回文右半径+1,即所有回文半径中能到达的最右位置+1,也就是上面那个公式里面最大的right+1
3)int C 表示最近一次更新R时,那个回文中心的位置。
3.接下来就是计算字符串的回文数组pArr[],计算时分两大情况:
3.1当前位置i不在回文右边界中,则向右边暴力扩张。
3.2当前位置i在回文右边界内(又分为三种情况):
3.2.1:i' (i关于c的对称点,即 i‘= 2c-i)的回文半径彻底在以C(回文中心)为中心所能到达的最右边界内,则R不用扩,i的回文半径为 i'的回文半径
例如:
3.2.2:i ’ 的回文半径在L外面,没包住,此时i的回文半径为i到R。
例如:
3.2.3:i'的回文半径压线,i与R之间的半径不用计算,但是不确定是否再向右扩
其实是有点模糊的,因为这个R其实是最右边界+1,所以要扣细节很多,不过背代码就完事了!!!!
因为回文半径这个屌玩意也是 right-i+1,所以这里R=right+1 刚好!!!
时间复杂度 O( n )
说下关键的记忆点:
1. 先变换字符串 插入#
2. 3个辅助变量
int index=-1;
int R= -1; // R:回文右半径+1, index 关于R的回文中心
vector<int> vec(new_str.size(),0); // vec: 存储回文半径3. 然后遍历新字符串, 关键代码:
vec[i]=R>i?min(vec[2*index-i],R-i):1; // 前面两个是在回文右半径里面的瓶颈 1是暴力扩 (包住了是瓶颈,没包住了是暴力扩)
4. 之后每人再给一次机会
5. 起点和长度怎么算:
return s.substr((res_dx-res_len+1)/2,res_len-1); // 注意起点是中心-半径+1 ,再÷2, 而长度是半径-1
二、算法实现
class Solution {
public:
string longestPalindrome(string s) {
if(s.size()<2)
return s;
int len=s.size();
string new_str="#";
for(int i=0;i<len;++i){
new_str =new_str+s[i]+"#";
} // 插入字符
int index=-1;
int R=-1; // R:回文右半径+1, index 关于R的回文中心
int res_dx=0, res_len=0; // res_dx: 最长的中心 res_len: 最长的半径
vector<int> vec(new_str.size(),0); // vec: 存储回文半径
for(int i=0;i<new_str.size();++i){
vec[i]=R>i?min(vec[2*index-i],R-i):1; // 前面两个是在回文右半径里面的瓶颈 1是暴力扩
while(i+vec[i]< new_str.size() && i-vec[i]>=0){
if(new_str[i+vec[i]]==new_str[i-vec[i]])
++vec[i];
else
break;
} //人人平等都再给你一次机会
if(i+vec[i]>R){
R=i+vec[i];
index=i;
} // 更新R index
if(vec[i]>res_len){
res_len=vec[i];
res_dx=i;
} // 更新 res
}
return s.substr((res_dx-res_len+1)/2,res_len-1); // 注意起点是中心-半径+1 ,再÷2, 而长度是半径-1
}
};
java:
public class Manacher {
public static char[] manacherString(String str){
char[] charArr = str.toCharArray();
char[] res = new char[str.length()*2+1];
int index = 0;
for(int i=0;i<res.length;i++){//01 10&01 100&001
res[i] = (i&1)==0?'#':charArr[index++];
}
return res;
}
public static int maxLcpsLength(String str){
if(str==null||str.length()==0){
return 0;
}
char[] charArr = manacherString(str);//扩充后的字符串
int[] pArr = new int[charArr.length];//存储每个位置的最大回文长度
int index = -1;//回文右边界中心 2*index-i是i位置关于index的对称点
int pR = -1;//目前所能到达的最右边界
int max = Integer.MIN_VALUE;
for(int i=0;i!=charArr.length;i++){//对每个位置
pArr[i] = pR>i?Math.min(pArr[2*index-i], pR-i):1;//起码回文的距离,看不用扩的区域是多少,是与之对称的点的回文半径限制了它还是它距离最右边界的距离限制了它
while(i+pArr[i]<charArr.length&&i-pArr[i]>-1){//如果在范围内则往外扩
if(charArr[i+pArr[i]]==charArr[i-pArr[i]]){
pArr[i]++;
}else{
break;
}
}
if(i+pArr[i]>pR){
pR = i+pArr[i];
index = i;
}
max = Math.max(max, pArr[i]);
}
return max-1;
}
public static void main(String[] args) {
//String str = "abc1234321ab";
String str = "1213121";
System.out.println(maxLcpsLength(str));
}
}
c++: