动态规划解题思路:入门篇

摘要 :动态规划(DP)是算法面试与竞赛的高频考点,核心在于把大问题拆分为重叠子问题,通过保存子问题解避免重复计算。本文从「思路」出发,给出可复用的解题五步法:明确状态定义 → 推导状态转移方程 → 确定初始化与边界 → 确定遍历顺序 → 确定返回值。随后用爬楼梯、最长递增子序列、0-1 背包三道经典题逐步拆解,并总结常见题型套路、误区与空间优化技巧,帮助读者建立系统的 DP 解题方法论。

1. 引言

动态规划(Dynamic Programming,简称 DP)是算法面试和竞赛中的高频考点,也是很多初学者觉得最难啃的硬骨头。它看似抽象,其实有一套固定的思考框架。本文不堆砌公式,而是从「思路」出发,带你建立一套可复用的解题方法论,配合经典例题逐步拆解,帮你真正掌握动态规划。

2. 什么是动态规划

动态规划的核心思想是:把一个大问题拆分成若干重叠的子问题,通过求解并保存子问题的解,避免重复计算,从而高效地得到原问题的解。

它通常适用于具有以下两个特征的问题:

  • 最优子结构:原问题的最优解可以由子问题的最优解推导而来。
  • 重叠子问题:在递归求解过程中,同一个子问题会被反复计算多次。

与之相对的是分治算法------分治的子问题通常是相互独立的,而动态规划的子问题之间是重叠的,这正是它能用「记忆化」或「填表」来加速的根本原因。

3. 动态规划解题五步法

面对一道动态规划题,不要急着写代码,先按下面五步走:

3.1 明确状态定义

状态是动态规划的灵魂。问自己:我需要记录哪些信息,才能描述当前所处的子问题?

通常用一个或多个维度来表示,例如:

  • dp[i] 表示前 i 个元素的最优解;
  • dp[i][j] 表示在二维场景下(如网格、两个字符串)的状态。

状态定义的好坏,直接决定转移方程是否好写。定义得太粗会丢失信息,定义得太细又会造成维度爆炸。

3.2 推导状态转移方程

这是最核心的一步。思考:当前状态是如何从更小的状态转移过来的?

可以这样问自己:

  • 最后一步做了什么决策?
  • 这个决策对应哪几个前驱状态?
  • 取最大值、最小值,还是求和、求布尔值?

例如斐波那契数列的转移方程就是:

text 复制代码
dp[i] = dp[i-1] + dp[i-2]

3.3 确定初始化和边界条件

边界条件决定了递推的起点。通常需要手动设置:

  • dp[0]dp[1] 等初始值;
  • 数组越界时的处理;
  • 空串、空数组等特殊输入。

初始化错误是动态规划最常见的 bug 来源,务必仔细。

3.4 确定遍历顺序

遍历顺序要保证:计算当前状态时,它所依赖的前驱状态已经被计算出来。

常见的有:

  • 一维问题通常从左到右遍历;
  • 二维问题通常按行从上到下、从左到右遍历;
  • 背包类问题要注意物品循环和容量循环的先后顺序。

3.5 确定返回值

最后想清楚:题目要的答案存在哪个状态里?是 dp[n]dp[n][m],还是 dp 数组中的最大值?

4. 经典例题拆解

理论讲完,我们用三道经典题把五步法串起来。

4.1 爬楼梯

题目 :每次可以爬 1 或 2 个台阶,爬到第 n 级台阶有多少种不同方法?

思路拆解

  1. 状态定义dp[i] 表示爬到第 i 级台阶的方法数。
  2. 转移方程 :最后一步要么从第 i-1 级跨 1 步,要么从第 i-2 级跨 2 步,所以 dp[i] = dp[i-1] + dp[i-2]
  3. 初始化dp[1] = 1dp[2] = 2
  4. 遍历顺序 :从 3n 递增。
  5. 返回值dp[n]
python 复制代码
def climb_stairs(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]

4.2 最长递增子序列

题目:给定一个整数数组,求其中最长的严格递增子序列的长度。

思路拆解

  1. 状态定义dp[i] 表示以 nums[i] 结尾的最长递增子序列长度。
  2. 转移方程 :遍历 j < i,若 nums[j] < nums[i],则 dp[i] = max(dp[i], dp[j] + 1)
  3. 初始化:每个位置至少为 1(自身构成子序列)。
  4. 遍历顺序 :外层 i 从左到右,内层 j 遍历 i 之前的所有位置。
  5. 返回值dp 数组的最大值。
python 复制代码
def length_of_lis(nums: list[int]) -> int:
    n = len(nums)
    if n == 0:
        return 0
    dp = [1] * n
    for i in range(n):
        for j in range(i):
            if nums[j] < nums[i]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)

4.3 0-1 背包问题

题目 :有 n 个物品,每个物品有重量 w[i] 和价值 v[i],背包容量为 C,求能装入的最大价值。

思路拆解

  1. 状态定义dp[i][j] 表示前 i 个物品放入容量为 j 的背包能获得的最大价值。
  2. 转移方程 :第 i 个物品不选或选,取较大者:
text 复制代码
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
  1. 初始化dp[0][j] = 0
  2. 遍历顺序:外层遍历物品,内层遍历容量。
  3. 返回值dp[n][C]
python 复制代码
def knapsack(weights: list[int], values: list[int], capacity: int) -> int:
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(capacity + 1):
            if j >= weights[i - 1]:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
            else:
                dp[i][j] = dp[i - 1][j]
    return dp[n][capacity]

5. 常见题型与套路总结

掌握基础后,可以按题型归类记忆套路:

题型 典型特征 常用状态
线性 DP 一维数组、序列问题 dp[i]
区间 DP 合并、切分区间 dp[i][j] 表示区间 [i, j]
背包 DP 容量限制下的选择问题 dp[i][j]i 个物品容量 j
状态压缩 DP 集合、子集枚举 用二进制位表示状态
树形 DP 树结构上的决策 dp[node] 以节点为根的子树

6. 常见误区与优化技巧

6.1 常见误区

  • 状态定义不清晰就急着写转移方程,导致越写越乱;
  • 忽略边界条件,数组越界或初始值错误;
  • 遍历顺序错误,依赖的状态还没算出来;
  • 把动态规划和贪心混淆,贪心只保证局部最优,动态规划才保证全局最优。

6.2 空间优化

很多一维 DP 只依赖前一个或前两个状态,可以用滚动数组把空间从 O(n) 降到 O(1)。例如爬楼梯问题:

python 复制代码
def climb_stairs_optimized(n: int) -> int:
    if n <= 2:
        return n
    prev2, prev1 = 1, 2
    for _ in range(3, n + 1):
        prev2, prev1 = prev1, prev2 + prev1
    return prev1

7. 总结

动态规划并不神秘,它只是一套「拆解问题、记录中间结果、避免重复计算」的思维工具。拿到题目后,按五步法走:定义状态 → 推导转移方程 → 初始化边界 → 确定遍历顺序 → 确定返回值,再配合大量练习,就能逐渐形成肌肉记忆。

建议从爬楼梯、打家劫舍、最长公共子序列等经典题入手,每道题都刻意用五步法写一遍思路,而不是直接抄代码。坚持一段时间,你会发现动态规划其实是最有规律可循的题型之一。

相关推荐
Tisfy1 天前
LeetCode 2472.不重叠回文子字符串的最大数目:动态规划(贪心)
动态规划·字符串·题解·贪心·dp·回文字符串
青山木2 天前
Hot 100 --- 最长递增子序列
java·数据结构·算法·leetcode·动态规划
Logic1013 天前
C语言/数据动态规划题解:翻转一次子数组后的最大子数组和——四状态DP(O(n)时间O(1)空间)
c语言·数据结构·动态规划·时间复杂度·算法题·状态机dp·最大子数组和
青山木3 天前
Hot 100 --- 打家劫舍
java·数据结构·算法·leetcode·动态规划
青山木3 天前
枚举类DP进阶:完全平方数与零钱兑换
java·数据结构·算法·leetcode·动态规划
wzdark4 天前
基于矩阵的动态规划路径问题性能优化4
线性代数·矩阵·动态规划
find1star6 天前
LeetCode 25:K 个一组翻转链表
java·数据结构·算法·leetcode·链表·职场和发展·动态规划
chushiyunen6 天前
动态规划、贪心算法、分治法
算法·贪心算法·动态规划
梓䈑7 天前
【动态规划系列】路径问题、子数组 和 子序列问题、背包问题
c++·算法·动态规划