对于一个字符串,请设计一个高效算法,计算其中最长回文子串的长度。
给定字符串A以及它的长度n,请返回最长回文子串的长度。
"abc1234321ab",12
返回:7
1. Manacher算法解决什么问题问题:求一个字符串的最长回文子串
当然暴力的解法就是:遍历每个字符,以其为中心往外扩,复杂度N^2
2. 步骤
(1)预处理,为了把像1221这样的情况也放到一起处理,先构造一个新的字符串#1#2#2#1#,这样就不会出现回文中心的index为2.5这样的情况了
(2)定义3个主要的变量,并初始化
int[] radius:radius[i]表示i位置对于的回文半径
int pr: 遍历i的过程中可以匹配到的最右边的位置
int index:与pr对于的中心索引
(3)遍历i,在遍历的同时更新3个主要的变量,遍历的时候有4种情况需要讨论,其中2种情况不需要扩,一种情况需要扩但是已经有一定的基础,最后一种情况从0开始往外扩
<1> i对称过去的索引被index的回文范围完全包住,比如:5#1#2#1#3#1#2#1#5,这种情况是不需要要扩的,radius[i]就是radius[i_mirror]
<2> 对称过去的左边落在index的回文范围外面,比如:#a#b#a#c#a#b#d#,这种情况也是不需要要扩的,radius[i]就是index对应的右大到i的距离(可证明)
<3>对称过去的左边正好落在index的最左边,比如:#d#a#b#a#c#a#b#a#c#,这种情况需要扩(因为可能还可以扩得更宽),但是已经有了一定的基础
<4>i落在pr的右边,肯定要从0开始开始扩
3. 可以看到算法的加速过程在于:利用了之前匹配过的信息!就像KMP利用已经结算过的next数组一样!
通过对pr变量的分析,可以知道每次扩都会引起pr的增大,pr和扩是同步的,扩大了,下次不需要扩的可能性就更大了,而pr只是从0遍历一遍到了2*n,所以复杂度O(N)
实际代码:
public class Palindrome {
public int getLongestPalindrome(String A, int n) {
// 为了把1221这样的情况也放到一起处理,先构造一个新的字符串
int[] cs = new int[A.length()*2+1];
cs[0] = '#';
for(int i=1; i<cs.length; i+=2) {
cs[i] = A.charAt(i/2);
cs[i+1] = '#';
}
int[] radius = new int[cs.length]; // i位置对于的半径
int pr = 0; // 当前匹配到的最右边的位置
int index = 0; // 与pr对于的中心索引
// 虽然for里面有while,但是for中的i是遍历一遍,while里面的pr也是遍历一遍,所以复杂度O(n)
for(int i=0; i<cs.length; i++) {
if(i < pr) {
int mir = 2*index-i;
int mir_left = mir-radius[mir];
int idx_left = index-radius[index];
// 1. 对称过去的被index的范围完全包住
if(mir_left > idx_left)
radius[i] = radius[mir];
// 2. 对称过去的左边落在index的回文范围外面
if(mir_left < idx_left)
radius[i] = mir - idx_left;
// 3. 对称过去的左边正好落在index的最左边
if(mir_left == idx_left) {
while(pr<cs.length && 2*i-pr>=0 && cs[pr]==cs[2*i-pr]) pr++;
radius[i] = pr-i;
index = i;
}
} else {
// 4. i落在pr的右边,肯定要开始扩
while(pr<cs.length && 2*i-pr>=0 && cs[pr]==cs[2*i-pr]) pr++;
radius[i] = pr-i;
index = i;
}
}
int max = 1;
for(int i : radius) max = Math.max(max, i);
return max-1;
}
}