Manacher 算法
2017-05-14 19:28
85 查看
Manacher 算法是时间、空间复杂度都为 O(n) 的解决 Longest palindromic substring(最长回文子串)的算法。回文串是中心对称的串,比如 'abcba'、'abccba'。那么最长回文子串顾名思义,就是求一个序列中的子串中,最长的回文串。本文最后用 Python 实现算法,为了方便理解,文中出现的数学式也采用 py 的记法。
在 leetcode 上用时间复杂度 O(n**2)、空间复杂度 O(1) 的算法做完这道题之后,搜了一下发现有 O(n) 的算法。可惜英文 wikipedia 上的描述太抽象,中文介绍又没找到说的很明白的,于是就下决心自己写一篇中文比较清楚的。我弄明白这个算法是通过
leetcode 上的一篇文章,也就是 wikipedia 词条中第一个外部链接。链接在此(http://articles.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html),图文并茂,很容易懂(我就是没通读文章,主要看图和图的说明就弄懂了)。如果还是觉得读英文费劲的话,那接着读我这篇吧。
Manacher 算法的最终目的,是根据原串构造出一个新队列,内容是以该点为中心,最长的对称长度。为了解决对称奇偶性的问题(比如 aba 和 abba,常规算法需要分成两种情况),首先是构造一个辅助串,在首尾和任何两字符中间插入一个相同的字符。比如串 ababa,构造成 #a#b#a#b#a#。接下来就是构造一个新队列,里面记录以该点为中心的最长对称长度:
那么,具体该如何构造序列 P 呢?首先,去除串长不大于1的 corner case,我们总能得到 P 前两个元素的值。
然后,我们就可以根据已经知道的 P 元素和 T 中的元素一步一步求出后面的值了。问题分解成:已知 P[:i],求 P[i] 的问题。
接下来,就要讨论一下回文子使 P 具有哪些性质。下面用 leetcode 文章中的例子,s = 'babcbabcbaccba'(len(s) == 14,t = '#b#a#b#c#b#a#b#c#b#a#c#c#b#a#',len(t) == len(s)*2+1 == 29)。
如下图,假设当我们已知 T、P[:8] 时,求 P[9]。
观察我用方括号包起来的部分,正是以 T[7] 为中心,7为长度构成的回文。直观来看,由于回文是对称的结构,P 中的元素值似乎也应该是根据中心对称的,那么 P[9] = P[5] = 1,从结果上来看也是正确的。那么接下来往后填,很快你会发现这个结论有错误。
当填到 P[11] 时,红色字部分是 T[7] 为中心7为长度的回文,而黄色背景色部分,是以 P[11] 为中心9为长度的回文。按照上一段的结论,我们应该填入 P[7-(11-7)] = P[3] = 3,但实际上应该填入9。这是怎么回事呢?
为了说清楚这个问题,先来定义一些变量。首先 center 是已知的对称点,right 是已知对称点的最右端,当前求的 P 索引为 i,i 关于 center 的对称点索引是 mirror。上面在求 P[9] 和 P[11] 时,center == 7,right == 14。接下来,让我们接着往后扫描,看一下另外一个情况:已知 center==11, right==20, 求 P[15]。
观察这两个过程,不难发现,发生这种情况的原因,是 mirror 为中心点的回文(示例中用黄色背景标注,{}之间的回文),其范围超过了以 center 为中心点的回文的左端(红字标注,[] 之间的回文)。而凡是 P[i] == P[mirror] 的回文,其 P[mirror] 的范围都不超过 P[center] 的范围。具体来说,就是 P[mirror] 的左端不超过 P[center] 的左端。
那么怎么来判断呢?mirror 到 P[center] 左侧的长度是 right-i,如果这段长度不小于 P[mirror] 的话,P[mirror] 就在 P[center] 的范围内。如果没想明白的话,下面是用笨方法来的数学推导:
需要注意的是,由于 P[mirror] == right - i 情况的存在,也就是 P[mirror] 的左端与 P[center] 的左端重合,这也就意味着 P[i] 的当前右端为 right,P[i] 也就有可能会比 P[mirror] 长,这种情况下仍需对 P[i] 进行扩展操作。
到目前为止,通过已知的 p[:i] 用 O(1) 的操作计算得出 p[i] 的结果的过程就讲完了。这个过程是 Manacher 算法的核心,也是最难理解的部分,剩下的过程就不复杂了。
现在再回到本文一开始,当我们拿到源数据时,马上可以得到的是 P 的前两项。
开始计算,i=2, mirror=0, P[mirror] == right-i。按照上一节的结论,需要对 P 进行扩展,也就是对 T[i+n] == T[i-n] 进行依次判断。不幸的是,第一次就失败了,于是我们进入下一步:
不论是以 P[1] 为中心,还是 P[2] 为中心,right 的值总是2,而下一步 i > right,这时不论哪个是中心,都不存在 i 的对称点了。那这种情况下,我们就从零开始吧:
这次我们得到了新的 center、right,并且 right 比原来的大,那么就用新的来替换老的。
到现在为止,i > right 和 P[mirror] <= right - i 两种情况都已经讨论完了,那么还剩最后一种。
现在还剩下的情况就剩 i <= right and P[mirror] > right - i 这一种了。既然是要扩展 P,那么从零开始似乎也没什么不妥的。先考虑下面的情况:
这种情况前面已经见过一次了 P[mirror] > right-i。这时,我们需要扩展 P[15],但是需要从 n=0 开始判断 P[15+n] == P[15-n] 成立吗?因为 P[mirror]=7 > right-i=5,由于当 0<=n<=7 时,有 P[mirror-n] == P[mirror+n],而当 0<=n<=5 时有 P[mirror-n] == P[i+n],所以不难得出当 0<=n<=5 时有 P[i+n] == P[i-n]。换句话说,由于 P[mirror] 的回文范围超过了
P[center] 的范围,所以 P[i] 在不超过 P[center] 的范围内一定是回文。所以这时我们不需要从0开始判断,而只要从 right+1 开始判断就可以了。
再上代码之前,首先对上述情况进行重分类和合并。
1. 直接用 O(1) 操作得到 P[i],不需要对 P 进行扩展,也不需要扫描 T,center、right 的值也不变。
2. 需要对 P 进行扩展,这时会从 T[right+1] 开始扫描,同时也需要更新 center、right 的值。
情况1的复杂度为 O(1),情况2的复杂度虽然为 O(n),但由于算法的实现,能够保证从左到右扫描 T 时,每次都从 right+1 开始,并且扫描的最右端成为 right,这样就能够保证从左到右访问 T 中的每个元素不超过2次。所以,Manacher 算法的复杂度是 O(n)。
注:代码中P向量用pv表示
string findLongestPalindromeManacher(string s) {
string s1("$#");
for (auto ch : s) s1 += ch, s1 += "#"; // Insert '#'
vector<int> pv(s1.size(), 0);
int index(0), mlen(1);
for (int k1(2),mid(1),mxr(0); k1 < s1.size(); ++k1) {
pv[k1] = k1 < mxr ? min(pv[mid + mid - k1], mxr - k1) : 1;
while (s1[k1 + pv[k1]] == s1[k1 - pv[k1]]) ++pv[k1];
if (mxr < k1 + pv[k1]) {
mxr = k1 + pv[k1];
mid = k1;
}
if (mlen < pv[k1]) {
mlen = pv[k1];
index = k1;
}
}
return s.substr((index - mlen) / 2, mlen - 1);
}
下面还有一种实现方法,基本思路大同小异,这里不多做解释,有兴趣的可以看看。
string findLongestPalindromeManacher2(string s) {
string s1("#");
for (auto ch : s) s1 += ch, s1 += "#"; // Insert '#'
int sl1(s1.length()), index(0), mlen(1);
vector<int> pv(sl1, 0);
for (int k1(1), kd(1), kt; k1 < sl1;) {
while (0 <= k1 - kd && k1 + kd < sl1 && s1[k1 - kd] == s1[k1 + kd]) ++kd;
pv[k1] = kd - 1;
for (kt = 1; kt <= pv[k1] && pv[k1 - kt] < pv[k1] - kt; ++kt) pv[k1 + kt] = pv[k1 - kt];
k1 += kt;
kd = kd - kt;
}
for (int k1(1); k1 < sl1; ++k1) {
if (mlen < pv[k1]) {
mlen = pv[k1];
index = k1;
}
}
return s.substr((index - mlen) / 2, mlen);
}
在 leetcode 上用时间复杂度 O(n**2)、空间复杂度 O(1) 的算法做完这道题之后,搜了一下发现有 O(n) 的算法。可惜英文 wikipedia 上的描述太抽象,中文介绍又没找到说的很明白的,于是就下决心自己写一篇中文比较清楚的。我弄明白这个算法是通过
leetcode 上的一篇文章,也就是 wikipedia 词条中第一个外部链接。链接在此(http://articles.leetcode.com/2011/11/longest-palindromic-substring-part-ii.html),图文并茂,很容易懂(我就是没通读文章,主要看图和图的说明就弄懂了)。如果还是觉得读英文费劲的话,那接着读我这篇吧。
Manacher 算法的最终目的,是根据原串构造出一个新队列,内容是以该点为中心,最长的对称长度。为了解决对称奇偶性的问题(比如 aba 和 abba,常规算法需要分成两种情况),首先是构造一个辅助串,在首尾和任何两字符中间插入一个相同的字符。比如串 ababa,构造成 #a#b#a#b#a#。接下来就是构造一个新队列,里面记录以该点为中心的最长对称长度:
S1 = ababa T1 = # a # b # a # b # a # P1 = 0 1 0 3 0 5 0 3 0 1 0 S2 = abaaba T2 = # a # b # a # a # b # a # P2 = 0 1 0 3 0 1 6 1 0 3 0 1 0
那么,具体该如何构造序列 P 呢?首先,去除串长不大于1的 corner case,我们总能得到 P 前两个元素的值。
T = # ? # ... P = 0 1 ? ...
然后,我们就可以根据已经知道的 P 元素和 T 中的元素一步一步求出后面的值了。问题分解成:已知 P[:i],求 P[i] 的问题。
接下来,就要讨论一下回文子使 P 具有哪些性质。下面用 leetcode 文章中的例子,s = 'babcbabcbaccba'(len(s) == 14,t = '#b#a#b#c#b#a#b#c#b#a#c#c#b#a#',len(t) == len(s)*2+1 == 29)。
1.核心算法
如下图,假设当我们已知 T、P[:8] 时,求 P[9]。0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 T = [# b # a # b # c # b # a # b #] c # b # a # c # c # b # a # P = [0 1 0 3 0 1 0 7 0 ? ...
观察我用方括号包起来的部分,正是以 T[7] 为中心,7为长度构成的回文。直观来看,由于回文是对称的结构,P 中的元素值似乎也应该是根据中心对称的,那么 P[9] = P[5] = 1,从结果上来看也是正确的。那么接下来往后填,很快你会发现这个结论有错误。
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 T = {# b [# a # b #} c # b # a # b #] c # b # a # c # c # b # a # P = 0 1 0 3 0 1 0 7 0 1 0 ? ... i = 11 center = 7 right = 14 mirror = 3
当填到 P[11] 时,红色字部分是 T[7] 为中心7为长度的回文,而黄色背景色部分,是以 P[11] 为中心9为长度的回文。按照上一段的结论,我们应该填入 P[7-(11-7)] = P[3] = 3,但实际上应该填入9。这是怎么回事呢?
为了说清楚这个问题,先来定义一些变量。首先 center 是已知的对称点,right 是已知对称点的最右端,当前求的 P 索引为 i,i 关于 center 的对称点索引是 mirror。上面在求 P[9] 和 P[11] 时,center == 7,right == 14。接下来,让我们接着往后扫描,看一下另外一个情况:已知 center==11, right==20, 求 P[15]。
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 T = {# b [# a # b # c # b # a # b #} c # b # a #] c # c # b # a # P = 0 1 0 3 0 1 0 7 0 1 0 9 0 1 0 ? ... i = 15 center = 11 right = 20 mirror = 7
观察这两个过程,不难发现,发生这种情况的原因,是 mirror 为中心点的回文(示例中用黄色背景标注,{}之间的回文),其范围超过了以 center 为中心点的回文的左端(红字标注,[] 之间的回文)。而凡是 P[i] == P[mirror] 的回文,其 P[mirror] 的范围都不超过 P[center] 的范围。具体来说,就是 P[mirror] 的左端不超过 P[center] 的左端。
那么怎么来判断呢?mirror 到 P[center] 左侧的长度是 right-i,如果这段长度不小于 P[mirror] 的话,P[mirror] 就在 P[center] 的范围内。如果没想明白的话,下面是用笨方法来的数学推导:
PalindromeLeftOf(mirror) = mirror - P[mirror] PalindromeLeftOf(center) = center - (right - center) mirror = center - (i - center) if leftOf(mirror) is inside P PalindromeLeftOf(center) <= PalindromeLeftOf(mirror) => center - (right - center) <= mirror - P[mirror] => center - (right - center) <= center - (i- center) - P[mirror] => -right <= -i - P[mirror] => P[mirror] <= right - i
需要注意的是,由于 P[mirror] == right - i 情况的存在,也就是 P[mirror] 的左端与 P[center] 的左端重合,这也就意味着 P[i] 的当前右端为 right,P[i] 也就有可能会比 P[mirror] 长,这种情况下仍需对 P[i] 进行扩展操作。
到目前为止,通过已知的 p[:i] 用 O(1) 的操作计算得出 p[i] 的结果的过程就讲完了。这个过程是 Manacher 算法的核心,也是最难理解的部分,剩下的过程就不复杂了。
2.扩展 P 的过程
现在再回到本文一开始,当我们拿到源数据时,马上可以得到的是 P 的前两项。T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a # P = 0 1 ? ... center = 1 right = 2
开始计算,i=2, mirror=0, P[mirror] == right-i。按照上一节的结论,需要对 P 进行扩展,也就是对 T[i+n] == T[i-n] 进行依次判断。不幸的是,第一次就失败了,于是我们进入下一步:
T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a # P = 0 1 0 ? ... i = 3 center = ? right = 2 mirror =?
不论是以 P[1] 为中心,还是 P[2] 为中心,right 的值总是2,而下一步 i > right,这时不论哪个是中心,都不存在 i 的对称点了。那这种情况下,我们就从零开始吧:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a # P = 0 1 0 3 ? ... i = 4 center = 3 right = 6 mirror = 2
这次我们得到了新的 center、right,并且 right 比原来的大,那么就用新的来替换老的。
到现在为止,i > right 和 P[mirror] <= right - i 两种情况都已经讨论完了,那么还剩最后一种。
3.最后一步
现在还剩下的情况就剩 i <= right and P[mirror] > right - i 这一种了。既然是要扩展 P,那么从零开始似乎也没什么不妥的。先考虑下面的情况:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 T = # b # a # b # c # b # a # b # c # b # a # c # c # b # a # P = 0 1 0 3 0 1 0 7 0 1 0 9 0 1 0 ? ... i = 15 center = 11 right = 20 mirror = 7
这种情况前面已经见过一次了 P[mirror] > right-i。这时,我们需要扩展 P[15],但是需要从 n=0 开始判断 P[15+n] == P[15-n] 成立吗?因为 P[mirror]=7 > right-i=5,由于当 0<=n<=7 时,有 P[mirror-n] == P[mirror+n],而当 0<=n<=5 时有 P[mirror-n] == P[i+n],所以不难得出当 0<=n<=5 时有 P[i+n] == P[i-n]。换句话说,由于 P[mirror] 的回文范围超过了
P[center] 的范围,所以 P[i] 在不超过 P[center] 的范围内一定是回文。所以这时我们不需要从0开始判断,而只要从 right+1 开始判断就可以了。
代码
再上代码之前,首先对上述情况进行重分类和合并。1. 直接用 O(1) 操作得到 P[i],不需要对 P 进行扩展,也不需要扫描 T,center、right 的值也不变。
2. 需要对 P 进行扩展,这时会从 T[right+1] 开始扫描,同时也需要更新 center、right 的值。
情况1的复杂度为 O(1),情况2的复杂度虽然为 O(n),但由于算法的实现,能够保证从左到右扫描 T 时,每次都从 right+1 开始,并且扫描的最右端成为 right,这样就能够保证从左到右访问 T 中的每个元素不超过2次。所以,Manacher 算法的复杂度是 O(n)。
注:代码中P向量用pv表示
string findLongestPalindromeManacher(string s) {
string s1("$#");
for (auto ch : s) s1 += ch, s1 += "#"; // Insert '#'
vector<int> pv(s1.size(), 0);
int index(0), mlen(1);
for (int k1(2),mid(1),mxr(0); k1 < s1.size(); ++k1) {
pv[k1] = k1 < mxr ? min(pv[mid + mid - k1], mxr - k1) : 1;
while (s1[k1 + pv[k1]] == s1[k1 - pv[k1]]) ++pv[k1];
if (mxr < k1 + pv[k1]) {
mxr = k1 + pv[k1];
mid = k1;
}
if (mlen < pv[k1]) {
mlen = pv[k1];
index = k1;
}
}
return s.substr((index - mlen) / 2, mlen - 1);
}
下面还有一种实现方法,基本思路大同小异,这里不多做解释,有兴趣的可以看看。
string findLongestPalindromeManacher2(string s) {
string s1("#");
for (auto ch : s) s1 += ch, s1 += "#"; // Insert '#'
int sl1(s1.length()), index(0), mlen(1);
vector<int> pv(sl1, 0);
for (int k1(1), kd(1), kt; k1 < sl1;) {
while (0 <= k1 - kd && k1 + kd < sl1 && s1[k1 - kd] == s1[k1 + kd]) ++kd;
pv[k1] = kd - 1;
for (kt = 1; kt <= pv[k1] && pv[k1 - kt] < pv[k1] - kt; ++kt) pv[k1 + kt] = pv[k1 - kt];
k1 += kt;
kd = kd - kt;
}
for (int k1(1); k1 < sl1; ++k1) {
if (mlen < pv[k1]) {
mlen = pv[k1];
index = k1;
}
}
return s.substr((index - mlen) / 2, mlen);
}
相关文章推荐
- manacher求最长回文子串算法模板
- 求回文子串O(n) manacher 算法
- hihoCoder #1032 : 最长回文子串 [ Manacher算法--O(n)回文子串算法 ]
- Manacher 算法理解
- manacher算法
- Manacher算法 线性时间求最大回文子串长度
- HDU 3068_求最大回文串_manacher算法_O(n)
- Manacher算法,回文串及后缀数组问题
- manacher算法--最长回文子串
- 最长回文子串——Manacher 算法
- Manacher 算法
- Manacher(马拉车)算法详解
- Manacher算法:求解最长回文字符串,时间复杂度为O(N)
- hdu 3068 && pku 3974 (最长回文串)(Manacher 算法)
- hdu3294 Girls' research 【manacher算法】
- Manacher 算法
- hdu 3068 Manacher算法 O(n)回文子串算法
- Manacher's Algorithm 马拉车算法
- hdu5371 Hotaru's problem(manacher 算法+枚举)
- Manacher算法 O(n)回文子串算法