Leetcode刷题笔记57:动态规划17(647. 回文子串-5. 最长回文子串-516. 最长回文子序列)

132 阅读4分钟

导语

leetcode刷题笔记记录,主要记录题目包括:

Leetcode 647. 回文子串

题目描述

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

 

示例 1:

输入: s = "abc"
输出: 3
解释: 三个回文子串: "a", "b", "c"

示例 2:

输入: s = "aaa"
输出: 6
解释: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

 

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成

动态规划解法

使用动规五部曲:

  1. dp数组含义:应该使用二维dp数组(好找规律),dp[i][j]表示字符串[i:j]是否为回文子串(左闭右闭);

  2. 递推公式:

    • 如果s[i]==s[j],则分为以下三种情况:
      • i==j:只有一个字符,肯定是,例如a
      • j-i=1:有两个字符,肯定是,例如aa
      • j-1>1:有多个字符,需要看dp[i+1][j-1]是否是,
    • 如果s[i]!=s[j],肯定不是
  3. 初始化:全部初始化为false

  4. 遍历顺序,由递推公式可以看到,dp[i][j]由dp[i+1][j-1]决定,所以应该从下往上,从左往右遍历

  5. 打印dp数组:略

完整代码如下:

class Solution:
    def countSubstrings(self, s: str) -> int:
        # 获取字符串的长度
        n = len(s)
        
        # 初始化一个二维动态规划数组,用于存储回文子串信息
        # dp[i][j] 为 True,表示 s[i:j+1] 是一个回文子串
        dp = [[False] * n for _ in range(n)]
        
        # 用于存储回文子串的数量
        result = 0

        # 从后往前遍历字符串,这样可以保证在计算 dp[i][j] 时,dp[i+1][j-1] 已经被计算过
        for i in range(n-1, -1, -1):
            for j in range(i, n):
                
                # 如果两个字符相等,那么它们有可能构成一个回文子串
                if s[i] == s[j]:
                    
                    # 如果子串 s[i:j+1] 的长度为 1 或 2,则它必然是一个回文子串
                    if j-i <= 1:
                        dp[i][j] = True
                        
                        # 增加回文子串的计数
                        result += 1
                    # 如果子串 s[i+1:j] 是一个回文子串,则 s[i:j+1] 也是一个回文子串
                    elif dp[i+1][j-1] is True:
                        dp[i][j] = True
                        
                        # 增加回文子串的计数
                        result += 1
        
        # 返回回文子串的数量
        return result

双指针解法

这道题目其实也可以通过双指针实现,即定义两个指针,从中心往两端扩散,只要碰到字符不相等,即终止扩散。外层,则使用一个for循环来遍历每个中心位置,同时,如果仅用一个字符为中心,会只判断子串为奇数个字符的情形,因此需要再考虑以相邻两个字符串为中心的情形。

完整代码如下:

class Solution:
    def countSubstrings(self, s: str) -> int:
        # 计数器,用于存储回文子串的数量
        count = 0
        
        # 获取字符串的长度
        n = len(s)
        
        # 定义一个辅助函数,用于扩展回文子串并计数
        # 其中,left 和 right 是中心点两侧的指针
        def extend_and_count(left: int, right: int) -> int:
            cnt = 0
            # 在边界内并且字符相等时扩展
            while left >= 0 and right < n and s[left] == s[right]:
                cnt += 1  # 发现一个回文子串
                left -= 1  # 向左扩展
                right += 1  # 向右扩展
            return cnt
        
        # 遍历每一个字符
        for i in range(n):
            # 以 s[i] 为中心的回文子串
            count += extend_and_count(i, i)
            
            # 以 s[i] 和 s[i+1] 中间为中心的回文子串
            # 只有当 i+1 < n 时,这样做才有意义
            if i + 1 < n:
                count += extend_and_count(i, i + 1)
        
        return count

Leetcode 5. 最长回文子串

题目描述

给你一个字符串 s,找到 s 中最长的回文子串。如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

 

示例 1:

输入: s = "babad"
输出: "bab"
解释: "aba" 同样是符合题意的答案。

示例 2:

输入: s = "cbbd"
输出: "bb"

 

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

解法

和上一题解法类似,只需要再多统计一步最大长度即可,使用动态规划和双指针法的具体代码如下:

  • 动态规划:
