LeetCode-Py 动态规划专题:从基础到进阶全面解析
引言:为什么动态规划是算法面试的必考重点?
还在为动态规划题目感到头疼吗?面对复杂的递推关系和状态转移方程无从下手?本文将带你系统掌握动态规划的核心思想和解题技巧,从基础概念到高级应用,一站式解决你的动态规划学习难题!
通过本文,你将获得:
- ✅ 动态规划三大特性的深度理解
- ✅ 0-1背包、完全背包等经典模型的完整解析
- ✅ 区间DP、树形DP等高级技巧的实战应用
- ✅ 20+道LeetCode经典题目的详细题解
- ✅ 空间优化和状态压缩的实用技巧
一、动态规划基础概念
1.1 什么是动态规划?
动态规划(Dynamic Programming,DP) 是一种求解多阶段决策过程最优化问题的方法。其核心思想是通过把原问题分解为相对简单的子问题,先求解子问题,再由子问题的解得到原问题的解。
1.2 动态规划的三大特性
1.2.1 最优子结构(Optimal Substructure)
一个问题的最优解包含其子问题的最优解。这意味着我们可以通过组合子问题的最优解来构造原问题的最优解。
1.2.2 重叠子问题(Overlapping Subproblems)
在求解过程中,相同的子问题会被多次重复计算。动态规划通过记忆化存储避免重复计算。
1.2.3 无后效性(No Aftereffect)
一旦某个状态确定,它就不会再受后续决策的影响。当前状态只与之前的状态有关。
1.3 动态规划解题五步法
| 步骤 | 描述 | 关键点 |
|---|---|---|
| 1. 划分阶段 | 将问题分解为若干阶段 | 确定阶段顺序 |
| 2. 定义状态 | 选择合适的状态变量 | 状态要满足无后效性 |
| 3. 状态转移 | 建立状态转移方程 | 核心递推关系 |
| 4. 初始条件 | 确定边界状态的值 | 基础case处理 |
| 5. 计算顺序 | 确定状态计算顺序 | 自底向上或自顶向下 |
二、基础动态规划问题
2.1 斐波那契数列
问题描述:计算第n个斐波那契数。
def fib(n: int) -> int:
if n <= 1:
return n
dp = [0] * (n + 1)
dp[0], dp[1] = 0, 1
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
复杂度分析:
- 时间复杂度:O(n)
- 空间复杂度:O(n)(可优化到O(1))
2.2 爬楼梯问题
问题描述:每次可以爬1或2个台阶,计算爬到n阶的方法数。
def climbStairs(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]
2.3 不同路径
问题描述:机器人从左上角到右下角的路径数。
def uniquePaths(m: int, n: int) -> int:
dp = [[0] * n for _ in range(m)]
# 初始化第一行和第一列
for i in range(m):
dp[i][0] = 1
for j in range(n):
dp[0][j] = 1
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
return dp[m - 1][n - 1]
三、背包问题专题
3.1 0-1背包问题
问题描述:有n件物品,第i件物品重量为weight[i],价值为value[i],背包容量为W,求最大价值。
3.1.1 二维DP解法
def zeroOnePack(weight: list, value: list, W: int) -> int:
n = len(weight)
dp = [[0] * (W + 1) for _ in range(n + 1)]
for i in range(1, n + 1):
for w in range(W + 1):
if w < weight[i - 1]:
dp[i][w] = dp[i - 1][w]
else:
dp[i][w] = max(dp[i - 1][w],
dp[i - 1][w - weight[i - 1]] + value[i - 1])
return dp[n][W]
3.1.2 一维空间优化
def zeroOnePackOptimized(weight: list, value: list, W: int) -> int:
n = len(weight)
dp = [0] * (W + 1)
for i in range(n):
for w in range(W, weight[i] - 1, -1):
dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
return dp[W]
3.2 完全背包问题
问题描述:每种物品有无限个,其他条件同0-1背包。
def completePack(weight: list, value: list, W: int) -> int:
n = len(weight)
dp = [0] * (W + 1)
for i in range(n):
for w in range(weight[i], W + 1):
dp[w] = max(dp[w], dp[w - weight[i]] + value[i])
return dp[W]
3.3 多重背包问题
问题描述:每种物品有有限个,其他条件同0-1背包。
def multiplePack(weight: list, value: list, count: list, W: int) -> int:
n = len(weight)
dp = [0] * (W + 1)
for i in range(n):
# 二进制优化
k = 1
while k <= count[i]:
for w in range(W, k * weight[i] - 1, -1):
dp[w] = max(dp[w], dp[w - k * weight[i]] + k * value[i])
count[i] -= k
k *= 2
if count[i] > 0:
for w in range(W, count[i] * weight[i] - 1, -1):
dp[w] = max(dp[w],
dp[w - count[i] * weight[i]] + count[i] * value[i])
return dp[W]
3.4 背包问题应用实例
3.4.1 分割等和子集
def canPartition(nums: list) -> bool:
total = sum(nums)
if total % 2 != 0:
return False
target = total // 2
dp = [False] * (target + 1)
dp[0] = True
for num in nums:
for j in range(target, num - 1, -1):
dp[j] = dp[j] or dp[j - num]
return dp[target]
3.4.2 零钱兑换
def coinChange(coins: list, amount: int) -> int:
dp = [float('inf')] * (amount + 1)
dp[0] = 0
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
return dp[amount] if dp[amount] != float('inf') else -1
四、区间动态规划
4.1 区间DP基本框架
def intervalDP(n: int, cost: list) -> int:
dp = [[0] * n for _ in range(n)]
# 初始化长度为1的区间
for i in range(n):
dp[i][i] = 0 # 根据具体问题调整
# 枚举区间长度
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
# 枚举分割点
for k in range(i, j):
dp[i][j] = min(dp[i][j],
dp[i][k] + dp[k + 1][j] + cost[i][j])
return dp[0][n - 1]
4.2 最长回文子序列
def longestPalindromeSubseq(s: str) -> int:
n = len(s)
dp = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 1
for i in range(n - 1, -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]
4.3 戳气球问题
def maxCoins(nums: list) -> int:
n = len(nums)
# 添加虚拟气球
arr = [1] + nums + [1]
m = len(arr)
dp = [[0] * m for _ in range(m)]
for length in range(3, m + 1):
for i in range(0, m - length + 1):
j = i + length - 1
for k in range(i + 1, j):
dp[i][j] = max(dp[i][j],
dp[i][k] + dp[k][j] + arr[i] * arr[k] * arr[j])
return dp[0][m - 1]
五、树形动态规划
5.1 二叉树中的最大路径和
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
def maxPathSum(root: TreeNode) -> int:
max_sum = float('-inf')
def dfs(node):
nonlocal max_sum
if not node:
return 0
left_max = max(0, dfs(node.left))
right_max = max(0, dfs(node.right))
# 当前节点作为路径转折点
current_sum = node.val + left_max + right_max
max_sum = max(max_sum, current_sum)
# 返回当前节点作为路径一部分的最大值
return node.val + max(left_max, right_max)
dfs(root)
return max_sum
5.2 打家劫舍III
def rob(root: TreeNode) -> int:
def dfs(node):
if not node:
return (0, 0) # (偷当前节点的最大值, 不偷当前节点的最大值)
left = dfs(node.left)
right = dfs(node.right)
# 偷当前节点,则不能偷子节点
rob_current = node.val + left[1] + right[1]
# 不偷当前节点,可以偷或不偷子节点
not_rob_current = max(left[0], left[1]) + max(right[0], right[1])
return (rob_current, not_rob_current)
result = dfs(root)
return max(result[0], result[1])
六、状态压缩动态规划
6.1 旅行商问题(TSP)
def tsp(graph: list) -> int:
n = len(graph)
# dp[mask][i] 表示访问过mask中的城市,当前在城市i的最小代价
dp = [[float('inf')] * n for _ in range(1 << n)]
dp[1][0] = 0 # 从城市0开始
for mask in range(1 << n):
for i in range(n):
if not (mask & (1 << i)):
continue
for j in range(n):
if mask & (1 << j):
continue
new_mask = mask | (1 << j)
dp[new_mask][j] = min(dp[new_mask][j],
dp[mask][i] + graph[i][j])
# 返回起点并形成环路
return min(dp[(1 << n) - 1][i] + graph[i][0] for i in range(n))
6.2 状态压缩DP优化技巧
七、动态规划优化技巧
7.1 斜率优化
对于形如 $dp[i] = min_{j<i}{dp[j] + f(i, j)}$ 的DP方程,当函数f具有特定性质时,可以使用单调队列优化。
def slopeOptimization(n: int, f: callable) -> int:
dp = [0] * (n + 1)
q = deque([0]) # 存储决策点
for i in range(1, n + 1):
# 维护队列单调性
while len(q) >= 2 and \
calcSlope(q[0], q[1]) <= f(i):
q.popleft()
j = q[0]
dp[i] = dp[j] + cost(j, i)
# 将i加入决策集合
while len(q) >= 2 and \
calcSlope(q[-2], q[-1]) >= calcSlope(q[-1], i):
q.pop()
q.append(i)
return dp[n]
7.2 四边形不等式优化
对于满足四边形不等式的DP问题,可以优化决策单调性。
def quadrangleInequality(n: int) -> int:
dp = [[0] * n for _ in range(n)]
# K[i][j] 记录最优决策点
K = [[0] * n for _ in range(n)]
for i in range(n):
dp[i][i] = 0
K[i][i] = i
for length in range(2, n + 1):
for i in range(n - length + 1):
j = i + length - 1
dp[i][j] = float('inf')
# 利用四边形不等式缩小决策范围
left = K[i][j - 1] if i <= j - 1 else i
right = K[i + 1][j] if i + 1 <= j else j
for k in range(left, right + 1):
current = dp[i][k] + dp[k + 1][j] + cost(i, j)
if current < dp[i][j]:
dp[i][j] = current
K[i][j] = k
return dp[0][n - 1]
八、实战训练计划
8.1 初级训练(掌握基础)
| 题目 | 难度 | 核心考点 |
|---|---|---|
| 509. 斐波那契数 | 简单 | 基础DP |
| 70. 爬楼梯 | 简单 | 基础DP |
| 198. 打家劫舍 | 中等 | 序列DP |
| 121. 买卖股票的最佳时机 | 简单 | 状态机DP |
8.2 中级训练(掌握经典模型)
| 题目 | 难度 | 核心考点 |
|---|---|---|
| 322. 零钱兑换 | 中等 | 完全背包 |
| 416. 分割等和子集 | 中等 | 0-1背包 |
| 300. 最长递增子序列 | 中等 | 序列DP |
| 1143. 最长公共子序列 | 中等 | 双序列DP |
8.3 高级训练(掌握优化技巧)
| 题目 | 难度 | 核心考点 |
|---|---|---|
| 312. 戳气球 | 困难 | 区间DP |
| 154. 寻找旋转排序数组中的最小值 II | 困难 | 二分+DP |
| 887. 鸡蛋掉落 | 困难 | 决策优化 |
| 10. 正则表达式匹配 | 困难 | 字符串DP |
九、常见误区与调试技巧
9.1 常见错误
- 状态定义不准确:状态应该包含解决问题的所有必要信息
- 边界条件处理不当:特别注意数组越界和初始状态
- 状态转移方程错误:仔细验证递推关系的正确性
- 计算顺序错误:确保依赖的状态已经计算完成
9.2 调试技巧
def debugDP(dp: list, name: str = "DP Table"):
print(f"=== {name} ===")
for i, row in enumerate(dp):
print(f"dp[{i}]: {row}")
print()
9.3 测试用例设计
def testDP():
test_cases = [
# (输入, 期望输出)
([1, 2, 3, 4], 10),
([], 0),
([5], 5),
# 边界测试用例
([0] * 1000, 0),
]
for i, (input_data, expected) in enumerate(test_cases):
result = solution(input_data)
assert result == expected, f"Case {i} failed: {result} != {expected}"
print(f"Case {i} passed")
十、总结与进阶学习
10.1 动态规划学习路线
10.2 推荐学习资源
-
经典书籍:
- 《算法导论》动态规划章节
- 《算法竞赛入门经典》DP专题
- 《背包九讲》
-
在线资源:
- LeetCode动态规划专题
- 各大OJ的DP训练集
- 算法竞赛培训材料
-
实践建议:
- 每天至少完成2道DP题目
- 定期复习经典模型
- 参与算法竞赛锻炼实战能力
结语
动态规划是算法领域的核心内容,也是面试中的高频考点。通过系统学习和大量练习,掌握动态规划不仅能够提升算法能力,更能培养抽象思维和问题分解能力。希望本文能够为你提供清晰的学习路径和实用的解题技巧,助你在动态规划的学习道路上稳步前进!
下一步行动建议:
- 从基础题目开始,逐步提升难度
- 重点掌握背包、区间、树形等经典模型
- 注重理解而非记忆,掌握思想精髓
- 坚持练习,量变引起质变
祝你学习顺利,早日成为动态规划高手!
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



