coding-interview-university动态规划专题:从入门到精通
你是否在面对动态规划(Dynamic Programming, DP)问题时感到无从下手?是否觉得递归解法效率低下却找不到优化方向?本文将系统拆解动态规划的核心原理、解题框架与实战技巧,带你从理论到实践全面掌握这一面试必备技能。读完本文,你将能够:
- 快速识别动态规划适用场景
- 熟练运用状态定义与转移方程设计技巧
- 掌握空间优化的常用策略
- 解决90%以上的经典DP面试题
动态规划本质:从暴力递归到智能递推
1.1 动态规划的核心思想
动态规划是一种通过将复杂问题分解为重叠子问题,并存储子问题解(即记忆化)来避免重复计算的优化技术。其本质是用空间换时间,将指数级时间复杂度优化为多项式级别。
1.2 动态规划三要素
| 要素 | 定义 | 作用 |
|---|---|---|
| 状态定义 | 描述问题在某一阶段的具体形态 | 建立问题与子问题的映射关系 |
| 转移方程 | 子问题之间的递推关系 | 实现从已知解推未知解的过程 |
| 边界条件 | 最小子问题的直接解 | 避免无限递归,确定递推起点 |
示例:斐波那契数列
- 状态定义:
dp[i]表示第i个斐波那契数 - 转移方程:
dp[i] = dp[i-1] + dp[i-2] - 边界条件:
dp[0] = 0, dp[1] = 1
动态规划解题四步法
2.1 步骤拆解
步骤1:问题建模与状态定义
状态定义是动态规划的核心,需要回答两个问题:
- 用哪些变量描述问题状态?
- 状态变量的取值范围是什么?
实战技巧:
- 从问题最后一步倒推
- 考虑是否需要多维状态(如二维DP数组)
- 状态维度尽可能精简
步骤2:转移方程推导
转移方程是状态之间的递推关系,常见推导方法:
- 分类讨论法(如0-1背包中的选与不选)
- 归纳法(从简单情况推广到一般情况)
- 状态压缩法(降维优化)
示例:最长递增子序列(LIS)
# 状态定义:dp[i]表示以nums[i]结尾的LIS长度
# 转移方程:dp[i] = max(dp[j] + 1) for j < i and nums[j] < nums[i]
def lengthOfLIS(nums):
if not nums: return 0
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
步骤3:边界条件处理
边界条件直接影响解的正确性,需注意:
- 初始状态的赋值
- 特殊情况的处理(如空输入)
- 数组越界问题
步骤4:空间优化
常见优化策略:
- 滚动数组(将二维降为一维)
- 变量替换(用单个变量代替数组)
- 状态压缩(如0-1背包的逆序遍历)
示例:0-1背包空间优化
# 优化前:二维数组
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
# 优化后:一维数组(逆序遍历避免覆盖)
for i in range(n):
for j in range(capacity, weight[i]-1, -1):
dp[j] = max(dp[j], dp[j-weight[i]] + value[i])
经典动态规划问题分类解析
3.1 线性DP
特点:状态按线性顺序递推,是最基础的DP类型。
3.1.1 爬楼梯问题
def climbStairs(n):
if n <= 2: return n
a, b = 1, 2
for _ in range(3, n+1):
a, b = b, a + b
return b
3.1.2 最大子数组和(Kadane算法)
def maxSubArray(nums):
dp = [0] * len(nums)
dp[0] = nums[0]
max_sum = dp[0]
for i in range(1, len(nums)):
dp[i] = max(nums[i], dp[i-1] + nums[i])
max_sum = max(max_sum, dp[i])
return max_sum
3.2 区间DP
特点:状态定义在区间上,通常用二维数组dp[i][j]表示区间[i,j]的最优解。
3.2.1 最长回文子序列
def longestPalindromeSubseq(s):
n = len(s)
dp = [[0]*n for _ in range(n)]
# 边界条件:长度为1的区间
for i in range(n-1, -1, -1):
dp[i][i] = 1
# 填充长度>1的区间
for j in range(i+1, n):
if s[i] == s[j]:
dp[i][j] = dp[i+1][j-1] + 2
else:
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
return dp[0][n-1]
3.3 背包问题
3.3.1 完全背包(物品可重复选择)
def coinChange(coins, amount):
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for j in range(coin, amount + 1):
dp[j] = min(dp[j], dp[j - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
3.4 状态压缩DP
3.4.1 打家劫舍问题
def rob(nums):
if not nums: return 0
n = len(nums)
if n == 1: return nums[0]
# dp[i] = max(dp[i-1], dp[i-2] + nums[i])
prev, curr = nums[0], max(nums[0], nums[1])
for i in range(2, n):
prev, curr = curr, max(curr, prev + nums[i])
return curr
动态规划优化技巧进阶
4.1 时间复杂度优化
4.1.1 单调队列优化
在某些问题中(如滑动窗口最大值),可通过维护单调队列将O(n^2)优化为O(n)。
4.1.2 矩阵快速幂
用于求解线性递推关系,将O(n)优化为O(log n)。以斐波那契数列为例:
def fib(n):
if n <= 1: return n
# 矩阵乘法
def multiply(a, b):
return [
[a[0][0]*b[0][0] + a[0][1]*b[1][0],
[a[0][0]*b[0][1] + a[0][1]*b[1][1]],
[a[1][0]*b[0][0] + a[1][1]*b[1][0],
[a[1][0]*b[0][1] + a[1][1]*b[1][1]]
]
# 矩阵快速幂
def matrix_pow(mat, power):
result = [[1,0],[0,1]] # 单位矩阵
while power > 0:
if power % 2 == 1:
result = multiply(result, mat)
mat = multiply(mat, mat)
power //= 2
return result
mat = [[1,1],[1,0]]
return matrix_pow(mat, n-1)[0][0]
4.2 空间优化策略对比
| 优化方法 | 适用场景 | 时间复杂度影响 | 空间复杂度优化 |
|---|---|---|---|
| 滚动数组 | 状态仅依赖前几行 | 不变 | O(n)→O(1) |
| 变量替换 | 状态仅依赖前一个值 | 不变 | O(n)→O(1) |
| 逆序遍历 | 0-1背包问题 | 不变 | O(nm)→O(m) |
动态规划面试解题模板
5.1 解题流程图
5.2 状态设计 checklist
- 是否包含所有必要信息?
- 是否可以进一步精简?
- 维度是否最低?
- 是否容易推导转移方程?
5.3 常见错误排查
- 状态定义不准确:导致子问题覆盖不全
- 转移方程遗漏情况:如边界条件处理不当
- 数组越界:循环范围错误
- 空间优化错误:覆盖了尚未使用的子问题解
- 初始化错误:未正确设置初始状态
实战训练:高频面试题精解
6.1 股票买卖问题(最多交易两次)
def maxProfit(prices):
if not prices: return 0
n = len(prices)
# 定义4种状态:
# buy1, sell1, buy2, sell2
buy1 = buy2 = -prices[0]
sell1 = sell2 = 0
for price in prices[1:]:
buy1 = max(buy1, -price)
sell1 = max(sell1, buy1 + price)
buy2 = max(buy2, sell1 - price)
sell2 = max(sell2, buy2 + price)
return max(sell1, sell2)
6.2 编辑距离问题
def minDistance(word1, word2):
m, n = len(word1), len(word2)
# dp[i][j]表示word1[:i]到word2[:j]的最小编辑距离
dp = [[0]*(n+1) for _ in range(m+1)]
# 边界条件
for i in range(m+1):
dp[i][0] = i
for j in range(n+1):
dp[0][j] = j
# 填充dp表
for i in range(1, m+1):
for j in range(1, n+1):
if word1[i-1] == word2[j-1]:
dp[i][j] = dp[i-1][j-1]
else:
dp[i][j] = min(
dp[i-1][j] + 1, # 删除
dp[i][j-1] + 1, # 插入
dp[i-1][j-1] + 1 # 替换
)
return dp[m][n]
总结与展望
动态规划作为算法面试中的重点和难点,需要通过大量练习培养问题拆解能力和状态设计直觉。掌握本文介绍的解题框架和优化技巧后,建议通过以下步骤进一步提升:
- 分类刷题:按线性DP、区间DP、背包问题等类别集中训练
- 一题多解:尝试不同的状态定义和解法优化
- 总结归纳:建立个人的DP问题模型库
- 模拟面试:限时解题,训练快速反应能力
动态规划不仅是面试必备技能,更是培养算法思维的重要途径。掌握动态规划后,你将能够以更高效的方式解决复杂问题,为未来的职业发展打下坚实基础。
练习资源推荐:
- LeetCode动态规划标签(中等难度20题+困难难度10题)
- 《算法导论》第15章动态规划
- 《Cracking the Coding Interview》第8章递归与动态规划
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



