[转载]LeetCode(5)-Python-最长回文子串(longest-palindromic-substring)

题目描述

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

示例 1:

输入: “babad”
输出: “bab”
注意: “aba” 也是一个有效答案。

示例 2:

输入: “cbbd”
输出: “bb”

解决思路1

回文字符串,就是顺序读取和逆序读取的结果是一样的,比如“上海自来水来自海上”,我们在判断回文字符串的时候,可以认为回文字符串都是有一个中心的,比如“上海自来水来自海上”的中心就是“水”,以“水”为中心,向两边拓展,每个对应的字符都是一样的,则我们认为这是一个回文字符串,这就是中心拓展思想。

但是我们应该还考虑一种情况,回文字符串长度为偶数的时候,比如“1221”,这个字符串的中心在“22”中间,依然可以用中心拓展思想来做,但是我们如何在代码里表示它的中心呢?我们在这里试着在字符串中间插入“#”字符,原来的字符串就变为“1#2#2#1”,那么这个回文字符串的中心就是“#”,对上面的字符串也进行字符中间插入“#”,那么“上#海#自#来#水#来#自#海#上”的中心依然是“水”,那么就很好的解决了字符串长度为奇数,或者偶数的问题。

我们观察上面的两个回文字符串可以发现,在转换之后的字符串中,第0,2,4,6…个字符是原来字符串中的字符,我们插入的“#”字符均在奇数位置上,那么我们就在遍历寻找中心的时候,当索引i指向偶数位置的时候,start = end = i//2,当索引i指向奇数位置的时候start = (i-1) // 2,end = (i+1)//2。具体代码如下:

#中心拓展算法
#时间复杂度O(n^2)
class Solution:
    def longestPalindrome(self, s):
        """
        :type s: str
        :rtype: str

        1#2#3#2#1
        """
        str_ = ""
        for i in range(2*len(s)-1):
            if i%2 == 0:           #索引i指向偶数位置
                start = end = i//2
                while start>=0 and end<len(s) and s[start]==s[end]:
                    start-=1
                    end+=1
            else:              #索引i指向奇数位置
                start = (i-1) // 2
                end = (i+1) //2
                while start>=0 and end<len(s) and s[start]==s[end]:
                    start-=1
                    end+=1
            if len(str_)<=(end-start-1):    #更新最长字符串
                str_ = s[start+1:end]
        return str_

结果 1296 ms

解决思路2

我们在判断回文字符串的时候,正常的情况下会这么判断:如i、j分别指向两个字符,如果字符i等于字符j,那么只要s[i+1,j-1]是回文字符串,那么s[i:j]也是回文字符串。这个就是动态规划的思想,我们在求解一个复杂问题的时候,可以利用以前的结果来缩短我们所需要的计算量。我们设置一个len(s)*len(s)的二维矩阵tmp,如果tmp[i][j]为0,则表示s[i:j]不是回文字符串,tmp[i][j]为1时,则表示s[i:j]为回文字符串,那么我们的思想可以简单归述为下面这个公式:
在这里插入图片描述
下面附上我们的实现的代码:

#动态规划思想
#时间复杂度O(n^2)
class Solution:
    def longestPalindrome(self,s):
        """
        :type s:str
        :rtype: str
        """
        str_ = ''
        s = list(s)

        tmp = [[0]*len(s) for i in range(len(s))] #生成二维数组

        for len_ in range(1,len(s)+1): #长度依次从1开始遍历
            for i in range(len(s)):
                j = len_+i-1
                if j<len(s):
                    if i==j:
                        tmp[i][j] = 1
                    if i+1 == j and s[i] == s[j]:
                        tmp[i][j] = 1
                    if j>i+1 and s[i] == s[j] and tmp[i+1][j-1]==1:
                        tmp[i][j] = 1

                    if tmp[i][j] == 1 and len(str_)<(j-i+1):
                        str_ = ''.join(s[i:j+1])

        return str_

结果 3836ms (偶尔会发生超出时间限制)

解决思路3

在处理最长回文子串的时候,有个线性的算法–Manacher算法,他可以将n^2的复杂度降到线性,大大缩短了程序计算的时间。

由于回文串的长度可奇可偶,比如"bob"是奇数形式的回文,"noon"就是偶数形式的回文,马拉车算法的第一步是预处理,做法是在每一个字符的左右都加上一个特殊字符,比如加上’#’,那么

bob --> #b#o#b#
noon --> #n#o#o#n#

这样做的好处是不论原字符串是奇数还是偶数个,处理之后得到的字符串的个数都是奇数个,这样就不用分情况讨论了,而可以一起搞定。接下来我们还需要和处理后的字符串t等长的数组p,其中p[i]表示以t[i]字符为中心的回文子串的半径,若p[i] = 1,则该回文子串就是t[i]本身,那么我们来看一个简单的例子:

