动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为更小的子问题,并利用子问题的解来高效解决原问题的算法设计方法。它通过存储中间结果避免重复计算,显著优化时间复杂度。
动态规划的核心思想
- 重叠子问题 (Overlapping Subproblems)
问题可以被分解为多个重复出现的子问题。例如,计算斐波那契数列时,fib(5)
需要fib(4)
和fib(3)
,而fib(4)
又需要fib(3)
和fib(2)
,子问题被多次重复计算。 - 最优子结构 (Optimal Substructure)
问题的最优解可以通过其子问题的最优解组合得到。例如,最短路径问题中,若A→B→C
是A→C
的最短路径,则A→B
和B→C
也必须是各自段的最短路径。
动态规划的两种实现方式
1. 自顶向下(记忆化递归)
-
从原问题出发,递归分解为子问题,并用**缓存(记忆化)**存储已计算的子问题结果。
-
示例(斐波那契数列) :
const memo = new Map(); function fib(n) { if (n <= 1) return n; if (memo.has(n)) return memo.get(n); // 直接使用缓存 const res = fib(n - 1) + fib(n - 2); memo.set(n, res); // 缓存结果 return res; }
2. 自底向上(迭代填表)
-
从最小的子问题开始,逐步迭代求解并填充表格,直到解决原问题。
-
示例(斐波那契数列) :
function fib(n) { const dp = [0, 1]; for (let i = 2; i <= n; i++) { dp[i] = dp[i - 1] + dp[i - 2]; } return dp[n]; }
动态规划的典型步骤
-
定义状态
明确dp[i]
或dp[i][j]
表示的含义(如:dp[i]
表示前i
项的最优解)。 -
建立状态转移方程
找到子问题之间的关系。例如,爬楼梯问题:dp[i]=dp[i−1]+dp[i−2]dp[i]=dp[i−1]+dp[i−2]
(到达第
i
阶的方式数 = 从第i-1
阶走一步 + 从第i-2
阶走两步) -
初始化边界条件
设置初始值(如dp[0] = 1
,dp[1] = 1
)。 -
确定计算顺序
自底向上时,需保证计算dp[i]
时所需的子问题已解。 -
优化空间复杂度(可选)
若状态仅依赖前几个子问题,可压缩存储空间。例如,斐波那契数列只需保存前两个状态。
动态规划的经典问题
问题名称 | 状态定义 | 状态转移方程 |
---|---|---|
斐波那契数列 | dp[i] 表示第 i 项的值 | dp[i] = dp[i-1] + dp[i-2] |
背包问题 | dp[i][w] 表示前 i 个物品、容量 w 的最大价值 | dp[i][w] = max(dp[i-1][w], dp[i-1][w-w_i] + v_i) |
最长公共子序列 (LCS) | dp[i][j] 表示 s1[0..i] 和 s2[0..j] 的 LCS 长度 | if (s1[i] == s2[j]) dp[i][j] = dp[i-1][j-1] + 1 else dp[i][j] = max(dp[i-1][j], dp[i][j-1]) |
最短路径 (Floyd-Warshall) | dp[k][i][j] 表示经过前 k 个节点的 i→j 最短路径 | dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j]) |
动态规划 vs. 其他算法
方法 | 特点 |
---|---|
分治法 | 子问题独立且不重叠(如归并排序),无记忆化。 |
贪心算法 | 每一步选择当前局部最优,无法保证全局最优(如 Dijkstra 最短路径)。 |
动态规划 | 通过子问题组合得到全局最优解,需满足最优子结构。 |
适用场景
- 最优化问题(最大/最小值、最长/最短路径等)。
- 问题可分解为重叠子问题(如斐波那契、背包问题)。
- 需利用历史状态避免重复计算(如股票买卖问题)。
代码示例(爬楼梯问题)
javascript
function climbStairs(n) {
if (n <= 2) return n;
let a = 1, b = 2; // 仅保存前两个状态,空间复杂度 O(1)
for (let i = 3; i <= n; i++) {
[a, b] = [b, a + b]; // 状态转移
}
return b;
}
总结
动态规划通过状态定义和状态转移方程,将复杂问题转化为可管理的子问题。关键在于识别重叠子问题和最优子结构,并合理设计存储与计算顺序。掌握动态规划,能高效解决许多看似棘手的算法难题。