最长的回文字符串第二部分
原文为英文页面,地址:https://articles.leetcode.com/longest-palindromic-substring-part-ii/
给定一个字符串S,找到S中最长的回文子字符串。
注意:
这是文章的第二部分:最长回文子串。在这里,我们描述了一个算法(Manacher算法),它可以在线性时间内找到最长的回文子串。请阅读第一部分了解更多背景信息。
在我以前的文章中,我们总共讨论了四种不同的方法,其中有一个非常简单的算法,运行时间为O(N 2),空间复杂度稳定。在这里,我们讨论在O(N)时间和O(N)空间中运行的算法,也称为Manacher算法。
提示:
想想你会如何改进简单的O(N 2)方法。考虑最糟糕的情况。最糟糕的情况是多个回文相互重叠的输入。例如,输入:“aaaaaaaaa”和“cabcbabcbabcba”。事实上,我们可以利用回文的对称性并避免一些不必要的计算。
一个O(N)解决方案(Manacher算法):
首先,我们通过在字母之间插入特殊字符‘#‘来将输入字符串S转换为另一个字符串T. 这么做的原因很快就会很快清楚。
例如:S =“abaaba”,T =“#a#b#a#a#b#a#”。
为了找到最长的回文子串,我们需要在每个T i周围扩展,使得T i-d ... T i + d形成回文。你应该马上看到d是以T i为中心的回文的长度。
我们将中间结果存储在数组P中,其中P [i]等于在T i处的回文中心的长度。最长的回文子串将成为P中的最大元素。
使用上面的例子,我们填充P如下(从左到右):
T = # a # b # a # a # b # a # P = 0 1 0 3 0 1 6 1 0 3 0 1 0
看着P,我们立即看到最长的回文是“abaaba”,如P 6 = 6所示。
您是否注意到通过在字母之间插入特殊字符(#),这两个长度均为偶数的回文是否被优雅地处理?(请注意:这是为了更容易地演示这个想法,并不一定需要对算法进行编码。)
现在,想象你在回文中心“abaaba”绘制一条想象的垂直线。你有没有注意到P中的数字是围绕这个中心对称的?不仅如此,请尝试使用另一个回文“aba”,这些数字也反映了类似的对称性。这是巧合吗?答案是肯定的,不是。这仅仅是一个条件,但无论如何,我们有很大的进步,因为我们可以消除P [i]的重新计算部分。
让我们继续讨论一个稍微复杂的例子,其中有更多的重叠回文,其中S =“babcbabcbaccba”。
上图显示T由S =“babcbabcbaccba”转化而来。假设你达到了表P部分完成的状态。垂直的实线表示回文“abcbabcba”的中心(C)。两条虚线垂直线分别表示其左(L)和右(R)边缘。您处于索引i处,并且围绕C的镜像索引是i‘。你如何有效地计算P [i]?
假设我们已经到达指数i = 13,并且我们需要计算P [13](由问号?表示)。我们首先看一下它在回文中心C周围的镜像索引i‘,索引i‘= 9。
上面的两条绿色实线表示以i和i‘为中心的两个回文序列覆盖的区域。我们看一下C周围的镜像索引,它是索引i‘。P [i‘] = P [9] = 1.由于回文在其中心附近具有对称性,因此P [i]也必须为1。
正如你在上面看到的那样,P [i] = P [i‘] = 1是非常明显的,由于回文中心周围的对称性,这一定是正确的。事实上,C之后的所有三个元素都遵循对称性(即P [12] = P [10] = 0,P [13] = P [9] = 1,P [14] = P [8] = 0)。
现在我们处于索引i = 15处。P [i]的价值是什么?如果我们遵循对称性,P [i]的值应该与P [i‘] = 7相同,但这是错误的。如果我们在T围绕中心展开15,它形成回文“a#b#a#b#a”,这实际上是比什么是它的对称对应显示短。为什么?
很显然,由两条实线表示的区域中的两个子串必须完全匹配。中心区域(用绿色虚线表示)也必须是对称的。注意P [i‘]是7,并且它一直扩展到回文的左边缘(L)(用红色实线表示),它不再落在回文的对称属性之下。我们所知道的是P [i] ≥5,并且为了找到P [i]的实际值,我们必须通过扩展右边缘(R)来进行字符匹配。在这种情况下,由于P [21]≠P [1],我们得出结论P [i] = 5。
我们来总结一下这个算法的关键部分,如下所示:
if P[ i’ ] ≤ R – i, then P[ i ] ← P[ i’ ] else P[ i ] ≥ P[ i’ ].(我们必须扩展到右边缘(R)以找到P [i]。
看看它有多优雅?如果你能够充分掌握上述总结,那么你已经获得了这个算法的本质,这也是最难的部分。
最后一部分是确定我们应该在何时将C的位置与R一起移动到右侧,这很容易:
如果以i为中心的回文右端扩展到R,我们将C更新为i(这个新回文的中心),并将R延伸到新回文的右边。
在每一步中,都有两种可能性。如果P [i]≤R-i,我们将P [i]设置为P [i‘],这只需要一步。否则,我们试图通过从右边缘R开始将回文中心改为i。扩展R(内部while循环)最多总共需要N步,并且定位和测试每个中心总共需要N步太。因此,该算法确保在至多2 * N步完成,给出线性时间解决方案。
// Transform S into T. // For example, S = "abba", T = "^#a#b#b#a#$". // ^ and $ signs are sentinels appended to each end to avoid bounds checking string preProcess(string s) { int n = s.length(); if (n == 0) return "^$"; string ret = "^"; for (int i = 0; i < n; i++) ret += "#" + s.substr(i, 1); ret += "#$"; return ret; } string longestPalindrome(string s) { string T = preProcess(s); int n = T.length(); int *P = new int[n]; int C = 0, R = 0; for (int i = 1; i < n-1; i++) { int i_mirror = 2*C-i; // equals to i‘ = C - (i-C) P[i] = (R > i) ? min(R-i, P[i_mirror]) : 0; // Attempt to expand palindrome centered at i while (T[i + 1 + P[i]] == T[i - 1 - P[i]]) P[i]++; // If palindrome centered at i expand past R, // adjust center based on expanded palindrome. if (i + P[i] > R) { C = i; R = i + P[i]; } } // Find the maximum element in P. int maxLen = 0; int centerIndex = 0; for (int i = 1; i < n-1; i++) { if (P[i] > maxLen) { maxLen = P[i]; centerIndex = i; } } delete[] P; return s.substr((centerIndex - 1 - maxLen)/2, maxLen); }
注意:
这个算法绝对不是微不足道的,在面试过程中你不会想到这样的算法。不过,我希望您喜欢阅读本文,希望它能帮助您理解这个有趣的算法。如果你走了这么远,你应该得到一个掌声!??
进一步思考:
- 事实上,这个问题存在第六种解决方案 - 使用后缀树。然而,它并不像这个那样高效(运行时间O(N log N)而且更多的开支在构建后缀树上)并且实现起来更加复杂。如果你有兴趣,请阅读维基百科有关最长回文子串的文章。
- 如果您需要找到最长的回文序列,该怎么办?(你知道子串和子序列之间的区别吗?)
有用的链接:
» Manacher的算法O(N)时间求字符串的最长回文子串(最好的解释,如果你可以读中文)
»一个简单的线性时间算法寻找最长的回文子串
»寻找回文
»寻找最长的回文在线性时间的子串
»维基百科:最长的回文子串