一、【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