LeetCode-Py 动态规划专题:从基础到进阶全面解析

LeetCode-Py 动态规划专题:从基础到进阶全面解析

【免费下载链接】LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 【免费下载链接】LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

引言:为什么动态规划是算法面试的必考重点?

还在为动态规划题目感到头疼吗?面对复杂的递推关系和状态转移方程无从下手?本文将带你系统掌握动态规划的核心思想和解题技巧,从基础概念到高级应用,一站式解决你的动态规划学习难题!

通过本文,你将获得:

  • ✅ 动态规划三大特性的深度理解
  • ✅ 0-1背包、完全背包等经典模型的完整解析
  • ✅ 区间DP、树形DP等高级技巧的实战应用
  • ✅ 20+道LeetCode经典题目的详细题解
  • ✅ 空间优化和状态压缩的实用技巧

一、动态规划基础概念

1.1 什么是动态规划?

动态规划(Dynamic Programming,DP) 是一种求解多阶段决策过程最优化问题的方法。其核心思想是通过把原问题分解为相对简单的子问题,先求解子问题,再由子问题的解得到原问题的解。

mermaid

1.2 动态规划的三大特性

1.2.1 最优子结构(Optimal Substructure)

一个问题的最优解包含其子问题的最优解。这意味着我们可以通过组合子问题的最优解来构造原问题的最优解。

1.2.2 重叠子问题(Overlapping Subproblems)

在求解过程中,相同的子问题会被多次重复计算。动态规划通过记忆化存储避免重复计算。

1.2.3 无后效性(No Aftereffect)

一旦某个状态确定,它就不会再受后续决策的影响。当前状态只与之前的状态有关。

1.3 动态规划解题五步法

步骤描述关键点
1. 划分阶段将问题分解为若干阶段确定阶段顺序
2. 定义状态选择合适的状态变量状态要满足无后效性
3. 状态转移建立状态转移方程核心递推关系
4. 初始条件确定边界状态的值基础case处理
5. 计算顺序确定状态计算顺序自底向上或自顶向下

二、基础动态规划问题

2.1 斐波那契数列

问题描述:计算第n个斐波那契数。

def fib(n: int) -> int:
    if n <= 1:
        return n
    
    dp = [0] * (n + 1)
    dp[0], dp[1] = 0, 1
    
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    
    return dp[n]

复杂度分析

  • 时间复杂度:O(n)
  • 空间复杂度:O(n)(可优化到O(1))

2.2 爬楼梯问题

问题描述:每次可以爬1或2个台阶,计算爬到n阶的方法数。

