线性DP
例题1
1143. 最长公共子序列 (LCS)
子序列不连续
给定两个字符串 text1
和 text2
,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0
。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
- 例如,
"ace"
是"abcde"
的子序列,但"aec"
不是"abcde"
的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
示例 1:
输入:text1 = "abcde", text2 = "ace" 输出:3 解释:最长公共子序列是 "ace" ,它的长度为 3 。
本质是选或不选
dfs(i,j)表示s的前i个字母和t的前j个字母的LCS长度
当s[i]=t[j]的时候,dfs(i-1,j-1)+1是要大于前面两个的,舍弃前两个
s[i]!=t[j]的时候,前面两个会自动递归到 dfs(i-1,j-1)+1,因此舍弃
代码:
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
x=len(text1)
y=len(text2)
@cache
def dfs(n,m):
if n<0 or m<0:
return 0
if text1[n]==text2[m]:
return (dfs(n-1,m-1)+1)
if text1[n]!=text2[m]:
return max(dfs(n-1,m),dfs(n,m-1))
return dfs(x-1,y-1)
改成数组:注意避免负数下标,这里都往后移一位
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
x1=len(text1)
y1=len(text2)
f=[[0]*(y1+1) for _ in range(x1+1)]
for i,x in enumerate(text1):
for j,y in enumerate(text2):
if x==y:
f[i+1][j+1]=f[i][j]+1
else:
f[i+1][j+1]=max(f[i][j+1],f[i+1][j])
return f[x1][y1]
给你两个单词 word1
和 word2
, 请返回将 word1
转换成 word2
所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
示例 1:
输入:word1 = "horse", word2 = "ros" 输出:3 解释: horse -> rorse (将 'h' 替换为 'r') rorse -> rose (删除 'r') rose -> ros (删除 'e')
思路:从末尾值开始比较,如果s[i]=t[j] 直接dfs(i-1.j-1),因为不能改变这个值已经想同了
删除一个字母:相当于去掉s[i],也就是dfs(i,j-1)
插入一个字母,插入的肯定是和t[j]是一样的,所以是dfs[i+1,j],因为此时s[i]=t[j] ,所以同时减去1,得到最终答案dfs(i.j-1)
替换:把s【i】替换成t【j】,在同时减去1,也就是dfs(i-1,j-1)+1
代码:
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
n=len(word1)
m=len(word2)
@cache
def dfs(i,j):
if i<0:
return j+1
if j<0:
return i+1
if word1[i]==word2[j]:
return dfs(i-1,j-1)
return min(dfs(i-1,j),dfs(i,j-1),dfs(i-1,j-1))+1
return dfs(n-1,m-1)
数组:
class Solution:
def minDistance(self, s: str, t: str) -> int:
n, m = len(s), len(t)
f = [[0] * (m + 1) for _ in range(n + 1)]
f[0] = list(range(m + 1))
for i, x in enumerate(s):
f[i + 1][0] = i + 1
for j, y in enumerate(t):
f[i + 1][j + 1] = f[i][j] if x == y else \
min(f[i][j + 1], f[i + 1][j], f[i][j]) + 1
return f[n][m]
#f[0] = list(range(m + 1))表示初始化第一行,即s为空字符串时与t的编辑距离。因为此时s为空,所以只需要插入操作即可将t转换为s,所以第一行从0开始递增。
f[i + 1][0] = i + 1表示初始化第一列,即t为空字符串时与s的编辑距离。因为此时t为空,所以只需要删除操作即可将s转换为t,所以第一列从0开始递增。
给你一个整数数组 nums
,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7]
是数组 [0,3,1,6,2,2,7]
的
子序列
。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18] 输出:4 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
递归代码:
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n=len(nums)
@cache
def dfs(i):
res=0
for x in range(i):
if nums[x]<nums[i]:
res= max(res,dfs(x))
return res+1
ans=0
return max(dfs(i) for i in range(n))
转化成数组
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
n=len(nums)
f=[0]*n
for i in range(n):
for j in range(i):
if nums[j]<nums[i]:
f[i]=max(f[j],f[i])
f[i]+=1
return max(f)
状态机DPww
给你一个整数数组 prices
,其中 prices[i]
表示某支股票第 i
天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3。 最大总利润为 4 + 3 = 7
从表示状态之间转换关系的图叫状态机
因为第i-1天结束也就是i天开始,所以dfs(i-1,-)也表示第i天开始时的最大利润
递归边界: dfs(-1,0)=0 第0天开始未持有股票,利润为0,dfs(-1,1)=-inf,第0天开始不可能有股票
递归入口:dfs(n-1,0)
递归+记忆化搜索
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n=len(prices)
@cache
def dfs(n,hold):
if n<0:
return -inf if hold else 0
if hold:
return max(dfs(n-1,True),dfs(n-1,False)-prices[n])
return max(dfs(n-1,False),dfs(n-1,True)+prices[n])
return dfs(n-1,False)
数组+递推
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n=len(prices)
f=[[0]*2 for _ in range(n+1)]
f[0][1]=-inf
f[0][0]=0
for i,p in enumerate(prices):
f[i+1][0]=max(f[i][0],f[i][1]+p)
f[i+1][1]=max(f[i][1],f[i][0]-p)
return f[n][0]
变型:
给定一个整数数组prices
,其中第 prices[i]
表示第 i
天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
- 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2] 输出: 3 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
首先在没有冷冻期的情况下,dp【i】【1】是从dp【i-1】【0】和dp【i-1】【1】转移过来的,然后有冷冻期时,仍然可以从dp【i-1】【1】转移过来,至于dp【i-1】【0】,我们只需要考虑第i-2天也没有持有的情况,而如果连续两天没有持有,等价于这两天没有任何操作,所以dp【i-1】【0】和dp【i-2】【0】是一样的。所以dp【i-2】【0】就是这种情况,直接从dp【i-2】【0】转移过来即可,只有这一种情况可以打通dp【i-2】【0】到dp【i-1】【0】再到dp【i】【1】这条道路,所以dp【i-2】【0】和dp【i-1】【1】已经包含了所有合法的情况。
变型2
class Solution:
def maxProfit(self, prices: List[int]) -> int:
n=len(prices)
@cache
def dfs(i,hold):
if i<0:return -inf if hold else 0
if hold :
return max(dfs(i-1,True),dfs(i-2,False)-prices[i])
return max(dfs(i-1,False),dfs(i-1,True)+prices[i])
return dfs(n-1,False)
买+卖=一次交易
给你一个整数数组 prices
和一个整数 k
,其中 prices[i]
是某支给定的股票在第 i
天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k
笔交易。也就是说,你最多可以买 k
次,卖 k
次。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入:k = 2, prices = [2,4,1] 输出:2 解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
j<0 返回-inf dfs(-1,j,0)=0 dfs(-1,j,1)=-inf
递归入口: dfs(n-1,k,0)
递归
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n=len(prices)
@cache
def dfs(i,j,hold):
if j<0:
return -inf
if i<0:return -inf if hold else 0
if hold :
return max(dfs(i-1,j,True),dfs(i-1,j,False)-prices[i])
return max(dfs(i-1,j,False),dfs(i-1,j-1,True)+prices[i])
return dfs(n-1,k,False)
递推
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
f = [[[-inf] * 2 for _ in range(k + 2)] for _ in range(n + 1)]
for j in range(1, k + 2):
f[0][j][0] = 0
for i, p in enumerate(prices):
for j in range(1, k + 2):
f[i + 1][j][0] = max(f[i][j][0], f[i][j][1] + p)
f[i + 1][j][1] = max(f[i][j][1], f[i][j - 1][0] - p)
return f[n][k+1][0]
递归中 j 是从 -1 到 k,所以有 k+2 个不同的 j,翻译的时候这个维度的长度就是 k+2 了
k+1 是因为需要在前面插入一个状态表示 j=-1 的情况,所以要把原来的 i 改成 i+1,k 改成 k+1。
区间DP
从小区间转移到大区间
给你一个字符串 s
,找出其中最长的回文子序列,并返回该序列的长度。
子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。
示例 1:
输入:s = "bbbab" 输出:4 解释:一个可能的最长回文子序列为 "bbbb" 。
示例 2:
输入:s = "cbbd" 输出:2 解释:一个可能的最长回文子序列为 "bb" 。
思路1:求s和s反转后的LCS
思路2: 从两侧向内缩小问题规模(选还是不选)
边界: dfs(i,i)=1,本身就是一个回文子序列
dfs(i+1,i) 没有字母,返回0
入口:dfs(0,n-1)
代码:
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n=len(s)
@cache
def dfs(i,j):
if i>j:
return 0
if i==j:
return 1
if s[i]==s[j]:
return dfs(i+1,j-1)+2
return max(dfs(i+1,j),dfs(i,j-1))
return dfs(0,n-1)
递推:
class Solution:
def longestPalindromeSubseq(self, s: str) -> int:
n = len(s)
f = [[0] * n for _ in range(n)]
for i in range(n - 1, -1, -1):
f[i][i] = 1
for j in range(i + 1, n):
if s[i] == s[j]:
f[i][j] = f[i + 1][j - 1] + 2
else:
f[i][j] = max(f[i + 1][j], f[i][j - 1])
return f[0][n-1]
你有一个凸的 n
边形,其每个顶点都有一个整数值。给定一个整数数组 values
,其中 values[i]
是第 i
个顶点的值(即 顺时针顺序 )。
假设将多边形 剖分 为 n - 2
个三角形。对于每个三角形,该三角形的值是顶点标记的乘积,三角剖分的分数是进行三角剖分后所有 n - 2
个三角形的值之和。
返回 多边形进行三角剖分后可以得到的最低分 。
示例 1:
输入:values = [1,2,3] 输出:6 解释:多边形已经三角化,唯一三角形的分数为 6。
示例 2:
输入:values = [3,7,4,5] 输出:144 解释:有两种三角剖分,可能得分分别为:3*7*5 + 4*5*7 = 245,或 3*4*5 + 3*4*7 = 144。最低分数为 144。
代码:
class Solution:
def minScoreTriangulation(self, values: List[int]) -> int:
n=len(values)
@cache
def dfs(i,j):
if i+1==j:
return 0
res=inf
for k in range(i+1,j):
res=min(res,dfs(i,k)+dfs(k,j)+values[i]*values[k]*values[j])
return res
return dfs(0,n-1)
代码:
树形DP
给你一棵二叉树的根节点,返回该树的 直径 。
二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root
。
两节点之间路径的 长度 由它们之间边数表示。
示例 1:
输入:root = [1,2,3,4,5] 输出:3 解释:3 ,取路径 [4,2,1,3] 或 [5,2,1,3] 的长度。
示例 2:
输入:root = [1,2] 输出:1
# 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 diameterOfBinaryTree(self, root: Optional[TreeNode]) -> int:
ans=0
def dfs(node):
if node is None:
return -1
left=dfs(node.left)
right=dfs(node.right)
nonlocal ans
ans=max(ans,left+right+2)
return max(left,right)+1
dfs(root)
return ans
二叉树中的 路径 被定义为一条节点序列,序列中每对相邻节点之间都存在一条边。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root
,返回其 最大路径和 。
示例 1:
输入:root = [1,2,3] 输出:6 解释:最优路径是 2 -> 1 -> 3 ,路径和为 2 + 1 + 3 = 6
示例 2:
输入:root = [-10,9,20,null,null,15,7] 输出:42 解释:最优路径是 15 -> 20 -> 7 ,路径和为 15 + 20 + 7 = 42
# 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 maxPathSum(self, root: Optional[TreeNode]) -> int:
ans=-inf
def dfs(node):
if node is None:
return 0
l_val=dfs(node.left)
r_val=dfs(node.right)
nonlocal ans
ans=max(ans,l_val+r_val+node.val)
return max(max(l_val,r_val)+node.val,0)
dfs(root)
return ans
为什么要和0比最大值,因为可能算出来的是负数,比如一个树是1,-1,2,那么答案肯定是1,2.但是这里算出来的左子树max是-1,因此要和0比较,小于0等于不选
给你一棵 树(即一个连通、无向、无环图),根节点是节点 0
,这棵树由编号从 0
到 n - 1
的 n
个节点组成。用下标从 0 开始、长度为 n
的数组 parent
来表示这棵树,其中 parent[i]
是节点 i
的父节点,由于节点 0
是根节点,所以 parent[0] == -1
。
另给你一个字符串 s
,长度也是 n
,其中 s[i]
表示分配给节点 i
的字符。
请你找出路径上任意一对相邻节点都没有分配到相同字符的 最长路径 ,并返回该路径的长度。
示例 1:
输入:parent = [-1,0,0,1,1,2], s = "abacbe" 输出:3 解释:任意一对相邻节点字符都不同的最长路径是:0 -> 1 -> 3 。该路径的长度是 3 ,所以返回 3 。 可以证明不存在满足上述条件且比 3 更长的路径。
class Solution:
def longestPath(self, parent: List[int], s: str) -> int:
n=len(parent)
g=[[]for _ in range(n)]
for i in range (1,n):
g[parent[i]].append(i)
ans=0
def dfs(x):
nonlocal ans
x_len=0
for y in g[x]:
y_len=dfs(y)+1
if s[y]!=s[x]:
ans=max(ans,x_len+y_len)
x_len=max(x_len,y_len)
return x_len
dfs(0)
return ans+1
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root
。
除了 root
之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root
。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1] 输出: 7 解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
示例 2:
输入: root = [3,4,5,1,3,null,1] 输出: 9 解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9
最大独立集需要从图中选择尽量多的点,他们互不相邻
变型:最大化点权之和
树和子树关系类似于原问题和子问题,如何由子问题算出原问题
# 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:
def dfs(node):
if node is None:
return 0,0
l_rob,l_not_rob=dfs(node.left)
r_rob,r_not_rob=dfs(node.right)
rob=l_not_rob+r_not_rob+node.val
not_rob=max(l_not_rob,l_rob)+max(r_rob,r_not_rob)
return rob,not_rob
return max(dfs(root))
P1352 没有上司的舞会 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
输入
7 1 1 1 1 1 1 1 1 3 2 3 6 4 7 4 4 5 3 5
输出 5
代码部分(c++)
给定一个二叉树,我们在树的节点上安装摄像头。
节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。
计算监控树的所有节点所需的最小摄像头数量
示例 1
输入:[0,0,null,0,null,0,null,null,0] 输出:2 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
当前节点是蓝色,表示选择安装谁摄像头
当前节点是黄色,表示选择不安装,且它的父节点安装摄像头
当前节点是红色,表示选择不安装,且它的至少一个儿子安装摄像头
对于蓝色节点,他的儿子装不装摄像头都可以
对于黄色节点,他的儿子不能是黄色,因为黄色节点的父节点必须要安装摄像头
对于红色节点,他的儿子不可能是黄色,因为它本身没有安装摄像头
因此
最终代码:
# 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 minCameraCover(self, root: Optional[TreeNode]) -> int:
def dfs(node):
if node is None:
return inf,0,0
l_choose,l_fa,l_child=dfs(node.left)
r_choose,r_fa,r_child=dfs(node.right)
choose=min(l_choose,l_fa,l_child)+min(r_choose,r_fa,r_child)+1
fa=min(l_choose,l_child)+min(r_choose,r_child)
child=min(l_choose+r_choose,l_choose+r_child,l_child+r_choose)
return choose,fa,child
choose,_,child=dfs(root)
return min(choose,child)
变型1:在节点x安装摄像头需要花费cost【x】,那么只需要把+1改成加cost【node】
变型2 优化红色:如果子节点很多没那么红色要罗列的情况就很多,如果3个子节点,那就要考虑2的3次方-1次情况
因为我们优化成
当红色都比蓝色小的时候,都选择红色,所以要添加一个max(0,min(蓝1-红1,蓝2-红2,蓝3-红3),也就是补上差值,让其中一个红色变成蓝色
课后作业:洛谷保安站岗