参考:https://blog.csdn.net/weixin_42373330/article/details/82118694
水平有限,只是记录下自己的学习过程
马拉车算法用于求最长回文子串,时间复杂度O(n)。
先了解一下p[i],id,mx的含义。
p[i]表示以Str[i]为中心的最长回文子串的边界到Str[i]的距离(包括Str[i])
算法用p[i]表示第i个字符到mx的距离(包括i);
id记录出现的最长回文子串的中心位置;
mx记录出现的最长回文子串的最右边界的右边一个位置,mx=id+p[id]。
以ababc为例,要先对字符串进行预处理(为了方便处理):用"#“插进字符串中,包括头和尾,再在头部插入“$”,尾部插入”&",最后变成
**$#a#b#a#b#c#&**
假如i=4,即第一个b,那么最长回文子串是#a#b#a#,p[i]=4.本次操作结束后的id=4,mx=8.
P数组的计算
算法的精髓就在于P数组,那么如何计算呢?
首先从左到右依次计算,但当计算P[i]时,P[j](j<i)已经计算完毕,所以要充分利用这个已知条件来简化计算。
根据i和mx的位置关系,可以分成两种情况
- i>=mx:这种情况没有已知条件能够利用,所以令p[i]=1
- i<mx:
令j=2*id-i,即i和j关于id对称。
由于是从左往右计算p[i],故此时p[ j ]已经计算好了,现在要做的事情是如何利用已经算好的p[ j ] 来更新p[ i ],从而提高效率。
图中,假设i-6~i是以i-3为中心的最长回文子串p[i-3]=4,此时id=i-3,mx=id+p[id]=i+1,
现在计算p[i]时,对i和mx的比较需要分两种情况:
一、i>=mx
i在mx的右边,例如遍历到 i+2,这个时候没有太多的已知信息能够利用,无法做出假设,只能先令p[i+2]=1,然后再做后续匹配
二、i<mx
令j=2*id-mx,str[j]是str[i]关于id的对称点
由于p[j]已知,现在就需要考虑如何利用p[j]简化计算从而提高效率。
p[j]的左边界存在两种情况:在mx的对称点的左边或右边。
如果在mx的对称点的左边,那么超出部分关于id不对称,也就是下图的1和4不相等
如果在mx的对称点的右边,也就是被mx完全包围,那么这是p[i]=p[j]
因此,将 mx-i 和 p[j] 进行比较分成两种情况:
1、p[j]>=mx-i
说明以 j 为中心的回文字串有部分超出了以id为中心的回文子串,而超出的部分(下图中的1部分)关于id的对称部分(下图中4部分)必定>=mx,,可知1,2,3部分都对应相等,而之前讲过mx与mx的对称点指向的字符是不相等的,说明1与4部分不对应相等,所以我们所能确保的是 p[i] 至少为mx-i
2、p[j]<mx-i
以第j个字符为中心的回文子串全包含在以第id个字符为中心的回文子串中,基于对称性可知,即下图中颜色相同的对称相等,所以p[i]=p[j]=p[2*id - i],也不需要进行延伸判断。
代码过程:
- 初始化:mx=0,i=1(“$”不考虑)
- 计算p数组
if (mx <= i) {
p[i] = 1;
} else if (mx - i > p[2 * mid - i]) {
// 这种情况不需要继续左右延伸
p[i] = p[2 * mid - i];
needExtend = false;
} else
// 推算出来已知的回文子串的长度
p[i] = mx - i;
- 进入while循环,从当前已知回文子串范围开始,即p[i]范围外,判断左右是否相等,如果相等p[i]++。
if (needExtend)
while (str.charAt(i - p[i]) == str.charAt(i + p[i]))
p[i]++;
- 更新变量
完整代码
//预处理
private String preHandleString(String s) {
StringBuilder sb = new StringBuilder();
//在首位各加一个不同的字符 防止遍历出界
sb.append("%#");
for (char c : s.toCharArray()) {
sb.append(c).append("#");
}
sb.append("$");
return sb.toString();
}
private String manacher(String s) {
String str = preHandleString(s);
int longest = 0, center = 0;
int mx = 0, mid = 0;
int len = str.length();
int[] p = new int[len];
for (int i = 1; i < len - 1; i++) {
boolean needExtend = true;
if (mx <= i) {
p[i] = 1;
} else if (mx - i > p[2 * mid - i]) {
// 这种情况不需要继续左右延伸
p[i] = p[2 * mid - i];
needExtend = false;
} else
// 推算出来已知的回文子串的长度
p[i] = mx - i;
if (needExtend)
while (str.charAt(i - p[i]) == str.charAt(i + p[i]))
p[i]++;
mid = i;
mx = i + p[i];
if (p[i] > longest) {
longest = p[i];
center = i;
}
}
StringBuilder out = new StringBuilder();
for (int i = center - longest + 2; i < center + longest - 1; i += 2) {
out.append(str.charAt(i));
}
return out.toString();
}