Manacher(马拉车算法)

## 算法概述 - 用于对字符串中回文串相关的操作 - 如寻找最长回文串 - 时间复杂度 O(n)

算法原理



example: str = "a film called tenet"



寻找最长回文串的一般解法(暴力)
对于字符串中的每一个字符 进行中心拓展
伪代码 时间复杂度O(n^2)

for(int i=0;i<len;++i){
int l=r=i,tmp=-1;
while(l>=0&&r<len&&str[l--]==str[r++]){
tmp+=2;}//字符串长度为奇数
ans=tmp>ans?tmp:ans;
l=i,r=i+1,tmp=0;
while(l>=0&&r<len&&str[l--]==str[r++]){
tmp+=2;}//字符串长度为偶数
ans=tmp>ans?tmp:ans;
}


马拉车算法流程如下



预处理
先在字符串开头与结尾插入两个不同的特殊字符
再在字符之间插入特殊符号
如:" ^#a# #f#i#l#m# #c#a#l#l#e#d# #t#e#n#e#t#$ "
将字符串转换为奇数长度进行统一处理,首尾不同的符号帮助边界判断



算法核心
在暴力的同时,最大化利用已有信息,对后续操作提供加速
流程如下 :



以每个字符为中心进行回文半径计算

raduis

1

2

1

2

1

6

1

2

1

2

1

location

0

1

2

3

4

5

6

7

8

9

10

11

12



这里raduis用于记录对应位置的回文半径


把目光放在str[6] 这里计算出半径为6 (记center=6)
可以知道指针在运算时实际已经处理到了0和10的位置 (记最前位置R=12)
一般暴力操作是以下个字符str[7]为中心继续向两端进行匹配拓展
而马拉车进行的优化就是在这里
对于str[7] 7<12 断言raduis[7]=raduis[5]=1
对于str[8] 7<12 断言raduis[8]=raduis[4]=2




分析

#

e

#

t

#

^

n

#

e

#

t

#

$

7

8

9

10

11

12


a.由对称性str[5]str[1]和str[7]str[11]是相同的两段
当计算raduis[7]和radius[8]时我们会注意它们和
对称的raduis[5]和radius[4]“大致”相同
仔细发现str[4]的回文串部分str[5]~str[3]
是包含在str[6]的回文串部分str[1]~str[11]的
而又由str[6]位置的回文性质(a.) 可以知道str[4]的回文串部分会对应在str[8]左右
即str[5]str[3]对应到str[7]str[9]
从而可以得到raduis[8]=raduis[4]

这就是马拉车算法用到的思想




其他情况的考虑


刚才是对应位置的回文串部分被包含在一个大的回文串里
即str[i]的 对应位置回文半径长 raduis[2C-i] + i < R (R是已知回文串最右位置)
对于raduis[2C-i] + i >= R 我们不知道溢出的部分 是否能匹配上
但可以知道至少有str[i]~ str[R]这一部分是对称的
也就是说raduis[i]数值上至少等于R - i
于是我们可以直接从溢出部分开始进行字符串比较




综合考虑


当正在计算的位置 i < R 的时候可以优化
raduis[2C-i] < R - i 的时候 raduis[i] = raduis[2C-i]
raduis[2*C-i] >= R - i 则raduis[i] = R - i 再从最远端位置R继续匹配




关于回文串返回

raduis

1

2

1

2

1

6

1

2

1

2

1

location

0

1

2

3

4

5

6

7

8

9

10

11

12



原回文串的长度 len = raduis[i] - 1

这里不是巧合
raduis[i]*2-1得到预处理后的回文串长度 总是奇数
对应的回文串只有两种形式


  • #x#x# 原回文串长度为偶数
  • #x#x#x# 原回文串长度为奇数

但不管如何'#'的个数总是比处理字符多1
(raduis[i] * 2 - 1 - 1) / 2得到正确长度



原回文串开始位置(包含) start = (i - raduis[i]) / 2
index/2得到原字符串位置+1的地方
i-radius[i]得到预处理串开始位置(不包含)
(i-radius[i])/2 得到原字符串开始位置(包含)







代码实现

class solution {
String manacher(String s) {
if (s.isEmpty()) {
return "";
}
StringBuilder builder = new StringBuilder("^#");
for (char c : s) {
builder.append('c');
builder.append('#');
}
builder.append('$');
char[] str = builder.toString().toCharArray();
int len = str.length;
int[] raduis = new int[len];
int C = -1, R = -1;
int ans = 0;
for (int i = 1; i < len - 1; ++i) {
raduis[i] = i > R ? 1 : Math.max(R - i, raduis[2 * C - i]);
while (str[i - raduis[i]] == str[i + raduis[i]]) {
++raduis[i];
}
if (raduis[i] > raduis[ans]) {
ans = i;
}
if (i + raduis[i] > R) {
R = i + raduis[i];
C = i;
}
}
int len = raduis[ans] - 1;
int start = (ans - raduis[ans]) / 2;
return s.subString(start, start + len);
}
}