代码随想录算法训练营Day40 | Leetcode 647. 回文子串、516.最长回文子序列
一、回文子串
相关题目:Leetcode647
文档讲解:Leetcode647
视频讲解:Leetcode647
1. Leetcode647. 回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。
提示:
- 1 <= s.length <= 1000
- s 由小写英文字母组成
- 思路:
- 动规五部曲:
- 确定 dp 数组以及下标的含义:本题如果定义 dp[i] 为下标 i 结尾的字符串有 dp[i] 个回文串的话,很难找到递归关系。需要观察回文串的性质,如图:
在判断字符串 s 是否是回文时,如果已知 s[i+1],…,s[j-1] 这个子串是回文的,那么只需要比较 s[i] 和 s[j] 这两个元素是否相同,如果相同的话,这个字符串 s 就是回文串。因此 dp 数组应该要定义成二维 dp 数组,dp[i][j] 表示区间范围 [i,j] (注意是左闭右闭)的子串是否是回文子串,如果是 dp[i][j] 为 true,否则为 false。 - 确定递推公式:在确定递推公式时需要分析如下几种情况:
- 当 s[i] 与 s[j] 不相等,则 dp[i][j] 一定是 false。
- 当 s[i] 与 s[j] 相等,有如下三种情况:
- 情况一:下标 i 与 j 相同,同一个字符例如 a,当然是回文子串;
- 情况二:下标 i 与 j 相差为 1,例如 aa,也是回文子串;
- 情况三:下标 i 与 j 相差大于 1 时,此时 s[i] 与 s[j] 已经相同,只需看 i+1 至 j-1 区间是不是回文即可,也就是看 dp[i + 1][j - 1] 是否为 true。
- dp 数组如何初始化:刚开始无法匹配上,所以 dp[i][j] 都初始化为 false。
- 确定遍历顺序:从递推公式中可以看出,情况三是根据 dp[i + 1][j - 1] 是否为 true,再对 dp[i][j] 进行赋值 true 的。dp[i + 1][j - 1] 在 dp[i][j] 的左下角,所以一定要从下到上,从左到右遍历,这样保证 dp[i + 1][j - 1] 都是经过计算的。
- 举例推导 dp 数组:以输入:“aaa” 为例,dp[i][j] 状态如下:
- 确定 dp 数组以及下标的含义:本题如果定义 dp[i] 为下标 i 结尾的字符串有 dp[i] 个回文串的话,很难找到递归关系。需要观察回文串的性质,如图:
- 动规五部曲:
- 动规
class Solution:
def countSubstrings(self, s: str) -> int:
dp = [[False] * len(s) for _ in range(len(s))]
result = 0
for i in range(len(s)-1, -1, -1): #注意遍历顺序
for j in range(i, len(s)):
if s[i] == s[j]:
if j - i <= 1: #情况一 和 情况二
result += 1
dp[i][j] = True
elif dp[i+1][j-1]: #情况三
result += 1
dp[i][j] = True
return result
###简洁版
class Solution:
def countSubstrings(self, s: str) -> int:
dp = [[False] * len(s) for _ in range(len(s))]
result = 0
for i in range(len(s)-1, -1, -1): #注意遍历顺序
for j in range(i, len(s)):
if s[i] == s[j] and (j - i <= 1 or dp[i+1][j-1]):
result += 1
dp[i][j] = True
return result
- 双指针法
class Solution:
def countSubstrings(self, s: str) -> int:
result = 0
for i in range(len(s)):
result += self.extend(s, i, i, len(s)) #以i为中心
result += self.extend(s, i, i+1, len(s)) #以i和i+1为中心
return result
def extend(self, s, i, j, n):
res = 0
while i >= 0 and j < n and s[i] == s[j]:
i -= 1
j += 1
res += 1
return res
二、最长回文子序列
相关题目:Leetcode516
文档讲解:Leetcode516
视频讲解:Leetcode516
1. Leetcode516.最长回文子序列
给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
提示:
- 1 <= s.length <= 1000
- s 仅由小写英文字母组成
- 思路:
- 动规五部曲:
-
确定 dp 数组以及下标的含义:dp[i][j] 表示字符串 s 在 [i, j] 范围内最长的回文子序列的长度为 dp[i][j]。
-
确定递推公式:
- 如果 s[i] 与 s[j] 相同,那么 dp[i][j] = dp[i + 1][j - 1] + 2,如图:
- 如果 s[i] 与 s[j] 不相同,说明 s[i] 和 s[j] 的同时加入并不能增加 [i,j] 区间回文子序列的长度,那么可以分别加入 s[i]、s[j] 看看是否可以组成最长的回文子序列:
- 加 入s[j] 的回文子序列长度为 dp[i + 1][j],
- 加入 s[i] 的回文子序列长度为 dp[i][j - 1],
那么 dp[i][j] 取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])。
- 如果 s[i] 与 s[j] 相同,那么 dp[i][j] = dp[i + 1][j - 1] + 2,如图:
-
dp 数组如何初始化:从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2 可以看出递推公式是计算到 i 和 j 相同时候的情况,所以需要初始化 dp[i][i]。当 i 与 j 相同,dp[i][j] 等于 1,即:一个字符的回文子序列长度就是 1。其他情况 dp[i][j] 初始为 0 ,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 中 dp[i][j] 才不会被初始值覆盖。
-
确定遍历顺序:从递归公式中,可以看出 dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图:
所以遍历 i 的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的;而 j 可以正常从左向右遍历。 -
举例推导 dp 数组:以输入 s:“cbbd” 为例,dp 数组状态如图:
-
- 动规五部曲:
- 动规
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
dp = [[0] * len(s) for _ in range(len(s))]
for i in range(len(s)):
dp[i][i] = 1
for i in range(len(s)-1, -1, -1):
for j in range(i+1, len(s)):
if s[i] == s[j]:
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][-1]