这个题目主要是运用manacher算法,又称为马拉车算法,下面对算法进行介绍。
manacher算法
首先,定义一些变量,假设当前访问的是第
i
个位置:
然后假设现在要求以第
1.j>=maxRight:
由于
maxRight=i+p[i]
,所以
j
所处的位置不在第
2.j<maxRight:
由于
maxRight=i+p[i]
,所以
j
所处的位置在第
2.1:(i−k−p[i−k])>(i−p[i]): 如图所示
此时 p[i−k] 对应的最长回文串被 p[i] 对应的最长回文串的左半部分完全包围,根据回文的对称性, p[i+k] 也应该被右半部分完全包围,并且 p[i−k]=p[i+k]=p[j] ,很好理解,不需要证明。
2.2:(i−k−p[i−k])=(i−p[i]): 如图所示
此时 p[i−k] 对应的最长回文串还是被 p[i] 对应的最长回文串所包围,所以对应的 p[i+k] 的最长回文串至少也会到达 i+p[i] 的位置,即 maxRight 处,但是因为 maxRight 位置之后的字符情况未知,可能还会有与以 i+k 为中心的左边位置的字符相等,所以 p[i+k] 的值可能还会更长,此时需要再次比较 ch[left]==ch[right] 的情况来判断是否会更长,此时 p[i+k]>=p[i−k] ,应该从 (i+k−p[i−k])和(i+k+p[i−k]) 两个位置进行左右比较。
2.3:(i−k−p[i−k])=(i−p[i]): 如图所示
此时,
p[i]
对应的最长回文子串的左半部分无法完整的包含
p[i−k]
对应的最长回文子串,此时
p[i+k]=p[i]−k
,即最长回文子串半径仅仅包含
i−p[i]
到
i−k
之间的那一小段。能不能更长呢,答案是否定的,下面给予证明。
假设
p[i+k]
的值能够更大,即
p[i+k]>p[i]−k
,如图所示,这相当于在
(i+p[i])
的位置后面延伸了一小段(图中最右边的那段小粗横线),根据根据对称性,图中另外几段小粗黑线必须也有的。假设从左到右依次标注那几段小黑线为
L1,L2,L3,L4
,现在假设
L4
是存在的,假设其值为
ABC
,则根据回文的对称性,
L3
的值为
CBA
,
L2
的值为
ABC
,
L1
的值为
CBA
,可以发现
L1
和
L4
也是回文对称的,所以以位置
i
为中心的最长回文串应该更长,不应该是
然后对上述三种情况进行汇总:
观察总结之后发现, p[i+k]=min{p[i−k],p[i]−k} ,当然,对于第二种情况,即可能更长的情况,其实可能会更大,我们将上面三种也统一,在赋值完之后,在进行左右比较,因为对于第一种和第三种情况,进行左右比较时肯定不相等,不影响值的变化,对于第二种情况进行左右比较就可以得到真实最长半径。
2.字符串预处理
在求最长回文子串的时候,我们还得对区分以单字符为中心的最长回文子串和以双字符为中心的最长回文子串进行判断,为了便于处理,我们可以通过在字符串中加入统一的字符,将奇偶情况统一为以单字符为中心的最长子串。例如:对于字符串“abbc”,我们可以通过加入’#’字符,将其变为”#a#b#b#c#”,这样就变为以单字符为中心;对于字符”aba”,也通过加入’#’,将其变为”#a#b#a#”,仍然是以单字符为中心。就完成了统一,也便于使用manacher算法,因为该算法就是以单字符为中心。
但是由于最左边和最右边都是’#’,这样在进行左右判断的时候,就会
left−−和right++
成立,会出现越界的情况。为了避免这种越界的发生,我们可以在两端再次添加两个不同的字符,例如最左边添加’@’,最右边添加’=’,此时”aba”处理之后的结果为”@#a#b#a#=”。
3.代码实现
public static String solve(String ss) {
/*
* 加#号,使得两种情况统一处理
* 如"aa"变为"#a#a#"
* 再如"aba"变为"#a#b#a#"
* 防止越界,在首部再加上@,尾部加上$,只要不同即可,防止越界
*/
char[] cs = ss.toCharArray();
int len = cs.length * 2 + 3; //首部,尾部加一个不同的字符,防止越界
char[] res = new char[len];
res[0] = '@';
res[len - 1] = '$';
for(int i = 1; i < len - 1; i++) {
if((i&1) != 0) { //如果是奇数
res[i] = '#';
}
else { //偶数
res[i] = cs[i / 2 - 1];
}
}
int maxRight = 0; //能到的最右边
int longestCenter = 0; //最长的中心
int longest = 0; //最长
int nowCenter = 0; //到达最右边对应的中心
int[] dp = new int[len]; //以i位置就中心的最长回文串
for(int i = 1; i < len - 1; i++) {
if(maxRight > i) {
dp[i] = Math.min(dp[2*nowCenter - i], maxRight - i);
}
else {
dp[i] = 1;
}
//考虑以此为中心,继续递增,因为恰好在边界上需要考虑
while(res[i + dp[i]] == res[i - dp[i]]) {
dp[i]++;
}
if(dp[i] + i > maxRight) {
maxRight = dp[i] + i;
nowCenter = i;
}
if(longest < dp[i]) {
longest = dp[i];
longestCenter = i;
}
}
StringBuffer sb = new StringBuffer();
for(int i = longestCenter - longest + 1; i < longestCenter + longest - 1; i++) {
if(res[i] != '#')
sb.append(res[i]);
}
return sb.toString();
}