1. 动态规划基础概念
动态规划是一种将复杂问题分解为更小的子问题,并存储子问题的解以避免重复计算的方法。其核心思想包含三个关键特征:
- 最优子结构:一个问题的最优解包含其子问题的最优解
- 重叠子问题:子问题会被重复计算多次
- 状态转移方程:如何从子问题的解推导出原问题的解
2. 入门基础题目详解
2.1 斐波那契数 (Fibonacci Number)
问题描述:计算第n个斐波那契数,其中:
- F(0) = 0, F(1) = 1
- F(n) = F(n-1) + F(n-2), n ≥ 2
解法一:暴力递归(非DP)
python
def fib_recursive(n):
if n <= 1:
return n
return fib_recursive(n-1) + fib_recursive(n-2)
# 时间复杂度:O(2^n) - 指数级,存在大量重复计算
解法二:动态规划(自顶向下 - 记忆化搜索)
python
def fib_memo(n, memo=None):
if memo is None:
memo = {}
if n in memo:
return memo[n]
if n <= 1:
return n
memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
return memo[n]
解法三:动态规划(自底向上 - 迭代)
python
def fib_dp(n):
if n <= 1:
return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
解法四:空间优化(滚动数组)
python
def fib_optimized(n):
if n <= 1:
return n
prev2, prev1 = 0, 1
for i in range(2, n + 1):
curr = prev1 + prev2
prev2, prev1 = prev1, curr
return prev1
Java实现:
java
public class Fibonacci {
// 方法1:基础DP
public int fibDP(int n) {
if (n <= 1) return n;
int[] dp = new int[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
// 方法2:空间优化
public int fibOptimized(int n) {
if (n <= 1) return n;
int prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
}
2.2 爬楼梯 (Climbing Stairs)
问题描述:每次可以爬1或2个台阶,有多少种不同的方法爬到第n阶?
状态定义
dp[i]:爬到第i阶楼梯的方法数
状态转移方程
dp[i] = dp[i-1] + dp[i-2]
- 从第i-1阶爬1步
- 从第i-2阶爬2步
Python实现
python
def climbStairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1
dp[2] = 2
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# 空间优化版本
def climbStairs_optimized(n):
if n <= 2:
return n
prev2, prev1 = 1, 2
for i in range(3, n + 1):
curr = prev1 + prev2
prev2, prev1 = prev1, curr
return prev1
Java实现
java
public class ClimbingStairs {
public int climbStairs(int n) {
if (n <= 2) return n;
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
public int climbStairsOptimized(int n) {
if (n <= 2) return n;
int prev2 = 1, prev1 = 2;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
}
2.3 使用最小花费爬楼梯 (Min Cost Climbing Stairs)
问题描述:每个台阶有一个cost[i],支付cost[i]后可以爬1或2步,求爬到顶部的最小花费。
状态定义
dp[i]:到达第i阶的最小花费
状态转移方程
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
注意:最后要到达顶部(第n阶之后)
Python实现
python
def minCostClimbingStairs(cost):
n = len(cost)
if n <= 1:
return 0
dp = [0] * n
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2, n):
dp[i] = min(dp[i-1], dp[i-2]) + cost[i]
# 到达顶部可以从倒数第一或倒数第二步直接到达
return min(dp[n-1], dp[n-2])
# 空间优化版本
def minCostClimbingStairs_optimized(cost):
n = len(cost)
if n <= 1:
return 0
prev2, prev1 = cost[0], cost[1]
for i in range(2, n):
curr = min(prev1, prev2) + cost[i]
prev2, prev1 = prev1, curr
return min(prev1, prev2)
Java实现
java
public class MinCostClimbingStairs {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
if (n <= 1) return 0;
int[] dp = new int[n];
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < n; i++) {
dp[i] = Math.min(dp[i-1], dp[i-2]) + cost[i];
}
return Math.min(dp[n-1], dp[n-2]);
}
public int minCostClimbingStairsOptimized(int[] cost) {
int n = cost.length;
if (n <= 1) return 0;
int prev2 = cost[0];
int prev1 = cost[1];
for (int i = 2; i < n; i++) {
int curr = Math.min(prev1, prev2) + cost[i];
prev2 = prev1;
prev1 = curr;
}
return Math.min(prev1, prev2);
}
}
2.4 不同路径 (Unique Paths)
问题描述:m×n网格,从左上角到右下角,每次只能向右或向下移动,求不同路径数。
状态定义
dp[i][j]:到达(i, j)位置的不同路径数
状态转移方程
dp[i][j] = dp[i-1][j] + dp[i][j-1]
- 从上方下来:dp[i-1][j]
- 从左方过来:dp[i][j-1]
Python实现
python
def uniquePaths(m, n):
# 创建DP数组
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
# 填充DP表
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]
# 空间优化版本(使用一维数组)
def uniquePaths_optimized(m, n):
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[n-1]
Java实现
java
public class UniquePaths {
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
// 初始化第一列
for (int i = 0; i < m; i++) {
dp[i][0] = 1;
}
// 初始化第一行
for (int j = 0; j < n; j++) {
dp[0][j] = 1;
}
// 填充DP表
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
// 空间优化版本
public int uniquePathsOptimized(int m, int n) {
int[] dp = new int[n];
// 初始化第一行
for (int j = 0; j < n; j++) {
dp[j] = 1;
}
// 从第二行开始计算
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[j] += dp[j-1];
}
}
return dp[n-1];
}
}
2.5 不同路径 II (Unique Paths with Obstacles)
问题描述:带障碍物的网格,障碍物处不能通过。
状态转移方程的修改
if obstacleGrid[i][j] == 1:
dp[i][j] = 0
else:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
Python实现
python
def uniquePathsWithObstacles(obstacleGrid):
m, n = len(obstacleGrid), len(obstacleGrid[0])
# 如果起点或终点有障碍物
if obstacleGrid[0][0] == 1 or obstacleGrid[m-1][n-1] == 1:
return 0
dp = [[0] * n for _ in range(m)]
dp[0][0] = 1
# 初始化第一列
for i in range(1, m):
if obstacleGrid[i][0] == 0:
dp[i][0] = dp[i-1][0]
# 初始化第一行
for j in range(1, n):
if obstacleGrid[0][j] == 0:
dp[0][j] = dp[0][j-1]
# 填充DP表
for i in range(1, m):
for j in range(1, n):
if obstacleGrid[i][j] == 0:
dp[i][j] = dp[i-1][j] + dp[i][j-1]
else:
dp[i][j] = 0
return dp[m-1][n-1]
# 空间优化版本
def uniquePathsWithObstacles_optimized(obstacleGrid):
m, n = len(obstacleGrid), len(obstacleGrid[0])
if obstacleGrid[0][0] == 1:
return 0
dp = [0] * n
dp[0] = 1
for i in range(m):
for j in range(n):
if obstacleGrid[i][j] == 1:
dp[j] = 0
elif j > 0:
dp[j] += dp[j-1]
return dp[n-1]
Java实现
java
public class UniquePathsWithObstacles {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
// 起点或终点有障碍物
if (obstacleGrid[0][0] == 1 || obstacleGrid[m-1][n-1] == 1) {
return 0;
}
int[][] dp = new int[m][n];
dp[0][0] = 1;
// 初始化第一列
for (int i = 1; i < m; i++) {
if (obstacleGrid[i][0] == 0) {
dp[i][0] = dp[i-1][0];
}
}
// 初始化第一行
for (int j = 1; j < n; j++) {
if (obstacleGrid[0][j] == 0) {
dp[0][j] = dp[0][j-1];
}
}
// 填充DP表
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
} else {
dp[i][j] = 0;
}
}
}
return dp[m-1][n-1];
}
// 空间优化版本
public int uniquePathsWithObstaclesOptimized(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if (obstacleGrid[0][0] == 1) {
return 0;
}
int[] dp = new int[n];
dp[0] = 1;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[j] = 0;
} else if (j > 0) {
dp[j] += dp[j-1];
}
}
}
return dp[n-1];
}
}
3. 动态规划解题模板总结
3.1 解题步骤
- 定义状态:确定dp数组的含义
- 确定初始条件:基础情况的值
- 写出状态转移方程:如何从已知状态推导新状态
- 确定遍历顺序:确保计算当前状态时依赖的状态已计算
- 确定返回值:dp数组的哪个元素是最终答案
3.2 空间优化技巧
- 滚动数组:当状态只依赖前几个状态时
- 降维:二维DP可优化为一维(如不同路径问题)
- 压缩状态:使用位运算等技巧
3.3 常见错误
- 忘记初始化:dp[0]、dp[1]等初始值
- 数组越界:访问dp[i-1]时i=0
- 遍历顺序错误:如背包问题的内外层循环
- 数据类型溢出:使用long代替int
4. 练习建议
4.1 顺序练习
- 先掌握基础的五道题
- 理解状态定义和转移方程的区别
- 练习空间优化版本
- 尝试自己推导状态转移方程
4.2 对比分析
- 比较斐波那契和爬楼梯的相似性
- 对比不同路径有无障碍物的区别
- 分析最小花费爬楼梯与普通爬楼梯的差异
4.3 扩展思考
- 如果爬楼梯可以爬1、2、3步怎么办?
- 不同路径问题如果允许斜向移动?
- 如果每个格子的移动有不同代价?
5. 复杂度分析
| 题目 | 时间复杂度 | 空间复杂度 | 优化后空间复杂度 |
|---|---|---|---|
| 斐波那契数 | O(n) | O(n) | O(1) |
| 爬楼梯 | O(n) | O(n) | O(1) |
| 最小花费爬楼梯 | O(n) | O(n) | O(1) |
| 不同路径 | O(m×n) | O(m×n) | O(n) |
| 不同路径II | O(m×n) | O(m×n) | O(n) |
6. 总结
入门基础的动态规划问题主要训练以下几个能力:
- 识别子问题:找到问题的最优子结构
- 定义状态:选择合适的dp数组维度
- 推导方程:建立状态间的数学关系
- 优化空间:理解如何减少空间复杂度
掌握这五道题后,你已经建立了动态规划的基本思维框架。接下来可以挑战更复杂的背包问题、序列DP等进阶题目。
关键点:动态规划的核心是"记住已经求过的解",避免重复计算。从最简单的斐波那契开始理解这个思想,逐步应用到更复杂的问题中。