一、动态规划的核心概念
动态规划(Dynamic Programming,简称 DP)是一种将复杂问题拆分为重叠子问题,通过存储子问题的解来避免重复计算 的算法思想。它的核心是 "状态转移" ------ 用子问题的最优解推导原问题的最优解,和贪心算法的 "局部最优→全局最优" 不同,DP 依赖子问题的递推关系,适用于有重叠子问题和最优子结构的场景。
动态规划的三大核心要素
- 状态定义 :确定
dp[i]表示什么含义(这是 DP 的灵魂,定义错了后续全错)。 - 状态转移方程 :建立
dp[i]和dp[i-1]、dp[i-2]等前面状态的关系(递推公式)。 - 初始化与边界条件 :确定
dp数组的初始值(比如dp[0]、dp[1]),避免递推时越界。
动态规划五部曲(解题方法论)
- 确定 dp 数组及下标的含义
- 确定递推公式
- dp 数组如何初始化
- 确定遍历顺序
- 举例推导 dp 数组(验证是否正确)
二、509. 斐波那契数
题目描述
斐波那契数,通常用 F(n) 表示,形成的序列称为斐波那契数列。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。即:F(0) = 0,F(1) = 1``F(n) = F(n - 1) + F(n - 2),其中 n > 1给你 n,请计算 F(n)。
解题思路(动态规划五部曲)
- 确定 dp 数组及下标的含义
dp[i]表示第i个斐波那契数的值。 - 确定递推公式 题目直接给出:
dp[i] = dp[i-1] + dp[i-2] - dp 数组初始化
dp[0] = 0,dp[1] = 1 - 确定遍历顺序 从
2到n顺序遍历(因为计算dp[i]需要前面的dp[i-1]和dp[i-2])。 - 举例推导 dp 数组 当
n=5时:dp[0]=0, dp[1]=1, dp[2]=1, dp[3]=2, dp[4]=3, dp[5]=5
完整 Java 代码
java
class Solution {
public int fib(int n) {
// 边界条件:n=0或1直接返回
if (n <= 1) {
return n;
}
// 1. 定义dp数组
int[] dp = new int[n + 1];
// 2. 初始化
dp[0] = 0;
dp[1] = 1;
// 3. 遍历顺序:从2到n
for (int i = 2; i <= n; i++) {
// 4. 递推公式
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
优化(空间复杂度 O (1))
由于 dp[i] 只依赖前两个值,无需存储整个数组,用两个变量滚动更新即可:
java
class Solution {
public int fib(int n) {
if (n <= 1) return n;
int a = 0, b = 1;
for (int i = 2; i <= n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
}
复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:优化前
O(n),优化后O(1)
三、70. 爬楼梯
题目描述
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
解题思路(动态规划五部曲)
- 确定 dp 数组及下标的含义
dp[i]表示爬到第i阶楼梯的不同方法数。 - 确定递推公式 要爬到第
i阶,有两种选择:- 从第
i-1阶爬1步上来 - 从第
i-2阶爬2步上来因此递推公式:dp[i] = dp[i-1] + dp[i-2]
- 从第
- dp 数组初始化
dp[1] = 1(1 阶楼梯只有 1 种方法:爬 1 步)dp[2] = 2(2 阶楼梯有 2 种方法:1+1 或 2)
- 确定遍历顺序 从
3到n顺序遍历。 - 举例推导 dp 数组 当
n=5时:dp[1]=1, dp[2]=2, dp[3]=3, dp[4]=5, dp[5]=8
注意:本题本质就是斐波那契数列的变形,区别仅在于初始化值不同。
完整 Java 代码
java
class Solution {
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];
}
}
优化(空间复杂度 O (1))
java
class Solution {
public int climbStairs(int n) {
if (n <= 2) return n;
int a = 1, b = 2;
for (int i = 3; i <= n; i++) {
int c = a + b;
a = b;
b = c;
}
return b;
}
}
复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:优化前
O(n),优化后O(1)
四、746. 使用最小花费爬楼梯
题目描述
给你一个整数数组 cost,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬 1 个或者 2 个台阶。你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
解题思路(动态规划五部曲)
- 确定 dp 数组及下标的含义
dp[i]表示到达第i阶台阶时的最低花费。 - 确定递推公式 到达第
i阶有两种途径:- 从
i-1阶爬上来,花费dp[i-1] + cost[i-1] - 从
i-2阶爬上来,花费dp[i-2] + cost[i-2]取两者最小值:dp[i] = Math.min(dp[i-1] + cost[i-1], dp[i-2] + cost[i-2])
- 从
- dp 数组初始化
dp[0] = 0(从第 0 阶开始,无需花费)dp[1] = 0(从第 1 阶开始,无需花费)
- 确定遍历顺序 从
2到n顺序遍历(n是楼梯顶部,对应cost数组长度)。 - 举例推导 dp 数组 假设
cost = [10, 15, 20],n=3(顶部是第 3 阶):dp[0] = 0,dp[1] = 0dp[2] = min(0+10, 0+15) = 10dp[3] = min(10+15, 0+20) = 20最终答案是20
完整 Java 代码
java
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int[] dp = new int[n + 1];
// 初始化:从0或1阶开始,花费为0
dp[0] = 0;
dp[1] = 0;
// 遍历顺序:从2到n
for (int i = 2; i <= n; i++) {
dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
return dp[n];
}
}
优化(空间复杂度 O (1))
java
class Solution {
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
int a = 0, b = 0;
for (int i = 2; i <= n; i++) {
int c = Math.min(b + cost[i-1], a + cost[i-2]);
a = b;
b = c;
}
return b;
}
}
复杂度分析
- 时间复杂度:
O(n) - 空间复杂度:优化前
O(n),优化后O(1)
总结
- 动态规划的核心:状态定义 + 状态转移方程,入门题的状态定义都比较直观,重点是找到递推关系。
- 三道题的关联:爬楼梯是斐波那契的变形,最小花费爬楼梯是爬楼梯的 "加权版",递推逻辑一脉相承。
- 优化技巧 :当
dp[i]只依赖前k个状态时,可使用滚动变量代替数组,降低空间复杂度。