#1 # 2 # 2 # 1 # 2 # 2 #
1 2 1 2 5 2 1 6 1 2 3 2 1

为啥我们关心回文子串的半径呢?看上面那个例子,以中间的 ‘1’ 为中心的回文子串 “#2#2#1#2#2#” 的半径是6,而未添加#号的回文子串为 “22122”,长度是5,为半径减1。这是个普遍的规律么?我们再看看之前的那个 “#b#o#b#”,我们很容易看出来以中间的 ‘o’ 为中心的回文串的半径是4,而 “bob"的长度是3,符合规律。再来看偶数个的情况"noon”,添加#号后的回文串为 “#n#o#o#n#”,以最中间的 ‘#’ 为中心的回文串的半径是5,而 “noon” 的长度是4,完美符合规律。所以我们只要找到了最大的半径,就知道最长的回文子串的字符个数了。只知道长度无法定位子串,我们还需要知道子串的起始位置。

我们还是先来看中间的 ‘1’ 在字符串 “#1#2#2#1#2#2#” 中的位置是7,而半径是6,貌似7-6=1,刚好就是回文子串 “22122” 在原串 “122122” 中的起始位置1。那么我们再来验证下 “bob”,“o” 在 “#b#o#b#” 中的位置是3,但是半径是4,这一减成负的了,肯定不对。所以我们应该至少把中心位置向后移动一位,才能为0啊,那么我们就需要在前面增加一个字符,这个字符不能是#号,也不能是s中可能出现的字符,所以我们暂且就用美元号 。 这 样 都 不 相 同 的 话 就 不 会 改 变 p 值 了 , 那 么 末 尾 也 要 增 加 一 个 特 殊 字 符 , 我 们 设 置 为 “ ‘ ” 。 那 此 时 “ o ” 在 “ 。这样都不相同的话就不会改变p值了,那么末尾也要增加一个特殊字符,我们设置为“`”。那此时 “o” 在 “ po#b#o#b#” 中的位置是4,半径是4,一减就是0了,貌似没啥问题。我们再来验证一下那个数字串,中间的 ‘1’ 在字符串 “$#1#2#2#1#2#2#” 中的位置是8,而半径是6,这一减就是2了,而我们需要的是1,所以我们要除以2。之前的 “bob” 因为相减已经是0了,除以2还是0,没有问题。再来验证一下 “noon”,中间的 ‘#’ 在字符串 “$#n#o#o#n#`” 中的位置是5,半径也是5,相减并除以2还是0,完美。可以任意试试其他的例子,都是符合这个规律的,最长子串的长度是半径减1,起始位置是中间位置减去半径再除以2。

那么我们的问题就转移到,如何求的数组p,只要求得一个正确的数组p,那么我们就可以正确的找到一个最长回文子串。我们设立两个辅助参数,center和mx,mx是回文串能延伸到的最右端的位置,center是延伸到最右端位置的回文串的中心。

当我们求解p的时候,我们可能就想到了上面的中心拓展算法,我们可以用中心拓展算法依次求得每个字符作为中心时的半径。但是我们有没有比较简便的方法计算p呢?

我们考虑到在从左往右遍历字符时,i永远都在center的右侧,那我们考虑imx的情况。
[转载]LeetCode(5)-Python-最长回文子串(longest-palindromic-substring)_第1张图片
当i

当i>mx的时候,我们发现直接使用中心拓展算法,然后更新center和mx即可。

我们实现的代码如下:

#manacher算法
#时间复杂度O(n)
class Solution:
    def longestPalindrome(self,s):

        if len(s) <= 1:
            return s

        # 每个字符之间插入 #
        ss = '$#' + '#'.join([x for x in s]) + '#`'
        p = [1] * len(ss)
        center = 0
        mx = 0
        max_str = ''
        for i in range(1, len(p)-1):

            if i < mx:
                j = 2 * center - i # i 关于 center 的对称点
                p[i] = min(p[j],mx-i)

            # 尝试继续向两边扩展,更新 p[i]
            while ss[i - p[i] ] == ss[i + p[i] ]: # 不必判断是否溢出,因为首位均有特殊字符,肯定会退出
                p[i] += 1

            # 更新中心
            if i + p[i] - 1 > mx:
                mx = i + p[i] - 1
                center = i

            # 更新最长串
            if 2 * p[i]-1 > len(max_str):
                max_str = ss[i - p[i]+1 : i + p[i]]

        return max_str.replace('#', '')

结果 120ms

GitHub地址:
https://github.com/wanghaoying/leetcode
参考博客:
Manacher’s Algorithm 马拉车算法:https://www.cnblogs.com/grandyang/p/4475985.html

你可能感兴趣的:(数据结构算法刷题)