动态规划面试真题解析
一、核心概念与高频考点
动态规划(DP)是面试中考察算法思维的核心题型,其核心思想是:
- 最优子结构:问题的最优解包含子问题的最优解。
- 重叠子问题:子问题被重复计算,需通过存储结果避免冗余。
- 状态转移方程:定义子问题间的关系,是解题关键。
高频问题类型:
- 序列型:如最长公共子序列(LCS)、最长上升子序列(LIS)。
- 双序列型:如编辑距离、字符串比对。
- 背包型:0-1背包、完全背包、多重背包。
- 矩阵型:如机器人路径、数字三角形。
- 区间型:如矩阵链乘、石子合并。
二、经典真题解析
1. 0-1背包问题
题目:给定物品重量 w[]
、价值 v[]
和背包容量 W
,求最大价值。
解析:
- 状态定义:
dp[i][j]
表示前i
个物品在容量j
时的最大价值。 - 状态转移:
- 不选第
i
个物品:dp[i][j] = dp[i-1][j]
。 - 选第
i
个物品:dp[i][j] = dp[i-1][j-w[i]] + v[i]
(需满足j ≥ w[i]
)。 - 综合:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])
。
- 不选第
- 初始化:
dp[0][j] = 0
(无物品时价值为0)。 - 空间优化:使用一维数组
dp[j]
,逆序更新以避免覆盖。
代码示例:
def knapsack_01(w, v, W):n = len(w)dp = [0] * (W + 1)for i in range(n):for j in range(W, w[i] - 1, -1):dp[j] = max(dp[j], dp[j - w[i]] + v[i])return dp[W]
2. 最长公共子序列(LCS)
题目:给定字符串 s1
和 s2
,求最长公共子序列长度。
解析:
- 状态定义:
dp[i][j]
表示s1[0..i-1]
和s2[0..j-1]
的LCS长度。 - 状态转移:
- 若
s1[i-1] == s2[j-1]
:dp[i][j] = dp[i-1][j-1] + 1
。 - 否则:
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
。
- 若
- 初始化:
dp[0][j] = dp[i][0] = 0
(空字符串的LCS为0)。
代码示例:
def lcs(s1, s2):m, n = len(s1), len(s2)dp = [[0] * (n + 1) for _ in range(m + 1)]for i in range(1, m + 1):for j in range(1, n + 1):if s1[i-1] == s2[j-1]:dp[i][j] = dp[i-1][j-1] + 1else:dp[i][j] = max(dp[i-1][j], dp[i][j-1])return dp[m][n]
3. 机器人路径问题(含障碍物)
题目:机器人从网格左上角到右下角,每次只能向右或向下移动,求路径数(含障碍物)。
解析:
- 状态定义:
dp[i][j]
表示到达(i,j)
的路径数。 - 状态转移:
- 若
(i,j)
无障碍物:dp[i][j] = dp[i-1][j] + dp[i][j-1]
。 - 否则:
dp[i][j] = 0
。
- 若
- 初始化:
- 第一行和第一列:若遇到障碍物,后续路径数为0。
dp[0][0] = 1
(起点无障碍物时)。
代码示例:
def unique_paths_with_obstacles(obstacle_grid):m, n = len(obstacle_grid), len(obstacle_grid[0])dp = [[0] * n for _ in range(m)]dp[0][0] = 1 if obstacle_grid[0][0] == 0 else 0# 初始化第一行for j in range(1, n):if obstacle_grid[0][j] == 0 and dp[0][j-1] == 1:dp[0][j] = 1# 初始化第一列for i in range(1, m):if obstacle[i][0] == 0 and dp[i-1][0] == 1:dp[i][0] = 1# 填充DP表for i in range(1, m):for j in range(1, n):if obstacle_grid[i][j] == 0:dp[i][j] = dp[i-1][j] + dp[i][j-1]return dp[m-1][n-1]
三、解题框架与优化技巧
- 通用解题框架:
- 分解问题:定义子问题(如
dp[i][j]
)。 - 找出递推关系:明确状态转移方程。
- 初始化边界条件:处理基础情况(如空字符串、容量为0)。
- 计算顺序:自底向上(迭代)或自顶向下(记忆化递归)。
- 分解问题:定义子问题(如
- 优化技巧:
- 空间优化:滚动数组(如背包问题中降维)。
- 状态压缩:用位运算或布尔数组减少空间。
- 决策单调性:优化时间复杂度(如四边形不等式)。
四、面试应对策略
- 判断是否为DP问题:
- 问题是否求最大值/最小值、可行性或方案数?
- 是否存在重叠子问题和最优子结构?
- 沟通思路:
- 先定义状态,再推导状态转移方程。
- 明确初始条件和计算顺序。
- 举例验证小规模输入(如
n=3
时的结果)。
- 避免常见错误:
- 忽略边界条件(如数组越界)。
- 状态定义模糊(如未明确
i
和j
的含义)。 - 更新顺序错误(如背包问题中正序更新导致重复计算)。
以下是动态规划的典型例题及其完整代码实现,涵盖斐波那契数列、背包问题、最长公共子序列、最小路径和、机器人路径计数等经典问题:
1. 斐波那契数列(Fibonacci Sequence)
题目:给定整数 n
,返回斐波那契数列的第 n
项(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)
)。
动态规划思路:用数组 dp
存储中间结果,避免递归重复计算。
代码:
def fibonacci(n):if n <= 1:return ndp = [0] * (n + 1)dp[1] = 1for i in range(2, n + 1):dp[i] = dp[i - 1] + dp[i - 2]return dp[n]print(fibonacci(10)) # 输出: 55
2. 0-1背包问题
题目:给定物品重量 weights
、价值 values
和背包容量 W
,求最大价值。
动态规划思路:dp[i][j]
表示前 i
个物品在容量 j
时的最大价值,状态转移方程为:
- 不选第
i
个物品:dp[i][j] = dp[i-1][j]
- 选第
i
个物品:dp[i][j] = dp[i-1][j-weights[i-1]] + values[i-1]
代码(空间优化版):
def knapsack_01(weights, values, W):n = len(weights)dp = [0] * (W + 1)for i in range(n):for j in range(W, weights[i] - 1, -1):dp[j] = max(dp[j], dp[j - weights[i]] + values[i])return dp[W]weights = [2, 3, 4, 5]
values = [3, 4, 5, 6]
W = 8
print(knapsack_01(weights, values, W)) # 输出: 10
3. 最长公共子序列(LCS)
题目:给定字符串 str1
和 str2
,求最长公共子序列的长度。
动态规划思路:dp[i][j]
表示 str1
前 i
个字符和 str2
前 j
个字符的 LCS 长度。
代码:
def longest_common_subsequence(str1, str2):m, n = len(str1), len(str2)dp = [[0] * (n + 1) for _ in range(m + 1)]for i in range(1, m + 1):for j in range(1, n + 1):if str1[i - 1] == str2[j - 1]:dp[i][j] = dp[i - 1][j - 1] + 1else:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])return dp[m][n]print(longest_common_subsequence("ABCDGH", "AEDFHR")) # 输出: 3
4. 最小路径和
题目:给定 m x n
网格,每个格子有非负整数,从左上角到右下角的最小路径和(只能向右或向下移动)。
动态规划思路:dp[i][j]
表示从起点到 (i,j)
的最小路径和。
代码:
def min_path_sum(grid):m, n = len(grid), len(grid[0])dp = [[0] * n for _ in range(m)]dp[0][0] = grid[0][0]for i in range(1, m):dp[i][0] = dp[i - 1][0] + grid[i][0]for j in range(1, n):dp[0][j] = dp[0][j - 1] + grid[0][j]for i in range(1, m):for j in range(1, n):dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]return dp[m - 1][n - 1]grid = [[1, 3, 1],[1, 5, 1],[4, 2, 1]
]
print(min_path_sum(grid)) # 输出: 7
5. 机器人路径计数(无障碍物)
题目:机器人从 m x n
网格的左上角出发,每次只能向右或向下移动,求到右下角的路径数。
动态规划思路:dp[i][j]
表示到 (i,j)
的路径数,状态转移方程为 dp[i][j] = dp[i-1][j] + dp[i][j-1]
。
代码(空间优化版):
def unique_paths(m, n):dp = [1] * nfor _ in range(1, m):for j in range(1, n):dp[j] += dp[j - 1]return dp[-1]print(unique_paths(3, 7)) # 输出: 28
6. 机器人路径计数(有障碍物)
题目:在 m x n
网格中,障碍物用 1
表示,求机器人到右下角的路径数。
动态规划思路:若 (i,j)
是障碍物,则 dp[i][j] = 0
;否则按无障碍物情况计算。
代码:
def unique_paths_with_obstacles(obstacle_grid):m, n = len(obstacle_grid), len(obstacle_grid[0])dp = [[0] * n for _ in range(m)]dp[0][0] = 1 if obstacle_grid[0][0] == 0 else 0for i in range(1, m):if obstacle_grid[i][0] == 0 and dp[i - 1][0] == 1:dp[i][0] = 1for j in range(1, n):if obstacle_grid[0][j] == 0 and dp[0][j - 1] == 1:dp[0][j] = 1for i in range(1, m):for j in range(1, n):if obstacle_grid[i][j] == 0:dp[i][j] = dp[i - 1][j] + dp[i][j - 1]return dp[m - 1][n - 1]obstacle_grid = [[0, 0, 0],[0, 1, 0],[0, 0, 0]
]
print(unique_paths_with_obstacles(obstacle_grid)) # 输出: 2
动态规划是一种通过将复杂问题分解为相互重叠的子问题,并存储子问题的解以避免重复计算,从而高效解决最优化问题的算法思想。其核心在于定义状态(如 dp[i][j]
表示前 i
个元素在条件 j
下的最优解)、确定状态转移方程(如递推关系 dp[i] = max(dp[i-1], dp[i-2]+val)
),并通过初始化边界条件和迭代填充状态表来逐步推导出最终解。典型应用场景包括斐波那契数列、背包问题、最长公共子序列、网格路径计数等,其优势在于将指数级时间复杂度优化为多项式级(如从 O(2ⁿ) 降至 O(n) 或 O(n²)),但需注意空间优化(如滚动数组)以减少内存消耗。