leetcode 5. Longest Palindromic Substring 字符串中的最长回文数 逐步从O(n^2)优化至线性时间

  • 题目
  • 解析
    • 思路一 暴力解法
    • 思路二 指针+最大长度
    • 思路3 由中间至两边找回数
    • 思路4 Manacher’s algorithm 线性时间
  • 参考文档

题目

链接

给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为1000。

示例 1:

输入: "babad"
输出: "bab"
注意: "aba"也是一个有效答案。
示例 2:

输入: "cbbd"
输出: "bb"

解析

这题应该算是面试中的经典题目了,题目描述很简单就是要找字符串中的最长的回文数

思路一 暴力解法

找到所有的回文字串,然后找到最长的
时间复杂度:O(n^3)
这一定是超时的

思路二 指针+最大长度

现在我们从回数的特点入手。
假若一个字符串是一个回数,那么该字符串内部一定还存在更多的回数。例如,abbbcbbba是一个回数,那么bbbcbbb一定是一个回数,那么bcb也是回数,最后到b。同理,bccb是一个回数,那么cc也是一个回数。因此可以看出,假设当前一个字符串是回数,那么加上两侧的字符可能还是回数。假设当前一个字符串不是回数,那么加上右侧的字符可能构成一个回数。
因此,假设当前得到的回数的最大长度为n,我们可以判断n+1或者n+2是不是回数。
为什么这么判断呢?下面给出证明。
我们假设有一个字符串xxxxxxxxabaxxxxxxs,其中x代表任意字符。
假设此时指针指向s,而已知最大回数子字符串的长度为3。我们只需要判断xxxs以及xxxxs是不是回数。无需判断xxs乃至更近是因为它们的长度必然无法超过当前的最大长度。而无需判断xxxxxs乃至更远是因为假如xxxxxs是回数,那么xxxx一定是回数,则当前的最大长度为4而不是3,与题设不符。所以只需判断两种情况。
这里就充分利用了回数的性质,省去了很多无效的遍历

class Solution:
# 判断是不是回文数
    def isPalindrome(self, s, start, end):
        if start < 0:
            return False
        while start < end:
            if s[start] != s[end]:
                return False
            start += 1
            end -= 1
        return True

    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        res = ""
        curr_len = 0
        for i in range(len(s)):
            if self.isPalindrome(s, i - curr_len - 1, i):
                res = s[i - curr_len - 1: i + 1]
                curr_len += 2
            elif self.isPalindrome(s, i - curr_len, i):
                res = s[i - curr_len : i + 1]
                curr_len += 1
        return res

时间复杂度:O(n^2)

思路3 由中间至两边找回数

通过从中间向两边查找,判断是否是回文数
时间复杂度:O(n^2)

