【字符串】力扣647:回文子串
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。
回文字符串 是正着读和倒过来读一样的字符串。
子字符串 是字符串中的由连续字符组成的一个序列。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
示例:
输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"
暴力破解法
两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。
时间复杂度:O(n^3)
中心拓展
计算有多少个回文子串的最朴素方法就是枚举出所有的回文子串,而枚举出所有的回文字串又有两种思路,分别是:
- 枚举出所有的子串,然后再判断这些子串是否是回文;
- 双指针:枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。
假设字符串的长度为 n,前者会用 O(n^2)的时间枚举出所有的子串 s[\(l_i\)⋯\(r_i\)],然后再用 O(\(r_i\) - \(l_i\) + 1) 的时间检测当前的子串是否是回文,整个算法的时间复杂度是 O(n^3);而后者枚举回文中心的次数是 O(n) 的,对于每个回文中心拓展的次数也是 O(n) 的,所以时间复杂度是 O(n^2)。所以最好是选择第二种方法来枚举所有的回文子串。
在实现的时候要处理一个问题:如何有序地枚举所有可能的回文中心?
考虑回文长度是奇数和回文长度是偶数的两种情况:
- 如果回文长度是奇数,那么回文中心是一个字符
- 如果回文长度是偶数,那么中心是两个字符
中心点总数看起来像是和字符串长度相等,但如果是这样,上面的例子永远也搜不到 abab,想象一下单个字符的哪个中心点扩展可以得到这个子串?似乎不可能。所以中心点不能只有单个字符构成,还要包括两个字符,比如上面这个子串 abab,就可以有中心点 ba 扩展一次得到,所以最终的中心点有 2 * len - 1 个,分别是 len 个单字符和 len - 1 个双字符。
1. 两种中心点情况分别计算
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
count = 0 # 初始化回文子串数目
if n == 0 or n == 1:
return n
for i in range(n):
count += self.extend(s, i, i, n) # 以 i 为中心
count += self.extend(s, i, i + 1, n) # 以 i 和 i+1 为中心
return count
def extend(self, s, i, j, n):
res = 0
while i >= 0 and j < n and s[i] == s[j]:
res += 1
i -= 1
j += 1
return res
时间复杂度:O(n^2)。
空间复杂度:O(1)。
2. 两种中心点情况一起计算
这里要想清楚的是两个中心点left和right的关系:right = left + center % 2
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
count = 0 # 初始化回文子串数目
if n == 0 or n == 1:
return n
for center in range(2 * n + 1):
left = center // 2 # 注意left必须是整数,符号为//而非/
right = left + center % 2 # 当 left 为偶数,只有1个中心点;当 left 为奇数,有2个中心点
while left >= 0 and right < n and s[left] == s[right]:
count += 1
left -= 1
right += 1
return count
动态规划
- 确定 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,例如cabac,此时 s[i] 与 s[j] 已经相同了,看 i 到 j 区间是不是回文子串就看 aba 是不是回文就可以了,那么 aba 的区间就是 i+1 与 j-1区间,这个区间是不是回文子串就看dp[i + 1][j - 1]是否为true。
-
以上三种情况分析完了,那么 dp 为 True 的递归公式为:
- dp数组初始化
dp[i][j]可以初始化为True么? 当然不行,怎么能刚开始就全都匹配上了。所以dp[i][j]初始化为False。
- 确定遍历顺序
首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为True,在对dp[i][j]进行赋值True的。在矩阵中,dp[i][j]在dp[i + 1][j - 1]的右上角,要保证计算dp[i][j]时dp[i + 1][j - 1]已经计算出来,就得先遍历列再遍历行,即从下到上,从左到右遍历。
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): # 注意遍历顺序,注意 i 从 len(s) - 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
作者:carlsun-2
链接:https://leetcode.cn/problems/palindromic-substrings/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-dpzi-vidge/
简洁版
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
作者:carlsun-2
链接:https://leetcode.cn/problems/palindromic-substrings/solution/dai-ma-sui-xiang-lu-dai-ni-xue-tou-dpzi-vidge/
时间复杂度:O(n^2)
空间复杂度:O(n^2)

浙公网安备 33010602011771号