【灵神高频面试题合集21-25】动态规划(下)

基础算法精讲·题目汇总:灵茶山艾府 - 【基础算法精讲】- 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)

作者:灵茶山艾府

链接:https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/solutions/2201488/shi-pin-jiao-ni-yi-bu-bu-si-kao-dong-tai-kksg/

来源:力扣(LeetCode)

课后作业

121. 买卖股票的最佳时机

123. 买卖股票的最佳时机 III

714. 买卖股票的最佳时机含手续费

2826. 将三个组排序

2786. 访问数组中的位置使分数最大

1911. 最大子序列交替和


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]

课后作业

1312. 让字符串成为回文串的最少插入次数

3472. 至多 K 次操作后的最长回文子序列

3040. 相同分数的最大操作数目 II

1130. 叶值的最小代价生成树

1770. 执行乘法运算的最大分数

1771. 由子序列构造的最长回文串的长度

1547. 切棍子的最小成本


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

课后作业

687. 最长同值路径

3203. 合并两棵树后的最小直径

1617. 统计子树中城市之间最大距离

2538. 最大价值和与最小价值和的差值


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,那么算出来的就是最大独立集了

课后作业

1377. T 秒后青蛙的位置

2646. 最小化旅行的价格总和


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

课后作业

LCP34. 二叉树染色

LCP64. 二叉树灯饰

相关推荐
释厄6231 小时前
学术引用本体论——WorkBuddy 学术科学文化三违反
网络·人工智能·算法
梦帮科技1 小时前
【3.0修订版】 RNS 代币架构:ERC20 五件套扩展与六钱包分配
数据结构·后端·算法·架构·node.js·区块链·php
薛定e的猫咪2 小时前
(AISTATS 2023)BaCaDI:未知干预下的贝叶斯因果发现
人工智能·深度学习·算法·机器学习
lvwangshu2 小时前
CF813D Two Melodies 题解
动态规划·题解·技巧·性质题·思维好题
漂流瓶jz3 小时前
UVA-11491 奖品的价值 题解答案代码 算法竞赛入门经典第二版
c++·算法·图论·题解·aoapc·算法竞赛入门经典·uva
weixin_307779137 小时前
C++代码实现MATLAB中的dlarray函数功能
开发语言·c++·算法·matlab
hetao173383712 小时前
2026-10-03~04 hetao1733837 的刷题记录
c++·算法
Zootopia62613 小时前
飞行力学知识梳理1|飞行性能与稳定性
人工智能·python·算法·机器学习·无人机·学习方法·信息与通信
longlongzihan13 小时前
从LeetCode 70. 爬楼梯看动态规划:如何精准推导状态转移方程?
c++·算法·leetcode·动态规划