Skip to content

动态规划

动态规划(Dynamic Programming,DP)是解决复杂问题的强大算法思想,通过将复杂问题分解为重叠子问题,并存储子问题的解来避免重复计算。


一、什么是动态规划?

1.1 生活化的理解

想象你在爬一个有100级的楼梯:

  • 每次可以爬1级或2级
  • 问:有多少种不同的方法爬到第100级?

如果用普通的递归,你会重复计算很多次:

  • 计算到第10级的方法时,会重复计算到第8级、第9级的方法
  • 动态规划的思想是:把每次计算的结果记下来,下次直接用

1.2 核心思想

动态规划的核心是:大事化小,小事化了

  1. 大事化小:把大问题分解成小问题
  2. 小事化了:把小问题的解存储起来,避免重复计算

二、动态规划的三大特征

2.1 最优子结构

定义:问题的最优解包含子问题的最优解

例子:最短路径问题

  • 从北京到上海的最短路径,一定包含从北京到某个中间城市的最短路径
  • 如果北京→南京→上海是最短路径,那么北京→南京也必须是北京到南京的最短路径

2.2 重叠子问题

定义:在求解过程中,许多子问题会被重复计算

例子:斐波那契数列

txt
F(5) = F(4) + F(3)
F(4) = F(3) + F(2)
F(3) = F(2) + F(1)

可以看到F(3)被计算了两次,这就是重叠子问题

2.3 无后效性

定义:当前状态一旦确定,后续决策不会影响前面的状态

例子:买股票问题

  • 第5天的决策只依赖于第4天的状态,与第1-3天的具体决策无关

三、动态规划解题五步法

步骤1:定义状态

问题:用什么变量来描述问题的状态?

技巧

  • 一维状态:dp[i]表示前i个元素的最优解
  • 二维状态:dp[i][j]表示前i个元素中,容量为j时的最优解
  • 三维状态:dp[i][j][k]表示前i个元素,使用j个,达到状态k的最优解

步骤2:状态转移方程

问题:如何从已知状态推导未知状态?

常见模式

  • 累加模式:dp[i] = dp[i-1] + dp[i-2](斐波那契)
  • 选择模式:dp[i] = max(dp[i-1], dp[i-2] + nums[i])(打家劫舍)
  • 组合模式:dp[i][j] = dp[i-1][j] + dp[i][j-1](路径计数)

步骤3:初始化

问题:边界条件是什么?

常见初始化

  • dp[0] = 0 或 1
  • dp[0][j] = 0
  • dp[i][0] = 0

步骤4:计算顺序

问题:从小到大还是从大到小?

原则

  • 确保计算dp[i]时,dp[i-1]等依赖的状态已经计算好
  • 通常是从左到右、从上到下

步骤5:返回结果

问题:最终答案在哪里?

常见结果位置

  • dp[n]:前n个元素的结果
  • dp[n][m]:前n个元素,容量为m的结果
  • max(dp):所有状态中的最大值

四、动态规划分类详解

4.1 线性动态规划

特征:状态是一维的,按线性顺序转移

经典问题1:爬楼梯

问题描述:每次可以爬1级或2级,求爬到第n级的方法数

生活场景:想象你在爬一个有5级的楼梯,每次可以选择迈1级或2级,问有多少种不同的方法爬到第5级?

状态定义:dp[i] = 爬到第i级台阶的方法总数

状态转移方程:dp[i] = dp[i-1] + dp[i-2]

  • 从第i-1级迈1级上来 → 方法数:dp[i-1]
  • 从第i-2级迈2级上来 → 方法数:dp[i-2]

初始化

  • dp[0] = 1(地面就是1种方法)
  • dp[1] = 1(只有1种方法:直接迈1级)

详细计算过程

级数i计算过程结果具体路径
dp[0]初始化1起点
dp[1]初始化11
dp[2]dp[1] + dp[0] = 1 + 121→1, 2
dp[3]dp[2] + dp[1] = 2 + 131→1→1, 1→2, 2→1
dp[4]dp[3] + dp[2] = 3 + 251→1→1→1, 1→1→2, 1→2→1, 2→1→1, 2→2
dp[5]dp[4] + dp[3] = 5 + 388种完整路径

8种具体方法

方法1:1→1→1→1→1(每次都迈1级)
方法2:1→1→1→2(最后迈2级)
方法3:1→1→2→1(中间迈2级)
方法4:1→2→1→1(第二个迈2级)
方法5:2→1→1→1(第一个迈2级)
方法6:1→2→2(两次迈2级)
方法7:2→1→2(中间迈1级)
方法8:2→2→1(前两次迈2级)

图解

级数:  0   1   2   3   4   5
方法数: 1   1   2   3   5   8

巧妙之处:动态规划把指数级O(2^n)的递归复杂度降到线性级O(n),通过存储中间结果避免重复计算!

经典问题2:打家劫舍

问题描述:一排房子,不能连续偷相邻的两家,求最大金额

状态定义:dp[i] = 偷到第i家时的最大金额

状态转移:dp[i] = max(dp[i-1], dp[i-2] + nums[i])

4.2 背包动态规划

特征:在容量限制下选择物品,使价值最大化

0-1背包问题

问题描述:每个物品只能选一次

状态定义:dp[i][j] = 前i个物品,容量为j时的最大价值

