算法——动态规划
动态规划(Dynamic Programming,DP)与递归常常可以一起使用。核心思想是:将大问题拆成小问题,先解决小问题,再根据小问题的解推导出大问题的解。关键是要找到状态定义和状态转移方程。
递归
递归是 DP 的基础——一个问题可以拆成若干个结构相同的子问题。比如爬楼梯:到第 n 级台阶,要么从 n-1 跨一步上来,要么从 n-2 跨两步上来。所以:
1 | f(n) = f(n-1) + f(n-2) |
用递归直接写:
1 | function climb(n) { |
但有大量重复计算——climb(5) 会计算 climb(3) 两次。DP 就是来消除这些重复的。
初始状态(Base Case)
递归需要终止条件,DP 需要初始状态。找初始状态就是问:问题最小到什么程度可以直接得出答案?
- 爬楼梯:
f(1) = 1,f(2) = 2 - 最大子序和:
dp[0] = nums[0],第一个元素的最大子序和就是它自己 - 斐波那契:
fib(0) = 0,fib(1) = 1
状态转移方程
这是 DP 的核心——当前状态怎么从前面的状态推导过来。写好方程,代码就是照着翻译。
| 问题 | 状态定义 | 转移方程 |
|---|---|---|
| 爬楼梯 | dp[i] 表示到第 i 阶的方法数 |
dp[i] = dp[i-1] + dp[i-2] |
| 最大子序和 | dp[i] 表示以 i 结尾的最大子序和 |
dp[i] = max(nums[i], dp[i-1] + nums[i]) |
| 斐波那契 | dp[i] 表示第 i 个斐波那契数 |
dp[i] = dp[i-1] + dp[i-2] |
爬楼梯完整实现
1 | function climbStairs(n) { |
最大子序和完整实现
1 | function maxSubArray(nums) { |
状态压缩(空间优化)
很多 DP 问题中,当前状态只依赖前几个状态,不需要保留整个 dp 数组,用几个变量滚动更新即可。
1 | // 爬楼梯空间优化:O(n) → O(1) |
1 | // 最大子序和空间优化 |
最优解
DP 求的是最优子结构——如果子问题是最优的,那么组合出来的父问题也是最优的。区分 DP 和贪心:
- 贪心:每一步选当前最优,不考虑未来
- DP:枚举所有可能的状态,通过状态转移方程找到全局最优
比如爬楼梯求的是「方法数」,不是最优值,严格来说属于计数型 DP。求最优值的典型问题是最大子序和。
题目
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶?
思路:到第 n 阶 = 到第 n-1 阶再跨 1 步 + 到第 n-2 阶再跨 2 步,即斐波那契数列。
1 | function climbStairs(n) { |
最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
思路:对于每个位置 i,要么自己单独成子数组,要么加入前面的子数组。取较大的那个。
1 | function maxSubArray(nums) { |