基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- GitHub
视频:灵茶山艾府的个人空间-灵茶山艾府个人主页-哔哩哔哩视频
力扣最全 DP 题单:分享丨【算法题单】动态规划(入门/背包/划分/状态机/区间/状压/数位/树形/优化) - 讨论 - 力扣(LeetCode)
21 状态机dp

课程讲解
不限交易次数
122. 买卖股票的最佳时机 II


这种表示状态之间转换关系的图叫【状态机】

定义状态和状态转移方程:



dfs(n-1, 1) 一定小于 dfs(n-1, 0)
python
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, 1), dfs(i-1, 0) - prices[i])
else:
return max(dfs(i-1, 0), dfs(i-1, 1) + prices[i])
return dfs(n-1, 0)
- 时间复杂度:状态个数 O(n) * 单个状态的计算时间 O(1)
- 空间复杂度:O(n)

python
class Solution:
def maxProfit(self, prices: list[int]) -> int:
n = len(prices)
f = [[0] * 2 for _ in range(n+1)]
f[0][1] = -inf
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]
空间优化,滚动计算:
python
class Solution:
def maxProfit(self, prices: list[int]) -> int:
n = len(prices)
f0 = 0
f1 = -inf
for p in prices:
new_f0 = max(f0, f1 + p) # 一定要用一个临时变量先存储起来
f1 = max(f1, f0 - p)
f0 = new_f0
return f0
- 空间复杂度 O(1)
309. 买卖股票的最佳时机含冷冻期
卖出后不能立刻买入股票。换句话说,在买入股票的时候,前一天不能有卖出股票的操作
python
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, 1), dfs(i-2, 0) - prices[i]) # 把i-1改成i-2即可
else:
return max(dfs(i-1, 0), dfs(i-1, 1) + prices[i])
return dfs(n-1, 0)
限制至多交易k次
188. 买卖股票的最佳时机 IV

既然有次数限制,就应当在递归的过程中去记录次数。因此在无限次交易的基础上增加一个参数 j,表示至多完成 j 笔交易

python
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, 1), dfs(i-1, j, 0) - prices[i])
else:
return max(dfs(i-1, j, 0), dfs(i-1, j-1, 1) + prices[i]) # j-1表示增加一次交易次数
return dfs(n-1, k, 0)
- 时空间复杂度都多了一个 k

python
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][1] + p)
f[i+1][j][1] = max(f[i][j][1], f[i][j][0] - p)
return f[n][k+1][0]
空间优化,优化到 O(k):
- fi+1 只用到 fi,所以这一维可以去掉
- 由于 fi+1j 需要从 fij-1 转移过来,j 改成倒序遍历
- 由于 fj1 会用到 fj0 的结果,而反过来是不需要的,所以可以先计算 fj1
python
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
n = len(prices)
f = [[-inf] * 2 for _ in range(k+2)]
# 初始化
for j in range(1, k+2):
f[j][0] = 0
for i, p in enumerate(prices):
for j in range(k+1, 0, -1):
f[j][1] = max(f[j][1], f[j][0] - p)
f[j][0] = max(f[j][0], f[j-1][1] + p)
return f[k+1][0]

恰好:递归到 i<0 时,只有 j=0 才是合法的,j>0 是不合法的
python
# 恰好
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)]
f[0][1][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[-1][-1][0]
# 记忆化搜索
# @cache
# def dfs(i: int, j: int, hold: bool) -> int:
# if j < 0:
# return -inf
# if i < 0:
# return -inf if hold or j > 0 else 0
# if hold:
# return max(dfs(i - 1, j, True), dfs(i - 1, j - 1, False) - prices[i])
# return max(dfs(i - 1, j, False), dfs(i - 1, j, True) + prices[i])
# return dfs(n - 1, k, False)
至少:递归到「至少 0 次」时,它等价于「交易次数没有限制」,那么这个状态的计算方式和 122. 买卖股票的最佳时机 II 是一样的
python
# 至少
class Solution:
def maxProfit(self, k: int, prices: List[int]) -> int:
# 递推
n = len(prices)
f = [[[-inf] * 2 for _ in range(k + 1)] for _ in range(n + 1)]
f[0][0][0] = 0
for i, p in enumerate(prices):
f[i + 1][0][0] = max(f[i][0][0], f[i][0][1] + p)
f[i + 1][0][1] = max(f[i][0][1], f[i][0][0] - p) # 无限次
for j in range(1, k + 1):
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[-1][-1][0]
# 记忆化搜索
# @cache
# def dfs(i: int, j: int, hold: bool) -> int:
# if i < 0:
# return -inf if hold or j > 0 else 0
# if hold:
# return max(dfs(i - 1, j, True), dfs(i - 1, j - 1, False) - prices[i])
# return max(dfs(i - 1, j, False), dfs(i - 1, j, True) + prices[i])
# return dfs(n - 1, k, False)
作者:灵茶山艾府
来源:力扣(LeetCode)
课后作业
22 区间dp

课程讲解
516. 最长回文子序列

python
class Solution:
# 最长公共子序列的代码
def longestCommonSubsequence(self, text1, text2):
n = len(text1)
m = len(text2)
f = [[0] * (m+1) for _ in range(n+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[n][m]
def longestPalindromeSubseq(self, s: str) -> int:
return self.longestCommonSubsequence(s, s[::-1])



python
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)
- 时间复杂度:状态有 O(n^2) 个,每个状态只需要 O(1) 的时间计算
- 空间复杂度:状态个数 O(n^2)

python
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]
1039. 多边形三角剖分的最低得分
把一个 n 边形剖分成 n-2 个三角形

整个多边形的分数就是所有三角形的分数之和
从其中的一条边开始思考:



递归边界就是只有两个点的情况,此时没有三角形,返回0
python
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[j] * values[k])
return res
return dfs(0, n-1)
- 时间复杂度:状态有 O(n^2) 个,每个状态需要 O(n) 的时间来计算
- 空间复杂度:状态个数

python
class Solution:
def minScoreTriangulation(self, values: list[int]) -> int:
n = len(values)
f = [[0] * n for _ in range(n)]
# j至少要从i+2开始,所以i要从n-3开始倒序循环
for i in range(n-3, -1, -1):
for j in range(i+2, n):
res = inf
for k in range(i+1, j):
res = min(res, f[i][k] + f[k][j] + values[i] * values[j] * values[k])
f[i][j] = res
return f[0][n-1]
课后作业
23 树形dp(上)树的直径

课程讲解
104. 二叉树的最大深度

python
# 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 maxDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
left_depth = self.maxDepth(root.left)
right_depth = self.maxDepth(root.right)
return max(left_depth, right_depth) + 1
543. 二叉树的直径
直径:在这条树上找到一条最长的路径,这里的路径长度定义为边的数目。那么这条路径的端点(即起点和终点)一定在叶子上


子树最长链的计算方式和二叉树的最大深度是类似的
python
# 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
l_len = dfs(node.left)
r_len = dfs(node.right)
nonlocal ans
ans = max(ans, l_len + r_len + 2)
return max(l_len, r_len) + 1
dfs(root)
return ans
- 时间复杂度:O(n),每个点都遍历一次
- 空间复杂度:O(n),最坏情况下这棵二叉树是一条链,递归需要 O(n) 的栈空间
124. 二叉树中的最大路径和
注意节点值有负数


负数返回0表示不选
python
# 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: TreeNode | None) -> 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) # 如果是负数就不选,返回0
dfs(root)
return ans
- 时间复杂度:O(n)
- 空间复杂度:O(n)
2246. 相邻字符不同的最长路径(一般树的直径)
引入一个邻居的概念,如果两个点 x y 之间有边相连,那么可以说x是y的邻居,或y是x的邻居
在二叉树中一个节点至多有三个邻居(左儿子、右儿子、父节点),而在一般树中,邻居的个数就不定了,需要用for循环去挨个遍历它的邻居
求直径:


如果没有相邻节点的限制,那么本题求的就是树的直径上的点的个数,见 1245. 树的直径
python
class Solution:
def longestPath(self, parent: List[int], s: str) -> int:
# 边是以parent数组的形式给出的,即从parent[i]到i有一条边
n = len(parent)
g = [[] for _ in range(n)]
for i in range(1, n): # 由于0是根节点,这条边不存在,所以从1开始遍历
g[parent[i]].append(i) # g[i]存储着第i个节点的邻居(不包含父节点,因为只有从parent[i]到i的边)
ans = 0
def dfs(x):
nonlocal ans
x_len = 0 # x的最长链长初始化为0
for y in g[x]: # 遍历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) # 0是根节点
return ans + 1 # 路径上点的数量 = 边的数量 + 1
- 时间复杂度:O(n)
- 空间复杂度:O(n)
如果x的邻居包含父节点,可以这样写:
python
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, fa): # fa表示x的父节点
nonlocal ans
x_len = 0
for y in g[x]:
if y == fa:
continue
y_len = dfs(y, x) + 1 # x是y的父节点
if s[y] != s[x]:
ans = max(ans, x_len + y_len)
x_len = max(x_len, y_len)
return x_len
dfs(0, -1) # 0是根节点,没有父节点,因此传入-1
return ans + 1
课后作业
24 树形dp(中)树上最大独立集
课程讲解
337. 打家劫舍 III

选/不选时 这棵子树选的数的和最大是多少

python
# 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: TreeNode | None) -> 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_rob, l_not_rob) + max(r_rob, r_not_rob)
return rob, not_rob
return max(dfs(root))
- 时间复杂度:每个节点只递归一次,O(n)
- 空间复杂度:在最坏情况下这棵二叉树是一条链,递归需要 O(n) 的栈空间

总结
树是图的一种特殊情况
如果点权都是1,那么算出来的就是最大独立集了

课后作业
25 树形dp(下)树上最小支配集
课程讲解
968. 监控二叉树
在二叉树的一些节点上安装摄像头,每个摄像头可以监控它自己所在的节点以及相邻的节点。至少要安装多少个摄像头才能把所有节点都覆盖到呢?


ps:如果一个节点的父节点和儿子节点都安装了摄像头,那它既可以是黄色节点,也可以是红色节点
分类讨论:

- 蓝色:根节点已经装了摄像头,那么儿子装不装摄像头都可以(最后的+1表示这棵子树的根节点装了摄像头)

- 黄色:

- 红色:


python
# 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_by_fa, l_by_childen = dfs(node.left) # 递归左子树返回的三个值
r_choose, r_by_fa, r_by_childen = dfs(node.right)
# 该节点安装摄像头
choose = min(l_choose, l_by_fa, l_by_childen) + min(r_choose, r_by_fa, r_by_childen) + 1
# 该节点没装,其父节点监控到
by_fa = min(l_choose, l_by_childen) + min(r_choose, r_by_childen)
# 该节点没装,其儿子节点监控到
by_children = min(l_choose+r_by_childen, l_by_childen+r_choose, l_choose+r_choose)
return choose, by_fa, by_children
# 根节点没有父节点,所以不用关心by_fa
choose, _, by_children = dfs(root)
return min(choose, by_children)
- 时间复杂度:递归这棵二叉树,每个节点都递归一次。O(n)
- 空间复杂度:最坏情况下,这棵二叉树是一条链,递归需要 O(n) 的栈空间
变形1:花费为cost

- 968题(LeetCode原题)相当于每个节点的花费都是1
- 只需要把代码里的 1 改成对应花费 costnode 就好了

变形2:一般树

扩展到一般树的情况,每个儿子蓝色or红色,总共有 2^n(n个儿子)-1(减去都是红色的情况)

红色节点要至少有一个蓝色的儿子,如果去掉这个约束,就只需要计算每个儿子是蓝色更小还是红色更小
红色都比蓝色小的情况,算出来不一样。如果要保证至少要选一个蓝色儿子,可以把其中一个红色改成蓝色,改一个蓝色减红色最小的儿子。如果说蓝色本来就是 ≤ 红色的,就不用改了

- 黄色式子不变
- 红色式子可以化简,在计算黄色公式的基础上,增加一个蓝色减红色的最小值
- 红色 >= 黄色,所以计算蓝色的公式也可以化简(去掉红色)
保安站岗,Python 代码 https://www.luogu.com.cn/paste/y9iiynzw