from typing import List

class Solution:
    def longestPalindrome(self, s: str) -> str:
        # 初始化变量
        length_of_s: int = len(s)  # 字符串s的长度
        max_length: int = 0  # 当前找到的最长回文子串的长度
        longest_substring: str = ''  # 当前找到的最长回文子串
        
        # 初始化DP表
        dp: List[List[bool]] = [[False] * length_of_s for _ in range(length_of_s)]
        
        # 单个字符都是回文
        for i in range(length_of_s):
            dp[i][i] = True
        
        # 检查回文并存储找到的最长的一个
        for i in range(length_of_s - 1, -1, -1):
            for j in range(i, length_of_s):
                if s[i] == s[j]:
                    if j - i <= 1:
                        dp[i][j] = True
                    else:
                        dp[i][j] = dp[i + 1][j - 1]
                
                # 如果找到更长的回文串,则更新最长回文串和其长度
                if dp[i][j] and j - i + 1 > max_length:
                    max_length = j - i + 1
                    longest_substring = s[i:j + 1]
        
        return longest_substring

  • 双指针:
class Solution:
    def longestPalindrome(self, s: str) -> str:
        max_len = 0  # 用于保存当前找到的最长回文子串的长度
        result = ''  # 用于保存当前找到的最长回文子串
        n = len(s)  # 字符串的长度

        def extend_and_count(left: int, right: int) -> (int, int):
            """向两边扩展指针,只要发现是回文串就继续。"""
            while left >= 0 and right < n and s[left] == s[right]:
                left -= 1  # 左指针向左移动
                right += 1  # 右指针向右移动
            return left + 1, right - 1  # 调整指针到有效的回文子串的边界

        for i in range(n):
            # 奇数长度的回文串
            left, right = extend_and_count(i, i)
            if right - left + 1 > max_len:
                max_len = right - left + 1
                result = s[left:right + 1]
            
            # 偶数长度的回文串,需要额外判断
            if i + 1 < n:
                left, right = extend_and_count(i, i + 1)
                if right - left + 1 > max_len:
                    max_len = right - left + 1
                    result = s[left:right + 1]

        return result

Leetcode 516. 最长回文子序列

题目描述

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

 

示例 1:

输入: s = "bbbab"
输出: 4
解释: 一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入: s = "cbbd"
输出: 2
解释: 一个可能的最长回文子序列为 "bb" 。

 

提示:

  • 1 <= s.length <= 1000
  • s 仅由小写英文字母组成

解法

使用动规五部曲:

  1. dp数组含义:dp[i][j]表示[i,j](左闭右闭)的最长回文子序列长度;

  2. 递推公式:

    • 如果s[i]==s[j]:
      • 如果j-i<=1:那么dp[i][j]=j-i+1
      • 如果j-i>1:那么dp[i][j]=dp[i+1][j-1]+2
    • 如果s[i]!=s[j]:
      • 考虑s[i]不考虑s[j],即dp[i][j-1]
      • 考虑s[j]不考虑s[i],即dp[i+1][j]
      • 综合考虑,取二者最大值
  3. 初始化:对角线上,即dp[i][i]=1

  4. 遍历顺序:由于递推公式中依赖于i+1和j-1,所以应该是从下到上,从左到右

  5. 打印dp数组:略

完整代码如下:

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        # 初始化字符串长度和DP数组。
        n = len(s)
        dp = [[0] * n for _ in range(n)]

        # 对角线上的值都是1,因为单个字符都是回文子序列。
        for i in range(n):
            dp[i][i] = 1
        
        # 从后向前遍历字符串。
        for i in range(n-1, -1, -1):
            # 从i到字符串末尾遍历。
            for j in range(i + 1, n):
                # 如果两个字符相等。
                if s[i] == s[j]:
                    # 如果是相邻或相同的字符。
                    if j - i <= 1:
                        dp[i][j] = j - i + 1
                    # 否则,两侧字符相等,所以回文子序列长度为子问题长度+2。
                    else:
                        dp[i][j] = dp[i + 1][j - 1] + 2
                # 如果两个字符不相等,则取两个子问题中较大的一个。
                else:
                    dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])

        # 返回从第一个字符到最后一个字符的最长回文子序列长度。
        return dp[0][n - 1]

总结

image.png