目录

如何攻克动态规划面试题

动态规划一直被公认为编程面试中最难的题型。从应届生到资深工程师,各个级别的候选人都表示 DP 题目是面试中最大的焦虑来源。然而这类题目在 Google、Amazon、Meta、Apple 等头部公司的编程面试中出现率高达 30–40%,你无法回避它们。好消息是,DP 遵循一套有限的、可重复的模式,通过 OfferBull 模拟面试环境进行刻意练习,你可以在最棘手的场景中建立真正的信心。

为什么动态规划让人感觉如此困难

大多数候选人在 DP 上碰壁,并不是因为数学太复杂,而是因为思维过程不够熟悉。和贪心算法或直接递归不同,DP 要求你识别重叠子问题和最优子结构——这些概念在大学课程中很少被充分讲解。结果是很多工程师依赖模式匹配而非真正理解,一旦面试官稍加变化就会崩盘。

关键洞察是:每一个 DP 问题本质上都是一个带有冗余计算的递归问题。一旦你看清这一点,解题路径就始终一致:定义状态,写出递推关系,然后决定使用自顶向下的记忆化搜索还是自底向上的递推表格法。

两种方法:记忆化搜索 vs. 递推表格法

自顶向下:记忆化搜索

从递归解法出发,加上缓存。这种方法通常更直观,因为它契合你自然思考问题的方式——从原始问题出发,逐步拆解为更小的子问题。

适用场景: 当状态空间很大但实际访问的状态只是一小部分时。树形 DP 问题和状态转移复杂的问题通常适合自顶向下。

示例模板:

def solve(i, j, memo={}):
    if (i, j) in memo:
        return memo[(i, j)]
    # 基础情况
    if i == 0 or j == 0:
        return 0
    # 递推关系
    result = max(solve(i-1, j, memo), solve(i, j-1, memo))
    memo[(i, j)] = result
    return result

自底向上:递推表格法

从最小的子问题开始迭代构建解。这种方法消除了递归开销,并使空间优化成为可能。

适用场景: 当你需要通过只保留上一行或上一列来优化空间时。大多数经典 DP 问题(背包、LCS、编辑距离)在面试中最适合用自底向上的方式解决,因为迭代顺序很清晰。

五大核心 DP 模式

研究了数百道大厂常考的 DP 问题后,几乎所有问题都可以归入五个类别。掌握这些,你就能应对任何变体。

模式一:线性序列 DP

状态依赖于单个数组或字符串中的前面元素。

经典问题: 爬楼梯、打家劫舍、最大子数组和、最长递增子序列。

模板: dp[i] 表示考虑前 i 个元素时的答案。转移通常查看 dp[i-1]dp[i-2],或回扫寻找最优前驱。

面试技巧: 先问自己——位置 i 的答案是否只依赖于之前的位置?如果是,你就在线性 DP 的领域。

模式二:双序列 / 网格 DP

两个字符串、两个数组,或一个二维网格,状态由两个索引定义。

经典问题: 最长公共子序列、编辑距离、最小路径和、不同路径、交错字符串。

模板: dp[i][j] 表示字符串 A 的前 i 个字符和字符串 B 的前 j 个字符的答案。转移来自 dp[i-1][j]dp[i][j-1]dp[i-1][j-1]

空间优化: 几乎总是只需要上一行,可将空间从 O(mn) 降到 O(min(m,n))。

模式三:背包变体

在重量、体积或数量约束下选择物品,以最大化或最小化某个值。

经典问题: 0/1 背包、零钱兑换、分割等和子集、目标和、完全背包。

模板: dp[i][w] = 使用前 i 个物品、容量为 w 时的最优值。0/1 背包:dp[i][w] = max(dp[i-1][w], dp[i-1][w-weight[i]] + value[i])

关键区别: 0/1 背包反向遍历容量(每个物品只用一次),完全背包正向遍历(物品可复用)。

模式四:区间 DP

某个区间的答案依赖于将其拆分为更小区间的结果。

经典问题: 矩阵链乘法、戳气球、回文分割、石子游戏。

模板: dp[i][j] = 从索引 ij 的子数组的答案。枚举 ij 之间的分割点 k,合并结果。

面试技巧: 区间 DP 问题不太常见,但常出现在高级别面试中。如果问题涉及合并或拆分一个连续区间,就想到区间 DP。

模式五:状态机 DP

