LeetCode第五题最长回文子串(Python)

题目描述

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

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

示例 2:

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

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解题方法和思路

暴力循环( O ( n 3 ) O(n^3) O(n3)

设立头尾两个游标遍历所有子串,一个一个判断。子串为个数为 O ( n 2 ) O(n^2) O(n2)级别,判断子串是否为回文串的复杂度则为 O ( n ) O(n) O(n)级别。因此总的时间复杂度为 O ( n 3 ) O(n^3) O(n3)
核心代码:

class Solution:
    def longestPalindrome(self, s: str) -> str:
        length = len(s)
        max_substring = 0
        if s == '' or length == 1:
            return s
        for i in range(length):
            for j in range(length,i,-1):
                if s[i:j] == s[i:j][::-1] and (j-i) > max_substring:
                    max_substring = (j-i)
                    temp_result_str = s[i:j]
                    break
        return temp_result_str

果不其然o(TヘTo)妥妥超时。
在这里插入图片描述

从中间开始向外扩展匹配( O ( n 2 ) O(n^2) O(n2))(自己做题时的实现)

思路:将回文串分为单数和双数分开讨论,单数时,确定一个中心点(最多n的中心点),之后向两边检测(最多 n 2 \frac{n}{2} 2n次判断),不匹配则跳出到下一个中心点进行判断。双数时则初始化 left \text{left} left right \text{right} right i \text{i} i i-1 \text{i-1} i-1即可。
看了解答以后发现这个方法有个名字叫中心扩散法。。。。
代码:

length = len(s)
        max_substring = 0
        result = ''
        if s == '' or length == 1:
            return s
        # 单数子串
        for i in range(length):
            left = right = i
            while left >= 0 and right < length and s[left] == s[right]:
                left -= 1
                right += 1
            if (right - left - 1) > max_substring:
                max_substring = (right - left - 1)
                result = s[left + 1:right]
        # 双数子串
        for i in range(length - 1):
            left = i
            right = i + 1
            while left >= 0 and right < length and s[left] == s[right]:
                left -= 1
                right += 1
            if (right - left - 1) > max_substring:
                max_substring = (right - left - 1)
                result = s[left + 1:right]
        return result

在这里插入图片描述

动态规划算法

解决这类 “最优子结构” 问题,可以考虑使用 “动态规划”:

1、定义 “状态”;
2、找到 “状态转移方程”。

记号说明: 下文中,使用记号 s[l, r] 表示原始字符串的一个子串,lr 分别是区间的左右边界的索引值,使用左闭、右闭区间表示左右边界可以取到。举个例子,当 s = 'babad' 时,s[0, 1] = 'ba's[2, 4] = 'bad'

1、定义 “状态”,这里 “状态”数组是二维数组。

dp[l][r] 表示子串 s[l, r](包括区间左右端点)是否构成回文串,是一个二维布尔型数组。即如果子串 s[l, r] 是回文串,那么 dp[l][r] = true

2、找到 “状态转移方程”。

首先,我们很清楚一个事实:

  1. 当子串只包含 1 个字符,它一定是回文子串;
  2. 当子串包含 2 个以上字符的时候:如果 s[l, r] 是一个回文串,例如“abccba”,那么这个回文串两边各往里面收缩一个字符(如果可以的话)的子串 s[l + 1, r - 1] 也一定是回文串,即:如果dp[l][r] == true 成立,一定有 dp[l + 1][r - 1] = true 成立。

根据这一点,我们可以知道,给出一个子串 s[l, r] ,如果 s[l] != s[r],那么这个子串就一定不是回文串。如果 s[l] == s[r] 成立,就接着判断 s[l + 1] 与 s[r - 1],这很像中心扩散法的逆方法。

事实上,当 s[l] == s[r] 成立的时候,dp[l][r] 的值由 dp[l + 1][r - 1] 决定,这一点也不难思考:当左右边界字符串相等的时候,整个字符串是否是回文就完全由“原字符串去掉左右边界”的子串是否回文决定。但是这里还需要再多考虑一点点:“原字符串去掉左右边界”的子串的边界情况。

  1. 当原字符串的元素个数为 3 个的时候,如果左右边界相等,那么去掉它们以后,只剩下 1个字符,它一定是回文串,故原字符串也一定是回文串;
  2. 当原字符串的元素个数为 2 个的时候,如果左右边界相等,那么去掉它们以后,只剩下 0 个字符,显然原字符串也一定是回文串。

把上面两点归纳一下,只要 s[l + 1, r - 1] 至少包含两个元素,就有必要继续做判断,否则直接根据左右边界是否相等就能得到原字符串的回文性。而“s[l + 1, r - 1] 至少包含两个元素”等价于 l + 1 < r - 1,整理得 l - r < -2,或者 r - l > 2

综上,如果一个字符串的左右边界相等,以下二者之一成立即可:

  1. 去掉左右边界以后的字符串不构成区间,即“ s[l + 1, r - 1] 至少包含两个元素”的反面,即 l - r >= -2,或者 r- l <= 2
  2. 去掉左右边界以后的字符串是回文串,具体说,它的回文性决定了原字符串的回文性。

于是整理成“状态转移方程”:

dp[l, r] = (s[l] == s[r] and (l - r >= -2 or dp[l + 1, r - 1]))

或者

dp[l, r] = (s[l] == s[r] and (r - l <= 2 or dp[l + 1, r - 1]))

**编码实现细节:**因为要构成子串 l 一定小于等于 r ,我们只关心 “状态”数组“上三角”的那部分取值。理解上面的“状态转移方程”中的 (r - l <= 2 or dp[l + 1, r - 1]) 这部分是关键,因为 or 是短路运算,因此,如果收缩以后不构成区间,那么就没有必要看继续 dp[l + 1, r - 1] 的取值。

具体编码细节在代码的注释中已经体现。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        size = len(s)
        if size <= 1:
            return s
        # 二维 dp 问题
        # 状态:dp[l,r]: s[l:r] 包括 l,r ,表示的字符串是不是回文串
        # 设置为 None 是为了方便调试,看清楚代码执行流程
        dp = [[False for _ in range(size)] for _ in range(size)]

        longest_l = 1
        res = s[0]

        # 因为只有 1 个字符的情况在最开始做了判断
        # 左边界一定要比右边界小,因此右边界从 1 开始
        for r in range(1, size):
            for l in range(r):
                # 状态转移方程:如果头尾字符相等并且中间也是回文
                # 在头尾字符相等的前提下,如果收缩以后不构成区间(最多只有 1 个元素),直接返回 True 即可
                # 否则要继续看收缩以后的区间的回文性
                # 重点理解 or 的短路性质在这里的作用
                if s[l] == s[r] and (r - l <= 2 or dp[l + 1][r - 1]):
                    dp[l][r] = True
                    cur_len = r - l + 1
                    if cur_len > longest_l:
                        longest_l = cur_len
                        res = s[l:r + 1]
            # 调试语句
            # for item in dp:
            #     print(item)
            # print('---')
        return res

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

作者:liweiwei1419
链接:https://leetcode-cn.com/problems/longest-palindromic-substring/solution/zhong-xin-kuo-san-dong-tai-gui-hua-by-liweiwei1419/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Manacher 算法

神仙算法,参考代码参考wiki我放弃了。(* /ω\*)

def manacher(s0 : str) -> list:
    T = '$#' + '#'.join(s0) + '#@'
    l = len(T)
    P = [0] * l
    R, C = 0, 0
    for i in range(1,l-1):
        if i < R:
            P[i] = min(P[2 * C - i], R - i)
        
        while T[i+(P[i]+1)] == T[i-(P[i]+1)]:
            P[i] += 1
        
        if P[i] + i > R:
            R = P[i] + i
            C = i
    return P
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值