动态规划基础
动态规划(Dynamic Programming,DP)通过把原问题分解为相对简单的子问题,并保存子问题的解来避免重复计算,从而大幅提高效率。
核心思想
什么是动态规划?
动态规划的核心是将复杂问题分解为子问题,并存储子问题的解以避免重复计算。
与分治的区别:分治子问题相互独立;动态规划子问题有重叠。 与贪心的区别:贪心每步局部最优;动态规划考虑全局最优。
适用条件
- 最优子结构:问题的最优解包含子问题的最优解。
- 重叠子问题:子问题会被重复计算。
- 无后效性:前面的状态一旦确定,不受后续决策影响。
解题步骤
- 定义状态:明确
dp[i]的含义。 - 状态转移方程:找出状态之间的数学关系。
- 初始化:确定边界条件(如
dp[0])。 - 计算顺序:确保计算当前状态时,依赖的状态已完成。
经典入门:斐波那契与爬楼梯
1. 斐波那契数列
// 动态规划 - O(n) 时间,O(n) 空间
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 prev2 = 0, prev1 = 1;
for (int i = 2; i <= n; i++) {
int curr = prev1 + prev2;
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
2. 爬楼梯 (LeetCode 70)
每次爬 1 或 2 阶。到达第 阶的方法数 = 到达第 阶的方法数 + 到达第 阶的方法数。
打家劫舍系列 (House Robber)
不能偷相邻的房子。
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
练习
- LeetCode 70:爬楼梯
- LeetCode 198:打家劫舍
- LeetCode 746:使用最小花费爬楼梯
- LeetCode 509:斐波那契数