5. 最长回文子串
1. 题目描述
来源: https://leetcode-cn.com/problems/longest-palindromic-substring/
2. 思路
2.1 中心拓展法
和“647. 回文子串”中的中心拓展完全类似,详细请访问https://www.cnblogs.com/taote/p/15448968.html中2.1部分。
2.2 Manacher算法
要学会Manacher算法,得先熟悉中心扩展法的过程。
在中心拓展法的过程中,我们将奇数长度的子串和偶数长度的子串在拓展的起始位置上做了不同的调整,相当于对其分开处理,事实上,对原字符串稍做处理,即可将奇偶两种情况进行统一,方法就是在相邻字符之间插入“#”(其实插入任何字符均可以,包括出现在原字符串中的字符),例如:
添加”#“后:
这样处理的结果是字符串中回文子串的长度总为奇数,原因很简单:将原来长度为k的回文子串变成了长度为k+(k+1)的回文子串(例如,上图中原字符串中的aba变为了#a#b#a#,所插入的#的数量总是比原回文串长度大1)。所以,只考虑处理奇数长度的回文子串即可。
在大致过程上,Manacher算法和中心拓展法相同,均是将某一个字符看作潜在的最长回文子串的中心位置然后向两边拓展,但是Manacher在向两边拓展的过程中,使用了一些技巧进行优化,使得算法的时间复杂度从O(n2)降低为O(n)。
- 首先介绍一下Manacher所用到的数据结构:
- 数组
p[n]
,p[i]保存i为中心的最长回文子串的长度。 - 整数R:最右回文子串右边界。顾名思义,就是计算以s[i]和s[i]之前字符为中心的最长回文子串时,回文子串能到达的最右边的位置。每以一个字符为中心向两边扩展时,都要看向右最大能扩展到哪个位置,如果这个位置超过了R,就要对R进行更新。
- 整数C:最后一次更新R对应的那个回文子串的中心位置。C的值和R的值一一对应。
- 具体过程
计算以s[i]为中心的最长回文子串的过程中,分两种情况:①如果s[i]在R的右侧(R<i),无法优化,拓展过程和中心拓展法完全相同。根据拓展结果更新R和C②如果s[i]在R的左侧(i<=R),那么说明以C为对称点,能找到s[i]的对称字符s[j](根据上面对R和C的描述,我们知道以C为中心,R-C为半径的子串为回文子串)。我们可以根据已经计算好的p[j]来判断以s[j]为中心的最长回文子串,是否完全被包含在C为中心,R-C为半径的回文子串内(不能到边界上),如果在,根据对称性,即可得到p[i],如果不在,可将被包含在内的部分对称到s[i]一侧,然后以s[i]为中心继续向两边扩展,如果向右边扩展的位置超过了R,则更新R,并用i更新C。
- 我们用上图中的字符串为例,来描述Manacher算法的详细过程
起始时,我们规定R=-1,C=-1。
拓展中心i | R | C | 是否满足优化条件 | 得到的最长回文子串 | p[i] | 是否更新R和C | R和C构成的回文子串范围 |
---|---|---|---|---|---|---|---|
0 | -1 | -1 | R<i,不满足,老老实实拓展。 | # | 1 | 更新,R=0,C=0 | [0,0] |
1 | 0 | 0 | R<i,不满足,老老实实拓展。 | #a# | 3 | 更新,R=2,C=1 | [0,2] |
2 | 2 | 1 | i<=R,满足。s[2]以C=1为中心的对称位置为s[0],s[0]为中心的最长回文子串虽然包含在[0,2]内,但是落在了边界上,所以将s[0]为中心的回文子串对应到s[2]的位置之后,还得以s[2]为中心继续拓展。 | # | 1 | 否 | [0,2] |
3 | 2 | 1 | R<i,不满足,老老实实拓展。 | #a#b#a# | 7 | 更新,R=6,C=3 | [0,6] |
4 | 6 | 3 | i<R,满足。s[4]以C=3为中心的对称位置为s[2],s[2]为中心的最长回文子串完全包含在[0,6]内,根据对称关系,s[4]为中心的最长回文子串为s[2]为中心最长回文子串的逆序,因此可以直接得到。 | # | 1 | 否 | [0,6] |
... | ... | ... | ... | ... | ... | ... | ... |
后面的过程和前面类似,不再赘述。
- 时间复杂度
Manacher之所以是O(n)的时间复杂度,是因为R起到了关键作用,每当以某一字符为中心进行拓展时,R左侧的回文子串时可以通过对称关系直接得到的,避免了一些重复计算。
- 代码(Java)
public class SolutionTest {
public static void main(String[] args){
Solution slt = new Solution();
System.out.println(slt.longestPalindrome("aacabkacaa"));
}
}
class Solution {
public String longestPalindrome(String s) {
String sbak = s;
int lP = 0;
int rP = 0;
int l = s.length();
StringBuffer spie = new StringBuffer("#");
for(int i = 0; i < l; i++){
spie.append(s.charAt(i));
spie.append("#");
}
s = spie.toString();
int n = s.length();
int[] p = new int[n];
int R = -1;
int C = -1;
for(int i = 0; i < n; i++){
//老老实实拓展
if(R < i){
int ToLeft = i;
int ToRight = i;
int count = -1;
while (ToLeft>=0 && ToRight<n && s.charAt(ToLeft)==s.charAt(ToRight)){
count+=2;
ToLeft--;
ToRight++;
}
R = ToRight-1;
C = i;
p[i] = count;
}
else{
int j = 2*C-i; // j是i关于C的对称点
int jL = j - p[j]/2;
int jR = j + p[j]/2;
int iR = 2*C - jL;
int iL = 2*C - jR;
if(iR < R){
p[i] = p[j];
}
else{
int ToLeft = 2*i - R - 1;
int ToRight = R+1;
int count = (R-i)*2+1;
while (ToLeft>=0 && ToRight<n && s.charAt(ToLeft)==s.charAt(ToRight)){
count+=2;
ToLeft--;
ToRight++;
}
C = i;
R = ToRight-1;
p[i] = count;
}
}
}
int max = -1;
int note = 0;
for(int i = 0; i < n; i++){
if(p[i] > max){
max = p[i];
note = i;
}
}
lP = note-max/2;
rP = note+max/2;
StringBuffer ret = new StringBuffer();
for(int i = lP; i <= rP; i++){
if(s.charAt(i) != '#')
ret.append(s.charAt(i));
}
return ret.toString();
}
}