代码随想录27期|Python|Day52|动态规划|647. 回文子串|516. 最长回文子序列
本文是动态规划的回文字符串部分。
647. 回文子串
本题需要搞清楚dp的定义、遍历顺序和递推公式。
1、dp数组的定义
由图片可知,不同于之前的dp数组直接定义为当前遍历到的位置处题目所要求得值,而是应该定义为i为开始,j为结束的子串是否是回文串。
这是因为之前的遍历过程一般都是单向的,而回文串需要两遍同时进行扩展判断,所以不能够只保留遍历到“某一处”位置的回文串个数。
所以,dp的定义发生了改变,定义一个子序列是否是回文串。
2、递推公式
随着dp定义发生的变化,递推公式也需要进行修改。
具体来讲包括:结果个数的更新、向两侧扩展子串是否是回文串的判断。
2.1当前i和j所指向的字符相等
(1)如果只有一个元素,也就是j-i=1,那么肯定这个单一的字符是回文串;此时res += 1,同时需要把此处的dp[i][j] = True标记为回文串。
后来发现,其实此处的单个字符就是最小的回文串单元,它们构成了其余回文串判断的基础。
(2)如果有2个以上的元素,此时就需要进行判断了。
判断的依据是依靠上一个状态,也就是dp[i+1][j-1],也就是在[i, j]这个区间内分别往内收缩一个索引的区间。
如果这个收缩的区间是True(回文串),那么[i, j]区间肯定也是回文串。此时res += 1,并把dp[i][j] = True。
2.2当前i和j所指向的字符不相等
如果不相等的话这个区间就会被设置为False。实际上在初始化的时候已经全部置为Fasle了,所以不需要再进行操作。
3、初始化
初始化其实是根据定义来的,定义为判断i,j是回文子串后设置为True,那么初始化全部置为False即可,表示全部都不是回文串。
4、遍历顺序
遍历顺序由递推公式确定。
在递推公式中,除了直接对s中字符的比较之外,还需要依靠dp[i+1][j-1]来进行更新,可以看到这个dp的位置在当前位置的左下方。
那么,遍历的顺序就相应地i从大到小(从下往上)、j从小到大(从左往右)。
class Solution(object):
def countSubstrings(self, s):
"""
:type s: str
:rtype: int
"""
dp = [[False] * len(s) for _ in range(len(s))]
res = 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:
dp[i][j] = True
res += 1
elif dp[i+1][j-1] == True:
dp[i][j] = True
res += 1
return res
本题和之前题目的根本区别在于dp数组的定义需要根据回文串判断的依据,同时向两边扩展,这决定了dp没法直接设置为题目所要求的个数,而是表征区间是否是回文串。
双指针解法
双指针解法主要是在遍历的是否分别以当前单个字符和两个字符为中心向左右延伸,统计满足条件的情况。
class Solution(object):
def countSubstrings(self, s):
"""
:type s: str
:rtype: int
"""
# 双指针
res = 0
for i in range(len(s)):
res += self.extend(s, i, i, len(s)) # 单个字符作为中心向两边延伸
res += self.extend(s, i, i+1, len(s)) # 两个字符作为中心向两边延伸
return res
def extend(self, s, i, j ,length):
res = 0
# 如果当前字符串满足回文串的要求,且区间首位在整个字符串内
while i >= 0 and j < length and s[i] == s[j]:
i -= 1 # 向左扩张
j += 1 # 向右扩张
res += 1 # 结果增加1
return res
516. 最长回文子序列
本题在原来的基础上加上了自由删减的能力,所以在进行更新的时候会有所区别,和之前做的编辑距离类的更新方式类似。
1、dp定义
dp[i][j]定义为在i,j区间内的最长回文子串的长度,注意,这里是“包含”回文子串,和上一题的要求“必须以i和j为边界”不同。
2、dp更新
对于dp的更新分为两个方面:
(1)s[i] == s[j],此时相当于当前满足回文子串的要求,由于可以删除不要的元素,所以只要两个字符相等,就可以在原来dp数组的基础上+2;
(2)字符不相等时,分为两种情况:
<1>i索引向内收缩,变为i+1,j索引hold住不变(编辑距离的处理方式),此时dp[i][j] = dp[i+1][j];
<2>j索引向内收缩,变为j-1,i索引hold住不变(也是编辑距离的方式),此时dp[i][j] = dp[i][j-1]
综上,当字符不相等的时候,dp[i][j] = max(dp[i+1][j], dp[i][j-1]),相当于保持两个边界分别向内收缩后的最大值。
3、初始化
由于单个字符自身就算是最小的字符串单位(上一题的结论),所以初始化就是对角线元素全部设置为1(i=j的时候)。
4、遍历顺序
从图上和2、中的更新所用到的索引位置来看,dp更新需要左边、下边、左下方向的dp数组来更新,所以依然是外层i从大到小,内层j从小到大。
class Solution(object):
def longestPalindromeSubseq(self, s):
"""
:type s: str
:rtype: 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]
Day52完结!!!