dp 动态规划篇
路径种数问题
63.不同路径 II
不同路径2
dp[i][j]表示到这个位置的走法,遍历到对应的obstacle位置如果是1,这个点就保持不变是0就行
def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int:
m,n = len(obstacleGrid), len(obstacleGrid[0])
if obstacleGrid[0][0] == 1 or obstacleGrid[m-1][n-1]: return 0
dp = [[0]*n for _ in range(m)]
for j in range(n):
if obstacleGrid[0][j] != 1: dp[0][j] = 1
else: break
for i in range(m):
if obstacleGrid[i][0] != 1: dp[i][0] = 1
else: break
for i in range(1,m):
for j in range(1,n):
if obstacleGrid[i][j] == 1: continue
else: dp[i][j] = dp[i-1][j]+dp[i][j-1]
return dp[m-1][n-1]
拆分问题
343. 整数拆分
整数拆分
写的时候有点把一个整数拉开,去乘的感觉了,但是还是没写出来
怎么拉开去算,还需要一个算子j从1-i, 然后dp[i]就被拆成 j * (i-j), 一个个去试
背过吧还是
def integerBreak(self, n: int) -> int:
dp = [0] * (n+1)
dp[1] = 1
for i in range(2,n+1):
for j in range(1,i+1):
dp[i] = max(dp[i], j*(i-j), j*dp[i-j])
return dp[n]
96.不同的二叉搜索树
不同的二叉搜索树
怎么说呢,二叉搜索树是 如果不空 ,左<根<右
dp[i]就是i个结点的种数
然后用一个j,j -> 1-i分别去盘j当头的情况,也很好盘么,dp[i] += dp[j-1] * dp[i-j]
背过吧
def numTrees(self, n: int) -> int:
dp = [0] * (n+1) #dp[i]表示由i个结点组成的二叉搜索树有多少种
dp[0] = 1
for i in range(1,n+1):
for j in range(1,i+1):
dp[i] += dp[j-1] * dp[i-j] # j 是1-i轮流做头结点,然后左边子树情况*右边子树情况
return dp[n]
背包问题
0-1背包,最多选一个,选or不选
状态压缩之后的代码,遍历顺序固定
# n, m 物品个数, 容积
dp = [0] * (m+1)
for i in range(0, n):
for j in range(m, weight[i]-1, -1):
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
406.分割等和子集
dp[j]的数值一定是小于等于j的。
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
分割等和子集
1049. 最后一块石头的重量 II
最后一块石头的重量 II
解的所在的话,最后我们计算的dp[target], 就是target = sum // 2的容积内,最接近数组之和一半的数,由于我们是向下取整,另一半sum-dp[target], 一定稍微大一点;所以最后的解,sum-dp[target] - dp[tartget]
494.目标和
目标和
要填正负号,必定有整数部分,假设和为x;我们就设法找到填满容积为x的背包有多少种方法就行
def findTargetSumWays(self, nums: List[int], target: int) -> int:
# 正数部分和是x, 负数部分和是sum-x, x-(sum-x) = target
# dp[j]表示填满j容积有多少种方法
if sum(nums) < abs(target) or (sum(nums)+target) % 2 != 0: return 0
n, m = len(nums), (sum(nums)+target) // 2
dp = [0] * (m+1)
dp[0] = 1
for i in range(n):
for j in range(m, nums[i]-1, -1):
dp[j] += dp[j-nums[i]]
return dp[m]
解释一下dp[j]的推导,dp[j] += dp[j-nums[i]]
就是
不考虑nums[i]的情况下,填满容量为j的背包,有dp[j]种方法。
那么考虑nums[i]的话(只要搞到nums[i]),凑成dp[j]就有dp[j - nums[i]] 种方法。
例如:dp[j],j 为5,
已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 dp[5]。
已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 dp[5]。
已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 dp[5]
已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 dp[5]
已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 dp[5]
那么凑成dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
474.一和零
一和零
这个容积有两个维度
dp[i][j]表示有i个0和j个1,最多能选几个
dp[i][j] = max(dp[i][j], dp[i-zeronum][j-onenum]+1) #不选,和 选
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
zeronum, onenum = 0, 0
dp = [[0] * (n+1) for _ in range(m+1)]
for str in strs:
zeronum = str.count('0')
onenum = str.count('1')
for i in range(m, zeronum-1, -1):
for j in range(n, onenum-1, -1):
dp[i][j] = max(dp[i][j], dp[i-zeronum][j-onenum]+1)
return dp[m][n]
背过吧
完全背包,每个物品可以选无限多次
完全背包和01背包不同之处就在于, 完全背包的内层遍历容积也要正序遍历
刚刚在复习01背包的时候,为了不让前面的物品被重复放入,倒序遍历容积;在完全背包可以重复放入,就正序遍历容积就行了
322.零钱兑换(用最少个数物品填满背包)
求怎么装能有的最大价值或者一个方案能最多选多少个物品的话:
dp[j] = max(dp[j], dp[j-nums[i]]+val[i]/1)
如果说,求一个方案最少能用几个物品填满背包的话,
dp[j] = min(dp[j], dp[j-nums[i]]+1)
这道题代码
def coinChange(self, coins: List[int], amount: int) -> int:
dp = [10001] * (amount+1)
dp[0] = 0
for c in coins:
for j in range(c, amount+1):
dp[j] = min(dp[j], dp[j-c]+1)
return dp[amount] if dp[amount] != 10001 else -1
279.完全平方数 (用最少的物品填满背包)
518. 零钱兑换 II (组合的种数)
如果说有多少种选法的话是组合数,dp[j]表示填满容积为j有多少种组合
dp[j] += dp[j-nums[i]]
并且dp[0] = 1表示说什么都不选也是一种
377. 组合总和 Ⅳ (排列的种数)
377. 组合总和 Ⅳ
组合的种数,就是不同顺序算一种
排列的种数,就是不同顺序的算多种
组合的种数,先遍历物品再遍历容积
排列的种数,先遍历容积再遍历物品
因为是完全背包,两者都要正序, 二者递推公式都是一样的
def combinationSum4(self, nums: List[int], target: int) -> int:
n,m = len(nums), target
dp = [0] * (m+1)
dp[0] = 1
for j in range(m+1):
for i in range(n):
if j >= nums[i]: dp[j] += dp[j-nums[i]]
return dp[m]
进阶版的爬楼梯就是这个题的解法
139.单词拆分(能不能由字典里的单词组成这个字符串)
单词拆分
也把它看成一个完全背包,j也有一个容积的意味
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [False] * (len(s)+1)
dp[0] = True
for j in range(1,len(s)+1):
for i in range(j):
if dp[i] and s[i:j] in wordDict:
dp[j] = True
return dp[len(s)]
多重背包
01背包,每种物品一个,选还是不选
完全背包,每种物品无限多个,选或不选
多重背包的话,就是每件物品多了一个件数,我们在遍历的时候还有遍历一下件数
def test_multi_pack1():
'''版本:改变遍历个数'''
weight = [1, 3, 4]
value = [15, 20, 30]
nums = [2, 3, 2]
bag_weight = 10
dp = [0] * (bag_weight + 1)
for i in range(len(weight)):
for j in range(bag_weight, weight[i] - 1, -1):
# 以上是01背包,加上遍历个数
for k in range(1, nums[i] + 1):
if j - k * weight[i] >= 0:
dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i])
print(" ".join(map(str, dp)))
if __name__ == '__main__':
test_multi_pack1()
也可以把ki数组给展开,就是把物品数组给扩大,相应的weight和value数组加进重复的,然后用01背包
打家劫舍
打家劫舍1
打家劫舍
打家劫舍就是隔一个,当前这个不取和取,来一个最大值
def rob(self, nums: List[int]) -> int:
if len(nums) < 2: return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2,len(nums)):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
return dp[len(nums)-1]
打家劫舍 II
打家劫舍2,就是首尾是一个圈,分别把不带首位和不带尾位的数组来做dp,找一个最大的结果
打家劫舍 II
def rob(self, nums: List[int]) -> int:
if len(nums) == 1: return nums[0]
if len(nums) == 2: return max(nums[0], nums[1])
def func(nums):
n = len(nums)
dp = [0]*n
dp[0], dp[1] = nums[0], max(nums[1], nums[0])
for i in range(2,n):
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
return dp[n-1]
return max(func(nums[1:]), func(nums[:-1]))
打家劫舍3
这个就纯建议背过了
打家劫舍3
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rob(self, root: Optional[TreeNode]) -> int:
if not root: return 0
if not root.left and not root.right: return root.val
def dfs(node): #对于每一个结点我们选和不选有两种情况,我们拿一个元组去存(不选的值,选的值)
if not node: return (0,0)
left = dfs(node.left)
right = dfs(node.right)
#不选,选子树最大的
val1 = max(left[0],left[1])+max(right[0],right[1])
#选,本身结点的值加上不选左孩子,不选右孩子
val2 = node.val + left[0] + right[0]
return (val1,val2)
res = dfs(root)
return max(res)
打家劫舍4
股票问题
121.买卖股票的最佳时机
买卖股票的最佳时机
这道题就是找到任意两个数组元素之间的最大的正差,可以维护一个最小左值,res初始化为0,找一个最大的正差
1.维护最小左边界
def maxProfit(self, prices: List[int]) -> int:
low = 10000
res = 0
for i in range(len(prices)):
low = min(low, prices[i])
res = max(res, prices[i]-low)
return res
2.dp
这个题就是只能买卖一次
状态定义dp[i][0]第i天持有,dp[i][1]第i天不持有
def maxProfit(self, prices: List[int]) -> int:
dp = [[0] * 2 for _ in range(len(prices))]
dp[0][0] = -prices[0]
dp[0][1] = 0
for i in range(1,len(prices)):
dp[i][0] = max(dp[i-1][0], -prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i])
return dp[len(prices)-1][1]
122. 买卖股票的最佳时机 II
这个题就是说能反复买卖多次,但是一次最多持有一股
1.就是只收割正利润
def maxProfit(self, prices: List[int]) -> int:
cnt = 0
for i in range(1, len(prices)):
w = prices[i] - prices[i-1]
if w > 0: cnt += w
return cnt
2.dp
这个和上一个只能买卖一次的区别就在于,持有状态的状态转移
def maxProfit(self, prices: List[int]) -> int:
m = len(prices)
dp = [[0] * 2 for _ in range(m)]
dp[0][0], dp[0][1] = -prices[0], 0
for i in range(1, m):
dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]) #持有,上个时态就持有,上个时态不持有现在买
dp[i][1] = max(dp[i-1][1], dp[i-1][0]+prices[i])
return dp[m-1][1]
- 买卖股票的最佳时机含手续费
买卖股票的最佳时机含手续费
和买卖股票2 是一样的,就是持有的时候不止要支付price[i]还要支付手续费
def maxProfit(self, prices: List[int], fee: int) -> int:
dp = [[0] * 2 for _ in range(len(prices))]
dp[0][0] = -prices[0] - fee
for i in range(1, len(prices)):
dp[i][0] = max(dp[i-1][0], dp[i-1][1]-prices[i]-fee) #持有
dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) #不持有
return dp[-1][1]
123. 买卖股票的最佳时机 III
123. 买卖股票的最佳时机 III
最多可以买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖
所以说这个题每天有五个状态,
0 —— 没有操作
1 —— 第一次买入
2 —— 第一次卖出
3 —— 第二次买入
4 —— 第二次卖出
状态转移,要么是上一个状态就这样转移过来的,要么是做了买卖对应上一个状态转移
dp[i][0] = dp[i - 1][0]
dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i])
dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i])
dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i])
dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i])
def maxProfit(self, prices: List[int]) -> int:
m = len(prices)
dp = [[0] * 5 for _ in range(m)]
dp[0][1], dp[0][3] = -prices[0], -prices[0] #初始化第一次买和第二次买
for i in range(1,m):
dp[i][0] = dp[i-1][0] #不做操作
dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]) #第一次买
dp[i][2] = max(dp[i-1][2], dp[i-1][1]+prices[i]) #第一次卖
dp[i][3] = max(dp[i-1][3], dp[i-1][2]-prices[i]) #第二次买
dp[i][4] = max(dp[i-1][4], dp[i-1][3]+prices[i]) #第二次卖
return dp[m-1][4]
188. 买卖股票的最佳时机 IV
买卖股票的最佳时机 IV
最多可以买卖k次
上一题抽象成规律,奇数买,偶数卖
def maxProfit(self, k: int, prices: List[int]) -> int:
m = len(prices)
if m == 0: return 0
dp = [[0]*(2*k+1) for _ in range(m)]
for j in range(1, 2*k, 2):
dp[0][j] = -prices[0]
for i in range(1,m):
for j in range(0, 2*k-1, 2):
dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j]-prices[i]) #奇数买
dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1]+prices[i]) #偶数卖
return dp[-1][2*k]
309.最佳买卖股票时机含冷冻期
最佳买卖股票时机含冷冻期
可以多次买卖,但是卖了之后,下一天不能买卖,进入冷冻期
这个也是挺难的,有四个状态
持有:
状态1:之前就买了之后没有操作,和今天买
不持有:
状态2:两天前就卖了,昨天是冷冻期,今天保持不持有
状态3:今天卖出了股票
今天是冷冻期:
状态4:今天是冷冻期那么昨天一定是卖出
状态转移
dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); #持有
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); #不持有中的2
dp[i][2] = dp[i - 1][0] + prices[i]; #不持有中的3,今天卖掉
dp[i][3] = dp[i - 1][2]; #今天是冷冻期
代码
def maxProfit(self, prices: List[int]) -> int:
dp = [[0] * 4 for _ in range(len(prices))]
dp[0][0] = -prices[0]
for i in range(1, len(prices)):
dp[i][0] = max(dp[i-1][0], max(dp[i-1][3], dp[i-1][1])-prices[i])#持有股票 分为前一天就持有股票, 今天买(又分为前一天是冷冻期和保持不持有股票状态)
dp[i][1] = max(dp[i-1][1], dp[i-1][3])#不持有股票状态中的保持卖出的状态,分为前一天就卖出,和前一天是冷冻期
dp[i][2] = dp[i-1][0] + prices[i] #今天就卖出,前一天肯定是持有
dp[i][3] = dp[i-1][2] #冷冻期,前一天肯定是卖出
return max(dp[-1][3], max(dp[-1][2], dp[-1][1]))
线性dp、区间dp
线性dp
300. 最长递增子序列
最长递增子序列
找连续递增子序列
def lengthOfLIS(self, nums: List[int]) -> int:
n = len(nums)
dp = [1] * n
res = 1
for i in range(1,n):
for j in range(i):
if nums[i] > nums[j]: dp[i] = max(dp[i], dp[j]+1)
res = max(res, dp[i])
return res
674. 最长递增子串
最长连续递增序列找连续递增子串
def findLengthOfLCIS(self, nums: List[int]) -> int:
n = len(nums)
dp = [1] * n
for i in range(1,n):
if nums[i] > nums[i-1]: dp[i] = dp[i-1] + 1
return max(dp)
1143.最长公共子序列
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1, n+1):
if text1[i-1] == text2[j-1]: dp[i][j] = dp[i-1][j-1] + 1
else: dp[i][j] = max(dp[i-1][j], dp[i][j-1])
return dp[-1][-1]
718.最长公共子串
def findLength(self, nums1: List[int], nums2: List[int]) -> int:
m,n = len(nums1), len(nums2)
dp = [[0]*(n+1) for _ in range(m+1)]
res = 0
for i in range(1,m+1):
for j in range(1,n+1):
if nums1[i-1] == nums2[j-1]: dp[i][j] = dp[i-1][j-1]+1
res = max(res, dp[i][j])
return res
总结,子串的状态转移来源只可能有一种,子序列的话要多
53.最大子数组和
最大子数组和
贪心
维护一个cnt, 当cnt<0就给之前累加的和清零,然后再从下个开始累加
def maxSubArray(self, nums: List[int]) -> int:
cnt = 0
res = nums[0]
for i in range(len(nums)):
cnt += nums[i]
if cnt > res: res = cnt
if cnt < 0: cnt = 0
return res
dp
def maxSubArray(self, nums: List[int]) -> int:
dp = [0] * len(nums)
dp[0] = nums[0]
res = nums[0]
for i in range(1,len(nums)):
dp[i] = max(dp[i-1]+nums[i], nums[i])
res = max(res, dp[i])
return res
编辑距离
392.判断子序列
判断子序列
dp[i][j]定义为以下标i-1, j-1为结尾的字符串S 和 字符串T 的相同子序列的长度
if (s[i - 1] == t[j - 1]),
那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1
if (s[i - 1] != t[j - 1]),
此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1];
def isSubsequence(self, s: str, t: str) -> bool:
m, n = len(s), len(t)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(1,m+1):
for j in range(1,n+1):
if s[i-1] == t[j-1]: dp[i][j] = dp[i-1][j-1] + 1
else:
dp[i][j] = dp[i][j-1]
return dp[m][n] == len(s)
115.不同的子序列
不同的子序列
dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
2.状态转移
这一类问题,基本是要分析两种情况
s[i - 1] 与 t[j - 1]相等
s[i - 1] 与 t[j - 1] 不相等
1) 当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成:
一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。
一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。
当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。
2) 当s[i - 1] 与 t[j - 1]不相等时
dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]
3.base case
从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][0] 和dp[0][j]是一定要初始化的
dp[0][j]一定都是0,s如论如何也变成不了t
dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。
那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。
dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
4.遍历顺序和解的位置
正序就行,解在dp[m][n]
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = 1
for j in range(1, n+1):
dp[0][j] = 0
for i in range(1, m+1):
for j in range(1, n+1):
if s[i-1] == t[j-1]: dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
else: dp[i][j] = dp[i-1][j]
return dp[m][n]
583.两个字符串的删除操作
这个题就是编辑距离,可以增 可以删 的情况 少了一种替换的情况
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
for i in range(1,m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1]
else: dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + 1
return dp[m][n]
72.编辑距离
编辑距离
word1[i-1] == word2[j-1],相等直接转化
word1[i-1] != word2[j-1], 增删改,操作+1
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
dp = [[0] * (n+1) for _ in range(m+1)]
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
for i in range(1,m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]: dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
return dp[m][n]
区间dp
647.回文子串数目
回文子串
1.状态定义
布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。
2.状态转移
整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。
当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。
3.base case
这道题开的数组是高宽都是n的,没有特殊的base case
初始化全为False就行
4.遍历顺序以及解的位置
由dp[i+1][j-1], 遍历顺序i是反序的,j是正序的
解的位置就是计数器的值
class Solution:
def countSubstrings(self, s: str) -> int:
n = len(s)
dp = [[False] * n for _ in range(n)]
cnt = 0
for i in range(n-1, -1, -1):
for j in range(i, n):
if s[i] != s[j]: dp[i][j] = False
else:
if j - i <= 1:
dp[i][j] = True
cnt += 1
elif dp[i+1][j-1]:
dp[i][j] = True
cnt += 1
return cnt
516.最长回文子序列
这道题和上面的不同在于这道题是子序列,不需要连续
第二个区别是求的最长的,我们可以把dp数组的状态定义为长度
1.状态定义
dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
这道题代码里我开的还是n行n列的数组,正好映射到s的下标那种
2.状态转移
在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同
如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;
但是这题来说,j可不是从i开始,子串那题,我们j是从i开始,是因为有 a 这种单个字母需要被判回文子串的情况,而这个需要统计的是长度;我们j从i+1开始遍历
如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。
加入s[j]的回文子序列长度为dp[i + 1][j]。
加入s[i]的回文子序列长度为dp[i][j - 1]。
那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
3.base case
由于上面我说的i j不可能重合,所以这题我们需要初始化dp[i][i] = 1, 根据定义单个的就是1, 当然了如果我们在代码里特判一下也行,那样的话j就可以从i开始遍历
其余的我们就初始化为0就行
4.遍历顺序以及解的位置
遍历顺序我们i倒序, j从i+1开始,正序遍历
解的位置就是dp[0][n], 从头到尾序列中最长回文序列是多长
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n-1, -1, -1):
for j in range(i+1, n):
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][n-1]