Manacher
问题引入
- 求一个长度为 n n n的字符串中回文子串的个数?
- (或:求一个长度为 n n n的字符串中最长的回文子串的长度?)
- 回文串:一个正读和反读相同的字符串,称之为回文串。
用已有的知识
- 算法一:枚举开头,枚举结尾,得到一个串,再扫一遍判断是否为回文串,若是,答案加 1 1 1,时间复杂度 O ( n 3 ) O(n^3) O(n3)。
- 算法二:枚举开头,枚举结尾,得到一个串,用字符串哈希判断其是否为回文串,若是,答案加 1 1 1,时间复杂度 O ( n 2 ) O(n^2) O(n2)。
- 算法三(对下文有用):枚举回文串的对称中心,分长度为奇或偶两种情况,分别向两端延伸,若延伸到的最大长度为 s s s(以奇数的情况为例,最大扩展到 s t i − s + 1 st_{i-s+1} sti−s+1和 s t i + s − 1 st_{i+s-1} sti+s−1),那么答案加上 s s s,时间复杂度 O ( n 2 ) O(n^2) O(n2)。
- 枚举回文串的对称中心,分长度为奇或偶两种情况,分别二分能延伸的长度,用字符串哈希判断,若延伸到的最大长度为 s s s(以奇数的情况为例,最大扩展到 s t i − s + 1 . . s t i + s − 1 st_{i-s+1}..st_{i+s-1} sti−s+1..sti+s−1),那么答案加上 s s s,时间复杂度 O ( n log 2 n ) O(nlog_2 n) O(nlog2n)。
提出疑问
- 有没有更快的方法,比如 O ( n ) O(n) O(n)?
- 有没有更简洁的方法,不用哈希?
- 答案是:有!
算法介绍
- 算法名称:Manacher’s Algorithm
- 主要思路是用已经计算出的回文子串,利用它回文的特性,对即将计算的回文子串提供一定的帮助。
- 需要记录 l , r l,r l,r表示已经找出的回文子串中,右端点最靠右的回文子串所在区间,
- 初始值分别为 0 , − 1 0,-1 0,−1。
- 以下以长度为奇数的情况为例,介绍算法操作流程。
- 从 1 − n 1-n 1−n依次枚举字符串的对称中心,对于每个 i i i,考虑向两边扩展,扩展的最长长度为 d i d_i di(也就是最大扩展到 s t i − d i + 1 . . s t i + d i − 1 st_{i-d_i+1}..st_{i+d_i-1} sti−di+1..sti+di−1)。
- 定义朴素算法为上文“用已有的知识”中的算法三,
- 那么我们考虑优化这个朴素算法,算法的核心是利用已有的 d d d来加速计算当前的 d i d_i di。
- 分两种情况:
- 一、 i > r i>r i>r(若长度为偶数则 i ≥ r i≥r i≥r)
- 直接调用朴素算法计算 d i d_i di。
- 二、 i ≤ r i≤r i≤r(若长度为偶数则 i < r i<r i<r)
- 即说明
i
i
i在回文子串
s
t
l
.
.
s
t
r
st_l..st_r
stl..str内,那么找到
i
i
i在这个回文子串中对称的点
j
j
j,
j = l + r − i j=l+r-i j=l+r−i - 因为对称,所以巧妙地利用这个性质, d i = d j d_i=d_j di=dj,
- 思考:这样是正确的吗??
- 其实不然——
- 因为以为中心
j
j
j的回文子串,左边界可能超出
l
l
l,那么对称过去到了
i
i
i就不在大回文子串
s
t
l
.
.
s
t
r
st_l..st_r
stl..str内了,不能保证也回文,所以
d i = m i n ( d j , r − i + 1 ) d_i=min(d_j,r-i+1) di=min(dj,r−i+1) - 同理,对 i i i而言,以它为中心的实际最大回文子串可能右边界超出 r r r,那么 d j d_j dj是计算不到的,所以,在上式的基础上,再次调用朴素算法。
- 至此,这个问题完美解决。
时间复杂度分析
- 算法是两重循环,看似复杂度极限会是 O ( n 2 ) O(n^2) O(n2)???
- 其实并不如此,实际上,它的复杂度十分优秀。
- 观察调用朴素算法的条件,发现每次朴素算法的调用,只会判断还没有被回文子串覆盖过的字符,也就是说,每个字符只会被朴素算法进行一次,
- 这样下来,时间复杂度是均摊 O ( n ) O(n) O(n)的。