飞道的博客

回文子序列和回文子串问题

237人阅读  评论(0)

一、【LeetCode 516】最长回文子序列

1. 问题描述

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

示例 1:

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

示例 2:

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

2. 解题思路

一般涉及到子序列和最值问题,考察的基本上就是动态规划

这个问题中我们对 memo 数组的定义是: 在子串 s[i…j] 中,要求的最长回文子序列的长度为 memo[i][j]
根本取决于 s[i] 和 s[j] 这两个字符是否相等:
(1)若 s[i] = s[j],那么它俩加上 s[i+1…j-1] 中的最⻓回⽂⼦序列就是 s[i…j] 的最⻓回⽂⼦序列memo[i][j] = memo[i+1][j-1] + 2(注意此处是加上2,而不是1,因为有 s[i] 和 s[j] 这两个字符);
(2)若 s[i] != s[j],说明他俩不可能同时出现在 s[i…j] 的最长回文子序列中,那么把他们分别加入 s[i+1…j-1] 中,看哪个子串产生的回文子序列更长即可memo[i][j] = max(memo[i+1][j], memo[i][j-1])

根据 memo 数组的定义我们求的是 memo[0][n-1],也就是整个 s 的最长回文子序列

  • 首先确定边界情况,当 s 为空时,不存在回文子序列,返回0即可;
  • 初始化 memo 为二维数组(n行n列):memo = [[0 for j in range(n)] for i in range(n)],先考虑特殊情况——若只有一个字符,显然最长回文子序列长度就是1:memo[i][j] = 1(i == j);因为 i 要小于等于 j,所以对于 memo 二维数组中,i > j 的位置不存在子序列,初始化为0(最开始初始化 memo 时全部初始化为0即可)
  • 接下来看一般位置, 因为是根据 memo[i+1][j-1]memo[i+1][j]memo[i][j-1] 这三个位置来计算 memo[i][j],即已知一个格子左,下,左下角方向的值,计算该格子的值可以反向遍历,内循环是for j in range(i+1, n),j 从 i+1开始,保证 j 永远比 i 大。最后返回memo[0][-1]即可。

3. 代码

class Solution:
    def longestPalindromeSubseq(self, s: str) -> int:
        if not s:
            return 0
        n = len(s) 
        memo = [[0 for j in range(n)] for i in range(n)]
        for i in range(n):
            memo[i][i] = 1
        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                if s[i] == s[j]:
                    memo[i][j] = memo[i+1][j-1] + 2
                else:
                    memo[i][j] = max(memo[i+1][j], memo[i][j-1])
        return memo[0][-1]

二、【LeetCode 647】回文子串

1. 题目描述

给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。

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

示例 1:

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

示例 2:

输入: "aaa"
输出: 6
说明: 6个回文子串: "a", "a", "a", "aa", "aa", "aaa".

注意: 输入的字符串长度不会超过1000。

2. 第一种方法——动态规划

子串和子序列的区别在于子串是连续的,而子序列不要求连续。

使用动态规划来进行解决:

  • 定义一个memo数组:memo[i][j] 表示字符串 s 在[i…j]内的子串是否是一个回文串memo是一个 bool 类型的二维数组,所有值全部初始化为False:memo = [[False for j in range(n)] for i in range(n)]
  • 考虑状态转移:
    • 当只有一个字符时,比如a,肯定是一个回文串。
    • 当有两个字符时,如果这两个字符相等,比如aa,也是一个回文串。
    • 当有三个及以上字符时,比如 ababa 这个字符记作串1,去掉两头的字符a,也就是 bab 记作串2,可以看出只要串2是一个回文串,那么左右多出的那个字符也相等的串1必定也是回文串。即当s[i]==s[j]时,若 memo[i+1][j-1] 是一个回文串,那么 memo[i][j] 肯定也是一个回文串。
    • 所以s[i] == s[j] and (j - i < 2 or memo[i+1][j-1])里面,s[i] == s[j] 是必须的条件,j - i < 2是考虑一个或两个字符的情况;memo[i+1][j-1] 是考虑有三个字符以及以上字符的情况。若该条件为 True,则意味着 s 在[i…j]内的子串是一个回文串,可将 memo[i][j]置为True,然后将 res加一,统计回文子串的个数。
  • 最后返回 res 即可。

