导语
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 <= 1000s由小写英文字母组成
动态规划解法
使用动规五部曲:
-
dp数组含义:应该使用二维dp数组(好找规律),dp[i][j]表示字符串[i:j]是否为回文子串(左闭右闭);
-
递推公式:
- 如果s[i]==s[j],则分为以下三种情况:
- i==j:只有一个字符,肯定是,例如a
- j-i=1:有两个字符,肯定是,例如aa
- j-1>1:有多个字符,需要看dp[i+1][j-1]是否是,
- 如果s[i]!=s[j],肯定不是
- 如果s[i]==s[j],则分为以下三种情况:
-
初始化:全部初始化为false
-
遍历顺序,由递推公式可以看到,dp[i][j]由dp[i+1][j-1]决定,所以应该从下往上,从左往右遍历
-
打印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 <= 1000s仅由数字和英文字母组成
解法
和上一题解法类似,只需要再多统计一步最大长度即可,使用动态规划和双指针法的具体代码如下:
- 动态规划:
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 <= 1000s仅由小写英文字母组成
解法
使用动规五部曲:
-
dp数组含义:dp[i][j]表示[i,j](左闭右闭)的最长回文子序列长度;
-
递推公式:
- 如果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]
- 综合考虑,取二者最大值
- 如果s[i]==s[j]:
-
初始化:对角线上,即dp[i][i]=1
-
遍历顺序:由于递推公式中依赖于i+1和j-1,所以应该是从下到上,从左到右
-
打印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]