状态转移

  • 不选第i个物品:dp[i][j] = dp[i-1][j]
  • 选第i个物品:dp[i][j] = dp[i-1][j-w[i]] + v[i]
  • 取最大值:dp[i][j] = max(上述两种情况)

完全背包问题

问题描述:每个物品可以选无限次

与0-1背包的区别

  • 0-1背包:从后往前更新(避免重复使用)
  • 完全背包:从前往后更新(允许重复使用)

4.3 区间动态规划

特征:状态表示一个区间[i,j],通过合并小区间得到大区间

经典问题:矩阵链乘法

问题描述:给定矩阵链,找出最优乘法顺序

状态定义:dp[i][j] = 计算矩阵i到j的最小乘法次数

状态转移

dp[i][j] = min(dp[i][k] + dp[k+1][j] + p[i-1]*p[k]*p[j])
其中k从i到j-1

4.4 状态压缩动态规划

特征:使用位运算来压缩状态,处理复杂约束

经典问题:旅行商问题

问题描述:访问所有城市的最短路径

状态定义:dp[mask][i] = 已经访问mask表示的城市集合,当前在i城市的最短路径


五、空间优化技巧

5.1 滚动数组优化

适用场景:当前状态只依赖于前一个或前几个状态

例子:斐波那契数列

python
# 优化前:O(n)空间
dp = [0] * (n + 1)

# 优化后:O(1)空间
prev2, prev1 = 0, 1
for i in range(2, n + 1):
    curr = prev1 + prev2
    prev2, prev1 = prev1, curr

5.2 降维优化

适用场景:二维DP可以降为一维

例子:0-1背包问题

python
# 优化前:二维数组
dp = [[0] * (capacity + 1) for _ in range(n + 1)]

# 优化后:一维数组
dp = [0] * (capacity + 1)

5.3 对称性优化

适用场景:问题具有对称性,可以减少计算量


六、实际应用场景

6.1 金融领域

  • 股票买卖:计算最大收益
  • 投资组合:在风险约束下最大化收益

6.2 生物信息学

  • 序列比对:DNA序列的相似性分析
  • 基因预测:寻找基因编码区域

6.3 游戏开发

  • AI决策:NPC的最优行动选择
  • 路径规划:角色的智能移动

6.4 网络优化

  • 路由算法:寻找最短路径
  • 流量控制:网络包的最优传输

七、学习路径建议

7.1 新手入门(1-2周)

目标:掌握基本思想和简单问题

推荐练习

  1. 爬楼梯(线性DP)
  2. 斐波那契数列(记忆化搜索)
  3. 最大子数组和(线性DP)

7.2 进阶提升(2-4周)

目标:掌握常见分类和解题技巧

推荐练习

  1. 0-1背包问题(背包DP)
  2. 最长递增子序列(序列DP)
  3. 编辑距离(二维DP)

7.3 高级应用(4-8周)

目标:解决复杂问题,掌握优化技巧

推荐练习

  1. 区间DP问题
  2. 状态压缩DP
  3. 树形DP
  4. 数位DP

八、常见误区与解决方案

8.1 误区1:状态定义不清晰

表现:不知道如何定义dp数组

解决方案

  • 先思考:用什么变量能完整描述问题的状态?
  • 画表格:手动计算小例子,找出规律
  • 问问题:如果已知dp[i-1],能否推导出dp[i]?

8.2 误区2:状态转移方程错误

表现:递推关系不正确

解决方案

  • 分类讨论:考虑所有可能的选择
  • 验证边界:检查边界条件是否满足
  • 小例子验证:用手算验证小例子

8.3 误区3:初始化错误

表现:边界条件设置不当

解决方案

  • 理解含义:明确dp[0]代表什么
  • 特殊情况:考虑空数组、空字符串的情况
  • 逐步调试:打印中间结果检查

九、经典问题总结表

问题类型经典问题状态定义状态转移时间复杂度
线性DP爬楼梯dp[i]dp[i]=dp[i-1]+dp[i-2]O(n)
背包DP0-1背包dp[i][j]max选/不选O(n×W)
序列DP最长递增子序列dp[i]max(dp[j])+1O(n²)
区间DP矩阵链乘法dp[i][j]min分割点O(n³)
编辑距离字符串编辑dp[i][j]min增删改O(m×n)

十、实战练习建议

10.1 入门练习

  1. LeetCode 70:爬楼梯
  2. LeetCode 53:最大子数组和
  3. LeetCode 198:打家劫舍

10.2 进阶练习

  1. LeetCode 300:最长递增子序列
  2. LeetCode 416:分割等和子集
  3. LeetCode 72:编辑距离

10.3 高级练习

  1. LeetCode 132:分割回文串II
  2. LeetCode 312:戳气球
  3. LeetCode 10:正则表达式匹配

总结

动态规划是一种强大的算法思想,关键在于:

  1. 理解问题:找出最优子结构和重叠子问题
  2. 正确定义状态:用最少变量描述问题状态
  3. 建立状态转移:找出递推关系
  4. 注意边界条件:正确初始化
  5. 考虑空间优化:在需要时进行优化

记住:动态规划不是背模板,而是理解思想,灵活运用!

建议下一步:选择3-5个经典问题,从简单到复杂逐步练习,体会动态规划的思想精髓。

别急,先让缓存热一下。