def climbStairs(n: int) -> int:
    if n <= 2:
        return n
    
    dp = [0] * (n + 1)
    dp[1], dp[2] = 1, 2
    
    for i in range(3, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    
    return dp[n]

2.3 不同路径

问题描述:机器人从左上角到右下角的路径数。

def uniquePaths(m: int, n: int) -> int:
    dp = [[0] * n for _ in range(m)]
    
    # 初始化第一行和第一列
    for i in range(m):
        dp[i][0] = 1
    for j in range(n):
        dp[0][j] = 1
    
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
    
    return dp[m - 1][n - 1]

三、背包问题专题

3.1 0-1背包问题

问题描述:有n件物品,第i件物品重量为weight[i],价值为value[i],背包容量为W,求最大价值。

3.1.1 二维DP解法
def zeroOnePack(weight: list, value: list, W: int) -> int:
    n = len(weight)
    dp = [[0] * (W + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for w in range(W + 1):
            if w < weight[i - 1]:
                dp[i][w] = dp[i - 1][w]
            else:
                dp[i][w] = max(dp[i - 1][w], 
                              dp[i - 1][w - weight[i - 1]] + value[i - 1])
    
    return dp[n][W]
3.1.2 一维空间优化
def zeroOnePackOptimized(weight: list, value: list, W: int) -> int:
    n = len(weight)
    dp = [0] * (W + 1)
    
    for i in range(n):
        for w in range(W, weight[i] - 1, -1):
            dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
    
    return dp[W]

3.2 完全背包问题

问题描述:每种物品有无限个,其他条件同0-1背包。

def completePack(weight: list, value: list, W: int) -> int:
    n = len(weight)
    dp = [0] * (W + 1)
    
    for i in range(n):
        for w in range(weight[i], W + 1):
            dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
    
    return dp[W]

3.3 多重背包问题

问题描述:每种物品有有限个,其他条件同0-1背包。

def multiplePack(weight: list, value: list, count: list, W: int) -> int:
    n = len(weight)
    dp = [0] * (W + 1)
    
    for i in range(n):
        # 二进制优化
        k = 1
        while k <= count[i]:
            for w in range(W, k * weight[i] - 1, -1):
                dp[w] = max(dp[w], dp[w - k * weight[i]] + k * value[i])
            count[i] -= k
            k *= 2
        
        if count[i] > 0:
            for w in range(W, count[i] * weight[i] - 1, -1):
                dp[w] = max(dp[w], 
                           dp[w - count[i] * weight[i]] + count[i] * value[i])
    
    return dp[W]

3.4 背包问题应用实例

3.4.1 分割等和子集
def canPartition(nums: list) -> bool:
    total = sum(nums)
    if total % 2 != 0:
        return False
    
    target = total // 2
    dp = [False] * (target + 1)
    dp[0] = True
    
    for num in nums:
        for j in range(target, num - 1, -1):
            dp[j] = dp[j] or dp[j - num]
    
    return dp[target]
3.4.2 零钱兑换
def coinChange(coins: list, amount: int) -> int:
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    
    for coin in coins:
        for i in range(coin, amount + 1):
            dp[i] = min(dp[i], dp[i - coin] + 1)
    
    return dp[amount] if dp[amount] != float('inf') else -1

四、区间动态规划

4.1 区间DP基本框架

def intervalDP(n: int, cost: list) -> int:
    dp = [[0] * n for _ in range(n)]
    
    # 初始化长度为1的区间
    for i in range(n):
        dp[i][i] = 0  # 根据具体问题调整
    
    # 枚举区间长度
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            # 枚举分割点
            for k in range(i, j):
                dp[i][j] = min(dp[i][j], 
                              dp[i][k] + dp[k + 1][j] + cost[i][j])
    
    return dp[0][n - 1]

4.2 最长回文子序列

def longestPalindromeSubseq(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]

4.3 戳气球问题

def maxCoins(nums: list) -> int:
    n = len(nums)
    # 添加虚拟气球
    arr = [1] + nums + [1]
    m = len(arr)
    dp = [[0] * m for _ in range(m)]
    
    for length in range(3, m + 1):
        for i in range(0, m - length + 1):
            j = i + length - 1
            for k in range(i + 1, j):
                dp[i][j] = max(dp[i][j], 
                              dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j])
    
    return dp[0][m - 1]

五、树形动态规划

5.1 二叉树中的最大路径和

class TreeNode:
    def __init__(self, val=0, left=None, right=None):
        self.val = val
        self.left = left
        self.right = right

def maxPathSum(root: TreeNode) -> int:
    max_sum = float('-inf')
    
    def dfs(node):
        nonlocal max_sum
        if not node:
            return 0
        
        left_max = max(0, dfs(node.left))
        right_max = max(0, dfs(node.right))
        
        # 当前节点作为路径转折点
        current_sum = node.val + left_max + right_max
        max_sum = max(max_sum, current_sum)
        
        # 返回当前节点作为路径一部分的最大值
        return node.val + max(left_max, right_max)
    
    dfs(root)
    return max_sum

5.2 打家劫舍III

def rob(root: TreeNode) -> int:
    def dfs(node):
        if not node:
            return (0, 0)  # (偷当前节点的最大值, 不偷当前节点的最大值)
        
        left = dfs(node.left)
        right = dfs(node.right)
        
        # 偷当前节点,则不能偷子节点
        rob_current = node.val + left[1] + right[1]
        # 不偷当前节点,可以偷或不偷子节点
        not_rob_current = max(left[0], left[1]) + max(right[0], right[1])
        
        return (rob_current, not_rob_current)
    
    result = dfs(root)
    return max(result[0], result[1])

六、状态压缩动态规划

6.1 旅行商问题(TSP)

def tsp(graph: list) -> int:
    n = len(graph)
    # dp[mask][i] 表示访问过mask中的城市,当前在城市i的最小代价
    dp = [[float('inf')] * n for _ in range(1 << n)]
    dp[1][0] = 0  # 从城市0开始
    
    for mask in range(1 << n):
        for i in range(n):
            if not (mask & (1 << i)):
                continue
            for j in range(n):
                if mask & (1 << j):
                    continue
                new_mask = mask | (1 << j)
                dp[new_mask][j] = min(dp[new_mask][j], 
                                     dp[mask][i] + graph[i][j])
    
    # 返回起点并形成环路
    return min(dp[(1 << n) - 1][i] + graph[i][0] for i in range(n))

6.2 状态压缩DP优化技巧

mermaid

七、动态规划优化技巧

7.1 斜率优化

对于形如 $dp[i] = min_{j<i}{dp[j] + f(i, j)}$ 的DP方程,当函数f具有特定性质时,可以使用单调队列优化。

def slopeOptimization(n: int, f: callable) -> int:
    dp = [0] * (n + 1)
    q = deque([0])  # 存储决策点
    
    for i in range(1, n + 1):
        # 维护队列单调性
        while len(q) >= 2 and \
              calcSlope(q[0], q[1]) <= f(i):
            q.popleft()
        
        j = q[0]
        dp[i] = dp[j] + cost(j, i)
        
        # 将i加入决策集合
        while len(q) >= 2 and \
              calcSlope(q[-2], q[-1]) >= calcSlope(q[-1], i):
            q.pop()
        q.append(i)
    
    return dp[n]

7.2 四边形不等式优化

对于满足四边形不等式的DP问题,可以优化决策单调性。

def quadrangleInequality(n: int) -> int:
    dp = [[0] * n for _ in range(n)]
    # K[i][j] 记录最优决策点
    K = [[0] * n for _ in range(n)]
    
    for i in range(n):
        dp[i][i] = 0
        K[i][i] = i
    
    for length in range(2, n + 1):
        for i in range(n - length + 1):
            j = i + length - 1
            dp[i][j] = float('inf')
            # 利用四边形不等式缩小决策范围
            left = K[i][j - 1] if i <= j - 1 else i
            right = K[i + 1][j] if i + 1 <= j else j
            
            for k in range(left, right + 1):
                current = dp[i][k] + dp[k + 1][j] + cost(i, j)
                if current < dp[i][j]:
                    dp[i][j] = current
                    K[i][j] = k
    
    return dp[0][n - 1]

八、实战训练计划

8.1 初级训练(掌握基础)

题目难度核心考点
509. 斐波那契数简单基础DP
70. 爬楼梯简单基础DP
198. 打家劫舍中等序列DP
121. 买卖股票的最佳时机简单状态机DP

8.2 中级训练(掌握经典模型)

题目难度核心考点
322. 零钱兑换中等完全背包
416. 分割等和子集中等0-1背包
300. 最长递增子序列中等序列DP
1143. 最长公共子序列中等双序列DP

8.3 高级训练(掌握优化技巧)

题目难度核心考点
312. 戳气球困难区间DP
154. 寻找旋转排序数组中的最小值 II困难二分+DP
887. 鸡蛋掉落困难决策优化
10. 正则表达式匹配困难字符串DP

九、常见误区与调试技巧

9.1 常见错误

  1. 状态定义不准确:状态应该包含解决问题的所有必要信息
  2. 边界条件处理不当:特别注意数组越界和初始状态
  3. 状态转移方程错误:仔细验证递推关系的正确性
  4. 计算顺序错误:确保依赖的状态已经计算完成

9.2 调试技巧

def debugDP(dp: list, name: str = "DP Table"):
    print(f"=== {name} ===")
    for i, row in enumerate(dp):
        print(f"dp[{i}]: {row}")
    print()

9.3 测试用例设计

def testDP():
    test_cases = [
        # (输入, 期望输出)
        ([1, 2, 3, 4], 10),
        ([], 0),
        ([5], 5),
        # 边界测试用例
        ([0] * 1000, 0),
    ]
    
    for i, (input_data, expected) in enumerate(test_cases):
        result = solution(input_data)
        assert result == expected, f"Case {i} failed: {result} != {expected}"
        print(f"Case {i} passed")

十、总结与进阶学习

10.1 动态规划学习路线

mermaid

10.2 推荐学习资源

  1. 经典书籍

    • 《算法导论》动态规划章节
    • 《算法竞赛入门经典》DP专题
    • 《背包九讲》
  2. 在线资源

    • LeetCode动态规划专题
    • 各大OJ的DP训练集
    • 算法竞赛培训材料
  3. 实践建议

    • 每天至少完成2道DP题目
    • 定期复习经典模型
    • 参与算法竞赛锻炼实战能力

结语

动态规划是算法领域的核心内容,也是面试中的高频考点。通过系统学习和大量练习,掌握动态规划不仅能够提升算法能力,更能培养抽象思维和问题分解能力。希望本文能够为你提供清晰的学习路径和实用的解题技巧,助你在动态规划的学习道路上稳步前进!

下一步行动建议

  1. 从基础题目开始,逐步提升难度
  2. 重点掌握背包、区间、树形等经典模型
  3. 注重理解而非记忆,掌握思想精髓
  4. 坚持练习,量变引起质变

祝你学习顺利,早日成为动态规划高手!

【免费下载链接】LeetCode-Py ⛽️「算法通关手册」:超详细的「算法与数据结构」基础讲解教程,从零基础开始学习算法知识,800+ 道「LeetCode 题目」详细解析,200 道「大厂面试热门题目」。 【免费下载链接】LeetCode-Py 项目地址: https://gitcode.com/gh_mirrors/le/LeetCode-Py

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值