动态规划(Dynamic Programming)

动态规划是「分解问题」思维模式的延伸,通过把问题分解为子问题、记忆化子问题答案,从而避免重复计算,优化递归效率。

定义

动态规划(Dynamic Programming,DP)是一种算法设计思想,通过将复杂问题分解为重叠的子问题,并存储子问题的解来避免重复计算,从而得到原问题的最优解。

核心本质:动态规划 = 递归(分解问题) + 记忆化(避免重复计算)

核心特性

特性 说明
重叠子问题 子问题会被重复计算多次
最优子结构 原问题的最优解包含子问题的最优解
状态转移 通过已解决的子问题推导当前问题
自底向上/自顶向下 两种实现方向

与递归的关系

动态规划与递归(分解问题思维)的关系:

维度 普通递归 动态规划
思维模式 分解问题 分解问题 + 记忆化
子问题处理 可能重复计算 存储子问题解,避免重复
效率 可能指数级 通常多项式级
示例 斐波那契递归 斐波那契带记忆化

关键洞察:理解递归是学习动态规划的前提。从树的角度理解递归,就更容易理解动态规划的状态转移。

与贪心算法的区别

动态规划与贪心算法都用于优化问题,但核心思路不同(参见 贪心算法解题套路框架):

维度 动态规划 贪心算法
穷举范围 穷举所有可行解(去重后) 不穷举所有可行解
优化手段 备忘录优化,避免重复计算 推导最优,直接选择局部最优解
适用条件 最优子结构 + 重叠子问题 最优子结构 + 贪心选择性质
时间复杂度 多项式级 $O(n^2)$ 等 通常 $O(n)$ 或 $O(1)$
正确性 只要满足最优子结构就正确 需要证明贪心选择性质成立

关键区别:动态规划仍然穷举所有可行解(只是去掉重复计算),而贪心算法直接跳过完整穷举,通过局部最优选择推导全局最优。

两种实现方式

1. 自顶向下(Top-Down)

特点:从原问题出发,递归求解子问题,用备忘录存储结果

示例(斐波那契带记忆化):

int fib(int n, Map<Integer, Integer> memo) {
    if (n < 2) return n;
    if (memo.containsKey(n)) return memo.get(n);
    int result = fib(n-1, memo) + fib(n-2, memo);
    memo.put(n, result);
    return result;
}

2. 自底向上(Bottom-Up)

特点:从最小的子问题开始,迭代计算更大问题的解

示例(斐波那契):

int fib(int n) {
    if (n < 2) return n;
    int[] dp = new int[n + 1];
    dp[0] = 0; dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

动态规划三要素

  1. 状态定义:dp[i] 或 dp[i][j] 表示什么
  2. 状态转移方程:如何由子问题推导当前问题
  3. 边界条件:最小的子问题如何求解

常见类型

类型 说明 经典问题
线性 DP 状态是线性序列 最长递增子序列、最大子数组和
区间 DP 状态是区间 [i, j] 矩阵链乘法、最长回文子串
背包 DP 状态包含容量和物品 0-1 背包、完全背包
树形 DP 状态在树节点上 二叉树最大路径和
状压 DP 状态用二进制压缩 旅行商问题(TSP)

经典算法

算法/问题 难度 说明
斐波那契数列(带记忆化) 简单 入门 DP,理解重叠子问题
0-1 背包问题 中等 经典 DP,理解状态和选择
最长公共子序列(LCS) 中等 二维 DP 经典问题
最长递增子序列(LIS) 中等 线性 DP
最大子数组和(Kadane) 简单 线性 DP,状态压缩

时间复杂度

操作 时间复杂度 说明
自顶向下(带记忆化) O(状态数) 每个状态只计算一次
自底向上 O(状态数 × 转移复杂度) 迭代计算所有状态
空间复杂度 O(状态数) 存储所有子问题解

注意事项

  • 状态定义要清晰:dp 数组的含义必须明确
  • 边界条件不能漏:最小的子问题必须正确初始化
  • 遍历顺序要正确:自底向上时要注意计算顺序
  • 空间可以优化:有时可以只用一维数组或几个变量

应用场景

  • 最优化问题:求最大值、最小值、最长、最短等
  • 计数问题:有多少种方式、方案数等
  • 存在性问题:是否能达到某个状态
  • 路径规划:最短路径、最小代价等

相关概念

参考资料


Interactive Graph