class Solution2:
    """
    通过从中间向两边查找,判断是否是回文数,时间复杂度:O(n^2)
    """
    start, end  = 0, 0
    def extendsPalindrome(self, s, j, k,):
        while j >= 0 and k < len(s) and s[j] == s[k]:
            j -= 1
            k += 1
        return k - j - 1

    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str
        """
        length = len(s)
        if length < 2:
            return s
        for i in range(length):
            # 如果是奇数长度
            len1 = self.extendsPalindrome(s, i, i)
            # 如果是偶数长度
            len2 = self.extendsPalindrome(s, i, i + 1)
            max_len = max(len1, len2)
            if max_len > self.end - self.start:
                self.start = i - (max_len - 1) // 2
                self.end = i + (max_len) // 2
        return s[self.start : self.end + 1]

思路4 Manacher’s algorithm 线性时间

以上都是时间复杂度在 O(n^2)解决问题的,但是其实求字符串的最长回文数还有O(n)的方法,叫Manacher’s algorithm(马拉车算法)

首先我们将输入字符串S通过在每个字符间插入#来转换成T,例如如下:

 S = “abaaba”, T = “#a#b#a#a#b#a#”

为了找到最长的回文子串,我们需要在每个 Ti T i 周围扩展,使得 Tid....Ti+d T i − d . . . . T i + d 形成回文。你应该马上明白d是以 Ti T i 为中心的回文的长度。

我们将中间结果存储在数组 P P 中,其中 P[i] P [ i ] 等于在 Ti T i 处的回文中心的长度。最长的回文子串将成为 P P 中的最大元素。

使用上面的例子,我们填充 P P 如下(从左到右):

T = # a # b # a # a # b # a #
P = 0 1 0 3 0 1 6 1 0 3 0 1 0

通过这个表我们可以看见 P6=6 P 6 = 6 ,所以我们可以知道最长的回文数是abaaba

您是否注意到通过在字母之间插入特殊字符(#),这两个长度均为偶数的回文是否被优雅地处理?(请注意:这是为了更容易地演示这个想法,并不一定需要对算法进行编码。)

现在,想象你在回文中心“abaaba”绘制一条想象的垂直线。你有没有注意到 P P 中的数字是围绕这个中心对称的?不仅如此,请尝试使用另一个回文“aba”,这些数字也反映了类似的对称性。这是巧合吗?肯定不是。这仅仅是一个条件,但无论如何,我们有很大的进步,因为我们可以消除 P[i] P [ i ] 的重新计算部分。

让我们继续讨论一个稍微复杂的例子,其中有更多的重叠回文,其中S =“babcbabcbaccba”
img1

上图显示 T T S =“babcbabcbaccba”转化而来。假设你达到了表 P P 部分完成的状态。垂直的实线表示回文“abcbabcba”的中心 C ( C ) 。两条虚线垂直线分别表示其左 L ( L ) 和右 R ( R ) 边缘。你现在处于索引 i i 处,其围绕 C C 的镜像索引是 i i ′ 。你如何有效地计算 P[i] P [ i ]

假设我们已经到达指数 i=13 i = 13 ,并且我们需要计算 P[13] P [ 13 ] (由问号?表示)。我们首先看一下它在回文中心 C C 周围的镜像索引 i i ′ ,索引 i=9 i ′ = 9

面的两条绿色实线表示以i和i’为中心的两个回文序列覆盖的区域。我们看一下 C C 周围的镜像索引,它是索引 i i ′ P[i]=P[9]=1 P [ i ′ ] = P [ 9 ] = 1 .由于回文在其中心附近具有对称性,因此 P[i] P [ i ] 必须也是1。

正如你在上面看到的那样, P[i]=P[i]=1 P [ i ] = P [ i ′ ] = 1 是非常明显的,由于回文中心周围的对称性,这一定是正确的。事实上, C C 之后的所有三个元素都遵循对称性(即 P[12]=P[10]=0P[13]=P[9]=1P[14]=P[8]=0 P [ 12 ] = P [ 10 ] = 0 , P [ 13 ] = P [ 9 ] = 1 , P [ 14 ] = P [ 8 ] = 0 )。

现在我们处于索引 i=15 i = 15 ,其 C C 上的镜像索引是 i=7 i ′ = 7 。那么 [15]=P[7]=7 [ 15 ] = P [ 7 ] = 7 ?

现在我们处于索引 i=15 i = 15 处。 P[i] P [ i ] 的值是什么?如果我们遵循对称性, P[i] P [ i ] 的值应该与 P[i]=7 P [ i ′ ] = 7 相同,但这是错误的。如果我们在 T15 T 15 围绕中心展开,它形成回文“a#b#c#b#a”,这实际上是比它的对称的 T7 T 7 显示短。为什么?

在索引 i i i i ′ 中央围绕着彩色线条。绿色实线表示由于 C C 周围的对称性而必须匹配两侧的区域。红色实线表示可能不匹配两侧的区域。虚线绿色线条显示穿过中心的区域。

很显然,由两条实线表示的区域中的两个子串必须完全匹配。中心区域(用绿色虚线表示)也必须是对称的。注意 P[i] P [ i ′ ] 是7,并且它一直扩展到回文的左边缘 L ( L ) (用红色实线表示),它不再落在回文的对称属性之下。我们所知道的是 P[i]5 P [ i ] ≥ 5 ,并且为了找到P [i]的实际值,我们必须通过扩展右边缘 R ( R ) 来进行字符匹配。在这种情况下,由于 P[21]P[1] P [ 21 ] ≠ P [ 1 ] ,又因为 P[15] P [ 15 ] P[7] P [ 7 ] 是对应的, P[21] P [ 21 ] P[1] P [ 1 ] 分别在他们的两侧,我们得出结论 P[i]=5 P [ i ] = 5
总结一下这个算法的关键部分如下:

如果 P [i']R-i,
则 P [i]P [i'] 
否则 P [i]P[i']。(我们必须扩展到右边缘(R)以找到P [i]

看看它有多优雅?如果你能够充分掌握上述总结,那么你已经获得了这个算法的本质,这也是最难的部分。

最后一部分是确定我们应该在何时将C的位置与R一起移动到右侧,这很容易:

如果以i为中心的回文确实扩展到R,我们将C更新为i(这个新回文的中心),并将R延伸到新回文的右边。

在每一步中,都有两种可能性。如果 P[i]Ri P [ i ] ≤ R − i ,我们将 P[i] P [ i ] 设置为 P[i] P [ i ′ ] ,这只需要一步。否则,我们试图通过从右边缘R开始将回文中心改为i。扩展R(内部while循环)总共最多需要N个步骤,并且定位和测试每个中心总共需要N步太。因此,该算法确保在至多2 * N步完成,给出线性时间解决方案如下:

class Solution3:
    """
    Manacher's algorithm  Time complexity:O(n)
    """
    def preprocess(self, s):
        length = len(s)
        if length == 0:
            return "^$"
        ret = "^"
        for i in range(length):
            ret += "#" + s[i]
        ret += "#$"
        return ret

    def longestPalindrome(self, s):
        if len(s) < 2:
            return s
        t = self.preprocess(s)
        n = len(t)
        p = [0 for i in range(n)]
        C, R = 0, 0
        for i in range(1, n - 1):
            # 等于i' = C - (i - C)
            i_mirror = 2 * C - i

            # 最核心的算法思想
            if R > i:
                p[i] = min(R - i, p[i_mirror])
            else:
                p[i] = 0

            while t[i + 1 + p[i]] == t[i - 1 - p[i]]:
                p[i] += 1

            # 如果以i为中心的回文串扩展超过R,基于扩展的回文串扩展调整中心位置
            if i + p[i] > R:
                C, R = i, i + p[i]

        max_len = 0
        center_index = 0
        for i in range(1, n - 1):
            if p[i] > max_len:
                max_len = p[i]
                center_index = i
        start = (center_index - max_len) // 2
        end = start + max_len
        # print(start, max_len)
        return s[start:end]

参考文档

leetcode solution
Manacher’s algorithm
segmentfault

你可能感兴趣的:(leetcode)