此种写法是正向遍历,要保证 i < j。

class Solution:
    def countSubstrings(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        memo = [[False for j in range(n)] for i in range(n)]
        res = 0
        for j in range(0, n):
            for i in range(0, j+1):
                if s[i] == s[j] and (j - i < 2 or memo[i+1][j-1]):
                    memo[i][j] = True
                    res += 1
        return res

此种写法是反向遍历,同516题一样,先对特殊情况(只有一个字符时:i==j)时进行了初始化:memo[i][i] = True(一个字符时肯定是回文串);接着就是双重循环反向遍历,条件不变。

class Solution:
    def countSubstrings(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        memo = [[False for j in range(n)] for i in range(n)]
        res = 0
        for i in range(n):
            memo[i][i] = True
            res += 1
        for i in range(n-1, -1, -1):
            for j in range(i+1, n):
                if s[i] == s[j] and (j - i < 2 or memo[i+1][j-1]):
                    memo[i][j] = True
                    res += 1
        return res

3. 第二种方法——中心扩展法

比如对一个字符串ababa,选择最中间的a作为中心点,往两边扩散,第一次扩散left指向的是b,right指向的也是b,所以是回文串,继续扩散,同理ababa也是回文串。

这是确定了一个中心点后的寻找的路径,然后我们只要寻找到所有的中心点,问题就解决了。

中心点一共有多少个呢?看起来像是和字符串长度相等,但如果是这样,上面的例子永远也搜不到 abab,以单个字符为中心点似乎永远不可能扩展到 abab 这样的四个字符,观察可知 abab 可由 ba 为中心点扩展得到所以中心点不能只有单个字符构成,还要包括两个字符,所以最终的中心点有 2 * n - 1个,分别是n个单字符和 n - 1个双字符(n是字符串的长度)。

  • 为什么有 2 * n - 1 个中心点?
    aba 有5个中心点,分别是 a、b、c、ab、ba
    abba 有7个中心点,分别是 a、b、b、a、ab、bb、ba
  • 三个字符可以由一个字符扩展得到,四个字符可以由两个字符扩展得到,所以只需要单字符和双字符组成的中心点。
  • left和right指针表示和中心点的关系;
    • 满足while循环条件时表示包括 left 指针和 right 指针在内的这个区间内的字符串是回文子串,接下来更新记录回文子串数目的变量 res,然后就可以向外扩展了(left 指针左移,right 指针右移)
  • 最后返回 res 即可。
class Solution:
    def countSubstrings(self, s: str) -> int:
        if not s:
            return 0
        n = len(s)
        res = 0
        for center in range(0, 2*n-1):
            left = center // 2
            right = left + center % 2
            while left >= 0 and right < n and s[left] == s[right]:
                res += 1
                left -= 1
                right += 1
        return res

三、【LeetCode 5】最长回文子串

1. 题目描述

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

示例 1:

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

示例 2:

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

2. 第一种方法——动态规划

上一题【回文子串】解决了本题就很容易了,区别在于上一题是统计回文子串的数量,本题是找出最长的回文子串,只需要将统计回文子串数目的变量 res改成记录最长回文子串,每次得到了回文子串后判断当前子串的长度是否大于 res 中已存的回文子串的长度if len(s[i:j+1]) > len(res),若是就更新 res 的值;若不是则继续循环判断。最后返回 res 即可。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
        n = len(s)
        memo = [[False for j in range(n)] for i in range(n)]
        res = ''
        for j in range(0, n):
            for i in range(0, j+1):
                if s[i] == s[j] and (j - i < 2 or memo[i+1][j-1]):
                    memo[i][j] = True
                    if len(s[i:j+1]) > len(res):
                        res = s[i:j+1]
        return res

3. 第二种方法——中心扩展法

与上一题【回文子串】的思路一模一样,需要改动的地方也跟动态规划一样,不再赘述。

class Solution:
    def longestPalindrome(self, s: str) -> str:
        if not s:
            return ''
        n = len(s)
        res = ''
        for center in range(0, 2*n-1):
            left = center // 2
            right = left + center % 2
            while left >= 0 and right < n and s[left] == s[right]:
                if len(s[left:right+1]) > len(res):
                    res = s[left:right+1] 
                	left -= 1
                	right += 1
        return res

转载:https://blog.csdn.net/oyall520/article/details/105471421
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场