算法背景
1975年,Manacher发明了Manacher算法(中文名:马拉车算法),是一个可以在O(n)的复杂度中返回字符串s中最长回文子串长度的算法,十分巧妙。
算法规律
Manacher发现将字符串每隔一个间隔放置'#',可以简化回文字符串的求解,如:
a b b a 字符串
添加 '#'
# a # b # b # a #
下标:
0 1 2 3 4 5 6 7 8
以i下标为中心的最长回文子串的半径长度: - p[i]
1 2 1 2 5 2 1 2 1
规律发现:
(i-p[i])/2 就是子回文串在原字符串的起始位置,但是这里有一个问题就是存在-1
p[i] - 1 就是子回文串在原回文串中的长度
接上述问题解决:
在扩展串中再添加'$'、'@'
如下:
$ # a # b # b # a # @
i:
0 1 2 3 4 5 6 7 8 9 10
p[i]:
1 2 1 2 5 2 1 2 1
再看上述规律是不是符合了呢!
算法高效原因?
不断利用之前的一个回文子串信息,包括了到达的最远边界mx,以及最远边界回文子串的中心坐标id,如果可用,使用,不可以使用就从1开始中心扩展。
将算法从中间拆解如下,相信理解起来会很容易:
代码实现
class Solution {
public int countSubstrings(String s) {
// 马拉车算法
StringBuilder sb = new StringBuilder("$");
for(int i=0;i<s.length();++i){
sb.append("#").append(s.charAt(i));
}
sb.append("#@");
int mx = 0,id=0;
int res = 0;
// 定义数组
int[] p = new int[sb.length()];
for(int i=1;i<sb.length()-1;++i){
p[i] = mx>i?Math.min(p[2*id-i],mx-i):1;
while(sb.charAt(i+p[i])==sb.charAt(i-p[i])){
p[i]++;
}
if(i+p[i]>mx){
id = i;
mx = id+p[i];
}
res += p[i]/2;
}
return res;
}
}
额外说一下这里的res += p[i]/2,可以将回文串想象成一颗洋葱,再看一下我写的过程图:
有序列表序号对应上图的下标i,第i个回文子串
- '#' 半径1,包含原串长度0,贡献度0
- 回文串 "#A#"半径2,包含的原串长度1,贡献度1
- '#' 半径1,包含原串长度0,贡献度0
- 回文串 "#B#"半径2,包含的原串长度1,贡献度1
- "#A#B#B#A#"半径5,包含的原串长度4,贡献度2
- "#B#"半径2,包含的原串长度1,贡献度1
- '#' 半径1,包含原串长度0,贡献度0
- "#A#"半径2,包含的原串长度1,贡献度1
- '#' 半径1,包含原串长度0,贡献度0
找一下规律即可得出上述结论公式。
class Solution {
public String longestPalindrome(String s) {
// 马拉车算法
StringBuilder sb = new StringBuilder("$");
for(int i=0;i<s.length();++i){
sb.append("#").append(s.charAt(i));
}
sb.append("#@");
// 定义数组
int[] p = new int[sb.length()];
// mx , id
int mx=0,id=0,maxLen=0,maxStart=0;
for(int i=1;i<sb.length()-1;++i){
p[i] = mx>i?Math.min(p[2*id-i],mx-i):1;
while(sb.charAt(i+p[i])==sb.charAt(i-p[i])){
p[i]++;
}
if(i+p[i]>mx){
mx = i+p[i];
id = i;
}
if(p[i]-1>maxLen){
maxStart = (i-p[i])/2;
maxLen = p[i]-1;
}
}
return s.substring(maxStart,maxStart+maxLen);
}
}