本题作为动态规划看上去很直接,可以定义 dp[i]
为 s[:i+1]
所包含的回文子串的数量。但是这个符合直觉的 dp 数组定义却没法找到对应的递推公式,因为没有办法有效利用子问题的解。
实际上,本题的 dp 子问题记录体现在了回文这个性质上:如果我们能够用 dp 来得到所有子串是否回文,那显然这道题就能够轻松拿下了。
想到这一点的一个思路是,回文本身就是一个和 dp 非常契合的性质。
dp[i][j]
记录了 s[i:j+1]
是否回文(根据定义,
i
≤
j
i \leq j
i≤j)s[i] != s[j]
,那么显然 s[i:j+1]
不可能是个回文串,dp[i][j] = False
(这大概是第一次比较的元素不相等时比较容易递推)dp[i][j]
就依赖于子问题的结果。这里的递推有一些特殊情况需要讨论:
dp[i][j] = True
dp[i][j] = True
dp[i][j] = dp[i+1][j-1]
False
即可,不会影响到后续的递推dp[i][j]
是依赖于左下角的值的,所以我们需要从下往上、从左往右遍历s = "abcba"
a | b | c | b | a | |
---|---|---|---|---|---|
a | True | False | False | False | True |
b | False | True | False | True | False |
c | False | False | True | False | False |
b | False | False | False | True | False |
a | False | False | False | False | True |
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 i >= j - 1:
result += 1
dp[i][j] = True
else:
if 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.extendBothSide(s, i, i)
result += self.extendBothSide(s, i, i+1)
return result
def extendBothSide(self, s: str, left: int, right: int):
count = 0
while (left >= 0 and right < len(s)):
if s[left] != s[right]:
return count
else:
left -= 1
right += 1
count += 1
return count
时间复杂度为 O ( n 3 ) O(n^3) O(n3) 的逆天暴力解法,跟 dp 其实完全无关(除了我自以为的 dp 命名)
class Solution:
def countSubstrings(self, s: str) -> int:
# dp[i] represents the number of palindromes in s[:i+1]
dp = [0] * len(s)
dp[0] = 1
for i in range(1, len(s)):
dp[i] = dp[i-1]
for j in range(i+1):
if self.isPalindrome(s, j, i):
dp[i] += 1
return dp[-1]
def isPalindrome(self, s: str, start: int, end: int):
while (start <= end):
if s[start] != s[end]:
return False
start += 1
end -= 1
return True
本题第一眼看上去实在是要素拉满:之前的编辑问题子序列、刚刚出现的违背直觉的回文子串,看上去实在是太难了。但有了上一题对于回文子串的处理,实际上本题反而不算很难。
dp 数组的下标含义:dp[i][j]
是 s[i:j+1]
中最长回文子序列的长度
dp 递推公式:
s[i] == s[j]
,那么可以直接利用子问题的解直接获得一个更长的回文子序列:dp[i][j] = dp[i+1][j-1] + 2
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
dp 数组的初始化:由于我在递推公式中检查了 i ≤ j − 1 i\leq j-1 i≤j−1 的基础情况,所以直接全部初始化为 0 即可。
dp 的遍历顺序:和上一题一样,根据递推公式可以看到,dp[i][j]
依赖于左下角的值,所以从下向上、从左到右遍历
举例推导:s = "cbbd"
c | b | b | d | |
---|---|---|---|---|
c | 1 | 1 | 2 | 2 |
b | 0 | 1 | 2 | 2 |
b | 0 | 0 | 1 | 1 |
d | 0 | 0 | 0 | 1 |
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
# dp[i][j] represents the max length of the palindrome inside s[i:j+1]
dp = [[0] * len(s) for _ in range(len(s))]
for i in range(len(s)-1, -1, -1):
for j in range(i, len(s)):
if s[i] == s[j]:
if i >= j - 1:
dp[i][j] = j - i + 1
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][-1]
dp 问题实在是博大精深,除了解决 dp 的思路,更重要的是能够意识到题目可以用 dp 来解决。有的题目非常经典,一眼能看出来;有的题目(例如不相交的线 ,似乎完全不像 dp)。要能做到识别+解决,方为解决 dp 的真正法器。