动态规划(Dynamic Programming,DP)是面试中区分度最高的算法题型,也是很多人的噩梦。其实它的核心只有一句话:把大问题拆成重叠的小问题,记住小问题的答案,避免重复计算。本文用一个套路带你入门。

1. 从斐波那契说起

还记得递归篇里指数级的朴素斐波那契吗?它的低效根源是重叠子问题:同一个子问题被反复计算。解决办法是把结果存下来——这就是 DP 的雏形。

# 自底向上:从最小子问题开始,一步步推到大问题
def fib_dp(n):
    if n <= 1:
        return n
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

print(fib_dp(50))   # 12586269025,瞬间完成

2. DP 五步法

  1. 定义状态:dp[i] 表示什么?最重要的一步。
  2. 状态转移方程:dp[i] 如何由更小的状态算出。
  3. 初始条件:dp[0]、dp[1] 等边界值。
  4. 遍历顺序:保证算 dp[i] 时依赖项已就绪。
  5. 优化(可选):滚动数组压缩空间。

3. 例题:爬楼梯

一次可以爬 1 或 2 阶,爬到 n 阶有几种方法?到第 i 阶,只能从 i-1 阶跨 1 步或从 i-2 阶跨 2 步过来,所以 dp[i] = dp[i-1] + dp[i-2]。

def climb_stairs(n):
    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]

print(climb_stairs(3))   # 3: (1,1,1) (1,2) (2,1)

看出来了吗?它和斐波那契几乎一样——识别题目背后的数学模型,是 DP 的关键能力。

4. 二维 DP:路径问题

进阶题:m×n 网格从左上走到右下,每次只能向右或向下,有多少条路径?状态变为二维:dp[i][j] = dp[i-1][j] + dp[i][j-1]。

def unique_paths(m, n):
    dp = [[1] * n for _ in range(m)]
    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]

print(unique_paths(3, 3))  # 6
print(unique_paths(3, 7))  # 28

若网格中有障碍物,把障碍处的 dp 值设为 0 即可——理解后 LeetCode 63 题一分钟能写出来。

5. 背包问题:DP 的经典试金石

0-1 背包:容量 W,每个物品有重量 w[i] 与价值 v[i],求能装下的最大价值。定义 dp[j] 为容量 j 时的最大价值,对每个物品倒序更新容量,保证每个物品只用一次:

def knapsack(W, weights, values):
    dp = [0] * (W + 1)
    for w, v in zip(weights, values):
        for j in range(W, w - 1, -1):   # 倒序,防止重复取
            dp[j] = max(dp[j], dp[j - w] + v)
    return dp[W]

W = 5
print(knapsack(W, [2, 1, 3], [4, 2, 3]))  # 6: 选 1 号 + 2 号

倒序遍历是 0-1 背包的命门:正序会让同一物品被用两次,退化成完全背包。

6. 新手最容易踩的坑

💡 学习建议:DP 靠"量的积累 + 套路总结"。按"一维 → 二维 → 背包 → 区间"的顺序刷 30 道经典题,每道按五步法写注释,比盲目刷 200 道有效。