动态规划(DP)从入门到精通:原理详解与经典问题解析

1. 引言
动态规划(Dynamic Programming,简称DP)是算法设计与优化中的一种重要方法,广泛应用于计算机科学、人工智能、运筹学等领域。许多经典问题,如背包问题、最短路径问题、编辑距离等,都可以通过动态规划高效解决。
本文将系统介绍动态规划的核心思想、解题步骤,并通过多个经典案例(附Java代码实现)帮助读者掌握动态规划的解题技巧。
2. 动态规划的核心思想
动态规划适用于具有以下两个性质的问题:
-
重叠子问题(Overlapping Subproblems)
问题可以被分解为多个相似的子问题,且子问题会被重复计算。
示例 :斐波那契数列F(n) = F(n-1) + F(n-2)
,其中F(3)
可能被F(4)
和F(5)
多次计算。 -
最优子结构(Optimal Substructure)
问题的最优解可以由其子问题的最优解推导而来。
示例:最短路径问题中,A→C 的最短路径 = A→B 的最短路径 + B→C 的最短路径。
动态规划通过**记忆化存储(Memoization)或填表法(Tabulation)**避免重复计算,提高效率。
3. 动态规划的解题步骤
步骤1:定义状态
- 明确
dp[i]
或dp[i][j]
表示的含义。 - 示例 :在斐波那契数列中,
dp[i]
表示第i
个斐波那契数。
步骤2:状态转移方程
- 找出
dp[i]
与dp[i-1]
、dp[i-2]
等子问题的关系。 - 示例 :
dp[i] = dp[i-1] + dp[i-2]
(斐波那契数列)。
步骤3:初始化
- 设置初始条件,如
dp[0]
和dp[1]
的值。 - 示例 :
dp[0] = 0, dp[1] = 1
(斐波那契数列)。
步骤4:计算顺序
- 确定填表顺序(通常自底向上)。
- 示例 :计算
dp[2]
→dp[3]
→ ... →dp[n]
。
步骤5:返回结果
- 从
dp
表中提取最终答案。 - 示例 :
return dp[n]
(斐波那契数列)。
4. 经典动态规划问题
问题1:斐波那契数列(LeetCode 509)
问题描述 :计算第 n
个斐波那契数(F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2)
)。
Java实现
java
public int fib(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];
}
// 空间优化版(O(1)空间)
public int fibOptimized(int n) {
if (n <= 1) return n;
int prev1 = 1, prev2 = 0;
for (int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
问题2:爬楼梯(LeetCode 70)
问题描述 :每次可以爬1或2阶台阶,求爬到第 n
阶有多少种方法。
状态转移方程
dp[i] = dp[i-1] + dp[i-2]
(类似斐波那契数列)
Java实现
java
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 prev1 = 2, prev2 = 1;
for (int i = 3; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
问题3:0-1背包问题
问题描述 :给定物品重量 weights
和价值 values
,背包容量 W
,求能装入的最大价值(每个物品只能选或不选)。
状态转移方程
css
dp[i][w] = max(dp[i-1][w], values[i] + dp[i-1][w - weights[i]])
Java实现
java
public int knapsack(int W, int[] weights, int[] values) {
int n = weights.length;
int[][] dp = new int[n + 1][W + 1];
for (int i = 1; i <= n; i++) {
for (int w = 1; w <= W; w++) {
if (weights[i - 1] <= w) {
dp[i][w] = Math.max(
dp[i - 1][w],
values[i - 1] + dp[i - 1][w - weights[i - 1]]
);
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
return dp[n][W];
}
// 空间优化版(一维数组)
public int knapsackOptimized(int W, int[] weights, int[] values) {
int[] dp = new int[W + 1];
for (int i = 0; i < weights.length; i++) {
for (int w = W; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], values[i] + dp[w - weights[i]]);
}
}
return dp[W];
}
5. 动态规划的优化技巧
-
空间优化
- 很多二维DP可以优化为一维(如背包问题)。
-
状态压缩
- 用位运算表示状态(如旅行商问题TSP)。
-
记忆化搜索(Memoization)
- 自顶向下递归+缓存,避免重复计算。
-
滚动数组
- 只保留必要的状态(如斐波那契数列只需
prev1
和prev2
)。
- 只保留必要的状态(如斐波那契数列只需
6. 总结
动态规划是算法竞赛和面试中的高频考点,掌握其核心思想和经典问题解法至关重要。建议:
- 从简单问题入手(斐波那契、爬楼梯)。
- 理解状态转移方程的推导过程。
- 多练习LeetCode上的DP专题(如LIS、编辑距离、股票买卖问题)。
- 尝试优化空间复杂度。
希望本文能帮助你系统掌握动态规划!欢迎在评论区交流讨论。🚀