二维动态规划
从左到右依次遍历型
1 要观察出正方形的边长取决于三个因素,上边,左边,对角线的正方形边长的最小值,写出动态规划方程是关键,注意matrix里面的数据类型是字符串,不是数字0 1,所以不能直接用if判断,用if只要字符串不空就为真,
2 同上一题,容易观察出以dp[i][j]为右下端点的最大正方形的边长也是其正方形的个数,
3 这个题无论用什么动态规划方法都要从四个方向遍历四次,每个位置的最大值是四个方向中的最小值,最后再求最大值,对于数组查找不方便时,可以先将其变为集合,注意set内的元素必须是不可变对象,所以不能是数组,要把数组变为tuple再放入set中,或者可以直接建一个二维数组来记录0 1
下面是非常相似的一类题型,都是处理两个字符串的问题,定义dp的时候行和列长度都要加1,因为要做初始化,状态转移方程不好写时可以举例子来写,注意初始化加了一列一行后,后面遍历时的索引是从1到row+1和从1到col+1,
4 高频面试题,和编辑距离类似,关键是写出状态转移方程,子序列问题往往可以用动态规划,这个题可以用两个数组互相赋值记录数据来降低空间复杂度,
dp[i][j]= dp[i−1][j−1]+1 s1[i]==s2[j]
max(dp[i−1][j],dp[i][j−1]) s1[i]!= s2[j]
和最长公共子序列实际上是同一个题,
思路同上,dp[i][j]为公共子序列的最大和,先求出所有字符的和,再减去公共的字符之和乘2,或者可以直接定义dp[i][j]为最小的删除和,只不过初始化的时候不为0,下面为计算最大公共子序列字符之和的动态转移方程:
dp[i][j]=dp[i−1][j−1] + ord(s1[col-1]) 如果当前字符相等
class Solution: def minimumDeleteSum(self, s1: str, s2: str) -> int: l1 = len(s1) l2 = len(s2) dp = [[0] * (l1+1) for _ in range(l2+1)] for i in range(1, l1+1): dp[0][i] += (dp[0][i-1]+ord(s1[i-1])) for i in range(1, l2+1): dp[i][0] += (dp[i-1][0]+ord(s2[i-1])) for col in range(1, l1+1): for row in range(1, l2+1): if s1[col-1] == s2[row-1]: dp[row][col] = dp[row-1][col-1] else: dp[row][col] = min(dp[row][col-1]+ord(s1[col-1]), dp[row-1][col]+ord(s2[row-1])) return dp[-1][-1]
可以直接用最长公共子序列的方法解,也可以直接定义dp[row][col]为word1[:row-1]到word2[:col-1]的最少的删除次数,
if word2[row - 1] == word1[col - 1]: dp[row][col] = dp[row - 1][col - 1] else: dp[row][col] = min(dp[row][col - 1], dp[row - 1][col]) + 1
6 由于这个题是要求子数组(暗含连续),所以只有相等时才计算,且相等时直接用左上角的长度加1,dp[i][j]表示A[:i] B[:j] 两个数组中以A[i] B[j] 结尾的最长的连续子数组的长度,所以每次都计算最大值,这是有dp方程的定义决定的,
7 dp[row][col]表示t[:row-1]在s[:col-1]中的个数,所以当t[row-1]=s[col-1]时,这时dp[row][col] = dp[row][col - 1] + dp[row - 1][col - 1],即左边的加左上的,当不相等时有 dp[row][col] = dp[row][col - 1],即直接等于左边的,注意初始化时,空字符串包含于任意的字符串中,所以第一行为1,
8 逐行遍历中的难题,首先要确定要初始状态,即什么时候空字符可以匹配p,其次写状态转移方程的时候要考虑到 a*和.*的三种情况,即空字符,a,多个a 三种情况,不可漏掉任何一种,
# 自己的代码 两个半小时 # 首先读懂题意,题中的匹配是指两个字符串可以相等,单独的一个‘.'不能表示空字符,只有’*‘前面有’.‘或字符才能构成空字符, # 所以‘*’必须是从第二个开始,它实际上是用作乘, # dp[row][col]表示s[:row]是否可以与p[:col]相等,如果相等为1,否则为0 class Solution: def isMatch(self, s: str, p: str) -> bool: ls = len(s) lp = len(p) dp = [[0] * (lp+1) for _ in range(ls+1)] dp[0][0] = 1 k = [] # 这里必须初始化,dp[0][col]表示空字符串与p[:col]是否匹配, # for i in range(lp): # # 如果当前的‘*’前面只有一个字符,则可以为空,赋值为1, # if p[i] == '*' and len(k) == 1: # k.pop() # dp[0][i+1] = 1 # # 下面这个是多余的,没必要 # # elif p[i] == '.' and len(k) == 0 and (i+1)<lp and p[i+1] == '*': # # dp[0][i+1] = 1 # # k.append(p[i]) # else: # k.append(p[i]) # 初始化简写后 # 如果当前字符为‘*’,则和它的前一个字符必定可以构成空字符串,所以当前状态就等于p[i-2]的状态, for i in range(lp): if p[i] == '*' and dp[0][i-1]: dp[0][i+1] = 1 for row in range(1, ls+1): for col in range(1, lp+1): # 如果当前字符为’.’(必定可以匹配,因为它可以匹配任意字符)或相等,则直接等于左上的值 if p[col-1] == '.' or s[row-1] == p[col-1]: dp[row][col] = dp[row-1][col-1] elif p[col-1] == '*': # 这里实际上是a* 的三种情况, # dp[row][col - 2] a* 为空 # dp[row][col - 1] a* 为单个字符 a # dp[row - 1][col - 1] 或 dp[row-1][col] a* 为多个字符 aa或者更多, if p[col-2] == s[row-1]: dp[row][col] = dp[row-1][col] or dp[row][col-2] or dp[row][col-1] # 如果前一个是点,则它必定可以和s[row-1]进行匹配,所以s[row-1]可有可无,当前的情况是否匹配决定与前面的情况 # dp[row - 1][col]表示的是s[row-2]和p[col-1]是否匹配,如果匹配,则加上s[row-1]也必定匹配,这时的 点* 为当个字符s[row-1] # dp[row][col-2]表示的是s[row-1]和p[col-3]是否匹配,这时的 点* 为空字符, # 这里之所以没有dp[row][col-1]是因为,当dp[row][col-1]为真的时候,dp[row-1][col] or dp[row][col-2]必定为真,所以可以省略, elif p[col-2] == '.': dp[row][col] = dp[row-1][col] or dp[row][col-2] # 最后是两个不等的情况,这个时候要相匹配 a* 只能为空,所以直接等于dp[row][col-2] else: dp[row][col] = dp[row][col-2] # # 上面的三种情况可以合并成两种, # for row in range(1, ls + 1): # for col in range(1, lp + 1): # if p[col - 1] == '.' or s[row - 1] == p[col - 1]: # dp[row][col] = dp[row - 1][col - 1] # elif p[col - 1] == '*': # # 二者相等时有三种情况 # # a*作为: 空字符, 单字符 a, 多字符 aaa... # if p[col - 2] == s[row - 1] or p[col - 2] == '.': # dp[row][col] = dp[row - 1][col] or dp[row][col - 2] or dp[row][col - 1] # else: # dp[row][col] = dp[row][col - 2] print(dp) return bool(dp[-1][-1])
这道题用动态规划不是最优解,和上一题类似,写动态转移方程的时候务必要考虑到 * 号的三种情况,这样才能不漏掉任何一种情况,
沿主对角线斜遍历型
1 与上面的1143题的状态转移方程类似,只不过遍历方式不同,
2 子串是连续的,子序列不连续,这个题的关键是初始状态的确定,主对角线和主对角线下面的是1,因为对于长度是2的字符串也要判断,row+1,col-1后就会到了主对角线下面,或者是先初始化主对角线和长度为2的(即主对角线上的),再进行遍历
# 自己的代码, class Solution: def longestPalindrome(self, s: str) -> str: l = len(s) dp = [[1] * l for _ in range(l)] # for i in range(l): # dp[i][i] = 1 res = [0,0] for col in range(1, l): for row in range(l-col): # 这里最巧妙,开始直接全赋值为1,这里再赋值为0,保证了上三角全是0,下三角全是1 dp[row][col] = 0 if s[row] == s[col] and dp[row+1][col-1]: dp[row][col] = 1 if res[1]-res[0] < col-row: res = [row,col] col += 1 return s[res[0]:res[1]+1]