字符串系列2 Manacher 算法
2018-02-07 21:05
162 查看
原博主已经写得非常清楚了,这里我只是将原文转过来,原博文如下:Manacher算法总结
计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为 O(n3)O(n3) 的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为 O(n2)O(n2),但是当n比较大的时候仍然无法令人满意,Manacher 算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
首先,Manacher 算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用 # 号。下面举一个例子:
对于上面的例子,可以得出 Len[i]Len[i] 数组为:
LenLen 数组有一个性质,那就是 Len[i]−1Len[i]−1 就是该回文子串在原字符串 SS 中的长度,至于证明,首先在转换得到的字符串 TT 中,所有的回文字串的长度都为奇数,那么对于以 T[i]T[i] 为中心的最长回文字串,其长度就为 2∗Len[i]−12∗Len[i]−1,经过观察可知,TT中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有 Len[i]Len[i] 个分隔符,剩下 Len[i]−1Len[i]−1 个字符来自原字符串,所以该回文串在原字符串中的长度就为 Len[i]−1Len[i]−1。
有了这个性质,那么原问题就转化为求所有的 Len[i]Len[i]。下面介绍如何在线性时间复杂度内求出所有的 LenLen。
第一种情况:i⩽Pi⩽P
那么找到 ii 相对于 popo 的对称位置,设为 jj,那么如果 Len[j]<P−iLen[j]<P−i,如下图:
那么说明以 jj 为中心的回文串一定在以 popo 为中心的回文串的内部,且 jj 和 ii 关于位置 popo对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即 Len[i]⩾Len[j]Len[i]⩾Len[j]。因为Len[j]<P−iLen[j]<P−i,所以说 i+Len[j]<Pi+Len[j]<P。由对称性可知 Len[i]=Len[j]Len[i]=Len[j]。
如果 Len[j]⩾P−iLen[j]⩾P−i,由对称性,说明以 ii 为中心的回文串可能会延伸到 PP 之外,而大于 PP 的部分我们还没有进行匹配,所以要从 P+1P+1 位置开始一个一个进行匹配,直到发生失配,从而更新 PP 和对应的 popo 以及 Len[i]Len[i]。
第二种情况: i>Pi>P
如果 ii 比 PP 还要大,说明对于中点为 ii 的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的 popo 以及 Len[i]Len[i]。
Python 实现如下,虽然这里也写了预处理函数,但是其实在 Python 中一句话就可以实现预处理函数,因此这里的预处理函数没用到。由于对转换后字符串的每个字符只遍历了一次,因此时间复杂度是线性的:
Manacher算法介绍
Manacher 算法是查找一个字符串的最长回文子串的线性算法。在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简单来说就是正着读和反着读都是一样的字符串,比如 abba,noon 等等,一个字符串的最长回文子串即为这个字符串的子串中,是回文串的最长的那个。计算字符串的最长回文字串最简单的算法就是枚举该字符串的每一个子串,并且判断这个子串是否为回文串,这个算法的时间复杂度为 O(n3)O(n3) 的,显然无法令人满意,稍微优化的一个算法是枚举回文串的中点,这里要分为两种情况,一种是回文串长度是奇数的情况,另一种是回文串长度是偶数的情况,枚举中点再判断是否是回文串,这样能把算法的时间复杂度降为 O(n2)O(n2),但是当n比较大的时候仍然无法令人满意,Manacher 算法可以在线性时间复杂度内求出一个字符串的最长回文字串,达到了理论上的下界。
Manacher算法原理与实现
下面介绍Manacher算法的原理与步骤。首先,Manacher 算法提供了一种巧妙地办法,将长度为奇数的回文串和长度为偶数的回文串一起考虑,具体做法是,在原字符串的每个相邻两个字符中间插入一个分隔符,同时在首尾也要添加一个分隔符,分隔符的要求是不在原串中出现,一般情况下可以用 # 号。下面举一个例子:
(1) Len 数组简介与性质
Manacher 算法用一个辅助数组 Len[i]Len[i] 表示以字符 T[i]T[i] 为中心的最长回文字串的最右字符到 T[i]T[i] 的长度,比如以 T[i]T[i] 为中心的最长回文字串是 T[l,r]T[l,r],那么 Len[i]=r−i+1Len[i]=r−i+1。对于上面的例子,可以得出 Len[i]Len[i] 数组为:
LenLen 数组有一个性质,那就是 Len[i]−1Len[i]−1 就是该回文子串在原字符串 SS 中的长度,至于证明,首先在转换得到的字符串 TT 中,所有的回文字串的长度都为奇数,那么对于以 T[i]T[i] 为中心的最长回文字串,其长度就为 2∗Len[i]−12∗Len[i]−1,经过观察可知,TT中所有的回文子串,其中分隔符的数量一定比其他字符的数量多1,也就是有 Len[i]Len[i] 个分隔符,剩下 Len[i]−1Len[i]−1 个字符来自原字符串,所以该回文串在原字符串中的长度就为 Len[i]−1Len[i]−1。
有了这个性质,那么原问题就转化为求所有的 Len[i]Len[i]。下面介绍如何在线性时间复杂度内求出所有的 LenLen。
(2) Len数组的计算
首先从左往右依次计算 Len[i]Len[i],当计算 Len[i]Len[i] 时,Len[j](0⩽j<i)Len[j](0⩽j<i) 已经计算完毕。设 PP 为之前计算中最长回文子串的右端点的最大值,并且设取得这个最大值的位置为 popo,分两种情况:第一种情况:i⩽Pi⩽P
那么找到 ii 相对于 popo 的对称位置,设为 jj,那么如果 Len[j]<P−iLen[j]<P−i,如下图:
那么说明以 jj 为中心的回文串一定在以 popo 为中心的回文串的内部,且 jj 和 ii 关于位置 popo对称,由回文串的定义可知,一个回文串反过来还是一个回文串,所以以i为中心的回文串的长度至少和以j为中心的回文串一样,即 Len[i]⩾Len[j]Len[i]⩾Len[j]。因为Len[j]<P−iLen[j]<P−i,所以说 i+Len[j]<Pi+Len[j]<P。由对称性可知 Len[i]=Len[j]Len[i]=Len[j]。
如果 Len[j]⩾P−iLen[j]⩾P−i,由对称性,说明以 ii 为中心的回文串可能会延伸到 PP 之外,而大于 PP 的部分我们还没有进行匹配,所以要从 P+1P+1 位置开始一个一个进行匹配,直到发生失配,从而更新 PP 和对应的 popo 以及 Len[i]Len[i]。
第二种情况: i>Pi>P
如果 ii 比 PP 还要大,说明对于中点为 ii 的回文串还一点都没有匹配,这个时候,就只能老老实实地一个一个匹配了,匹配完成后要更新P的位置和对应的 popo 以及 Len[i]Len[i]。
Python 实现如下,虽然这里也写了预处理函数,但是其实在 Python 中一句话就可以实现预处理函数,因此这里的预处理函数没用到。由于对转换后字符串的每个字符只遍历了一次,因此时间复杂度是线性的:
def pre_process(s): n = len(s) if n == 0: return "^$" t = "^" for i in range(0, n, 1): t += '#' + s[i] t += '#$' return t def mancher(s): # p[i] records the longest palindrome substring centered on s_new[i] # ps and pl is the longest palindrome substring and its length # r is the maximum value of the right end point of the longest palindrome substring in the previous calculation # and the position obtained from this maximum is pos # s_new = pre_process(s) s_new = "^$" if len(s) == 0 else '^#' + '#'.join(s) + '#$' p = [0] * len(s_new) pl = pos = r = 0 ps = "" for i in range(1, len(s_new) - 1, 1): # 2 * pos - i is i's symmetry point about pos p[i] = min(r - i, p[2 * pos - i]) if r > i else 1 while s_new[i - p[i]] == s_new[i + p[i]]: p[i] += 1 if p[i] + i > r: r = p[i] + i pos = i # update if pl < p[i]: pl = p[i] - 1 begin = (i - p[i]) / 2 ps = s[begin:begin + pl] return pl, ps
相关文章推荐
- 每天学习一算法系列(22)(在字符串中找出连续最长的数字串,并把这个串的长度返回)
- C/C++面试之算法系列--几个最大子字符串的算法题
- 求取最长回文字符串,o(n)的最优算法manacher
- 字符串专题小结:Manacher算法求最长回文串
- [算法系列之七]Manacher算法之最大回文子串
- 算法系列之四:字符串的相似度
- 算法系列(一) 删除字符串中的相同字符片段
- C/C++面试之算法系列--atoi(char *str)将字符串转换成整数
- HDU 4513 吉哥系列故事——完美队形II(Manacher算法最大回文长度 && 两侧沿中点递减)
- C/C++面试之算法系列--如何实现用更少的空间表示英文字母(a ~ z)构成char A[n]字符串
- 算法细节系列(34):再见字符串(2)
- 每天学习一算法系列(20)(输入一个表示整数的字符串,把该字符串转换成整数并输出)
- 【常用算法思路分析系列】字符串高频题集
- C# SEO整合系列之字符串相似度算法——Levenshtein Distance method
- 算法细节系列(14):动态规划之字符串处理
- Manacher 算法(求字符串的回文子串的最大长度)
- 算法系列之四:字符串的相似度
- Manacher算法求字符串的最长回文子串
- 算法系列—低位优先的字符串排序(基于键索引计数法)
- C/C++面试之算法系列--从“整数转换成字符串”看算法的联想