问题有明确的状态和定义好的转移,你需要跟踪每个状态下的最优值。

经典问题: 买卖股票的最佳时机(所有变体)、粉刷房子、解码方法含状态。

模板: 显式定义状态——以股票问题为例:持有已卖冷冻期。在每一步写出状态之间的转移。

万能解题五步框架

在每次面试中使用这个五步流程。它让你保持条理清晰,也帮助你与面试官有效沟通。

第一步:确认是 DP 问题。 寻找以下信号:问题求最优值(最小、最大、计数),选择影响后续选项,暴力解法会涉及指数级枚举。

第二步:定义状态。 问自己:在每个决策点,我需要哪些信息才能做出最优选择?这就是你 DP 数组的维度。

第三步:写出递推关系。 用更小的状态表达 dp[state]。先用数学公式写出,再编码。

第四步:确定基础情况。 什么是最简单的子问题?空字符串、零个物品、第一行和第一列——先填充这些。

第五步:确定迭代顺序。 确保计算 dp[state] 时,它依赖的所有状态都已经计算完成。对于自底向上,通常意味着从左到右、从上到下迭代。

常见的丢分错误

急于写代码。 DP 问题比其他类型需要更多的前期思考。在写代码之前至少花五分钟定义状态和递推关系。面试官会特别关注这种纪律性。

状态定义错误。 如果你的递推关系不够简洁,问题几乎总是出在状态定义上,而不是转移上。回头增加一个维度或重新定义状态的含义。

基础情况的边界错误。 这是隐形杀手。在扩展之前,始终用一个小例子(规模 0、1、2)验证你的基础情况。

忘记讨论空间优化。 即使面试官没有要求,提到你的 O(mn) 解法可以优化到 O(n) 表明你的成熟度和全局意识。

如何高效练习

盲目刷几百道随机 DP 题效率很低。应该按模式分类练习,每个模式类别做两到三题,确保你能从零开始写出解法而不依赖提示。然后进入混合练习阶段,不预先知道模式类型。

使用 AI 面试助手 可以模拟这种体验,实时出题并评估你的解题思路。这对 DP 尤其有价值,因为正确和错误的方法之间往往只有细微差别——对你的状态定义和递推关系获得即时反馈,能让你比独自练习更快地建立正确的直觉。

面试官真正看什么

资深面试官评估 DP 解法时,不仅仅检查正确性,还会评估:

  • 问题分解能力: 你能把复杂问题拆分为干净的子问题吗?
  • 沟通能力: 你是否在编码前解释了状态定义和递推关系?
  • 优化意识: 你能识别何时可以减少空间复杂度吗?
  • 边界处理: 你是否主动识别边界条件?
  • 复杂度分析: 你能从递推关系推导出时间和空间复杂度吗?

一个思路清晰的 O(n²) 解法,配上清楚的解释,往往比一个你说不清的背诵式 O(n log n) 解法得分更高。

例题详解:零钱兑换

题目: 给定面额为 [1, 5, 10, 25] 的硬币和目标金额 n,求凑出该金额所需的最少硬币数。

第一步: 这是 DP 问题——我们求最小值,每次选择(用哪种硬币)影响剩余金额。

第二步: 状态:dp[amount] = 凑出 amount 所需的最少硬币数。

第三步: 递推关系:dp[amount] = min(dp[amount - coin] + 1),对每种满足 amount - coin >= 0 的硬币。

第四步: 基础情况:dp[0] = 0(零金额需要零枚硬币)。

第五步: 从 1 到 n 遍历 amount,每个金额尝试每种硬币。

def coin_change(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for a in range(1, amount + 1):
        for coin in coins:
            if a - coin >= 0:
                dp[a] = min(dp[a], dp[a - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

这个完全背包变体的时间复杂度为 O(n × k),空间复杂度为 O(n),其中 k 是硬币种类数。在面试中,你应该解释这个分析,并讨论如果每种硬币只能用一次,问题会如何变化(转换为 0/1 背包,反向迭代)。

总结

动态规划是一种技能,而非天赋。每一个能在面试中稳定解出 DP 题目的工程师,都是通过有结构的练习达到的——而不是靠背答案。专注于五大核心模式,用五步框架保持条理清晰,练习大声解释你的思维过程。只要准备得当,DP 就能从你最大的弱点变成最可靠的竞争优势。


掌握你的职业发展方向: