核心要点

  • 五步法:定义状态 dp[i] 的含义 → 推导转移方程 → 初始化边界 → 确定遍历顺序 → 滚动数组优化空间

  • 复杂度:状态数 × 单状态转移代价,常见 O(n) 或 O(n·m)

  • 易错点:状态定义不清导致转移写错;遍历顺序与依赖关系不一致;边界 dp[0] 未初始化

标准回答

动态规划适用于具有「最优子结构」和「重叠子问题」的题目,核心是把大问题拆成可复用的子问题并缓存结果。解题套路固定为五步:先明确 dp 数组每个下标的含义,再根据当前状态如何由更小状态推导写出转移方程,然后初始化边界值,接着按依赖方向确定遍历顺序,最后视情况用滚动变量降低空间。下面以经典的爬楼梯(每次走 1 或 2 阶,问到第 n 阶有几种走法)和最长公共子序列两个例子说明:

python
def climb_stairs(n: int) -> int:
    # 状态 dp[i]:到达第 i 阶的方法数
    # 转移方程:dp[i] = dp[i-1] + dp[i-2](最后一步走 1 或 2 阶)
    # 边界:dp[0]=1(原地一种), dp[1]=1
    if n <= 1:
        return 1
    prev2, prev1 = 1, 1          # 用两个变量做滚动数组,省空间
    for _ in range(2, n + 1):    # 遍历顺序:从小到大,因为 dp[i] 依赖更小的下标
        prev2, prev1 = prev1, prev1 + prev2
    return prev1


def longest_common_subsequence(a: str, b: str) -> int:
    # 状态 dp[i][j]:a 前 i 个字符与 b 前 j 个字符的最长公共子序列长度
    m, n = len(a), len(b)
    dp = [[0] * (n + 1) for _ in range(m + 1)]   # 边界整行整列为 0
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if a[i - 1] == b[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1          # 末字符相同
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])  # 否则取较大者
    return dp[m][n]


if __name__ == '__main__':
    print(climb_stairs(5))                       # 8
    print(longest_common_subsequence('abcde', 'ace'))  # 3

常见误区

⚠️ 常见踩坑

把「子序列」误当成「子串」(子序列不要求连续);遍历顺序与状态依赖方向相反,导致用到尚未计算的值。

追问

追问 1爬楼梯和 LCS 的时间空间复杂度分别是多少?

爬楼梯时间 O(n)、空间 O(1)(滚动变量)。LCS 时间 O(m·n)、空间 O(m·n),可用一维滚动数组优化到 O(min(m,n))。

追问 2如何判断一道题能不能用动态规划?

看是否同时具备最优子结构(大问题最优解由子问题最优解组成)和重叠子问题(递归时同一子问题被反复计算)。若子问题不重叠,分治即可;若有后效性(当前决策影响未来状态的合法性),需重新设计状态消除后效性。

延伸学习

与本题相关的知识库文章、术语、工具与行业资讯。