🧩 故事背景:金币搬运工的优化之路
假设你是一名金币搬运工,每天要把阶梯上的金币搬到仓库(阶梯共n级)。规则:
- 每次只能搬1或2级(防止闪腰)
- 目标:计算搬完所有金币的不同方式总数
- 噩梦场景:直接递归的灾难(见下图👇)
💡 问题核心:计算
f(10)需先算f(9)和f(8),但算f(9)时又重复计算f(8)→ 指数级重复(时间复杂度O(2^n))
⚙️ 一、DP核心思想:存档点破解重复困局
1. 三大黄金特征
| 特征 | 解释 | 搬金币示例 |
|---|---|---|
| 重叠子问题 | 子问题被重复计算 | f(8)被计算4次 |
| 最优子结构 | 大问题解由小问题最优解组成 | f(10)=f(9)+f(8) |
| 无后效性 | 当前决策不受后续决策影响 | 迈出1步后,剩余问题与之前无关 |
2. 两种破解姿势
- 备忘录法(自顶向下):准备笔记本记录已算过的台阶(递归+哈希表)
- DP表法(自底向上):从第1级开始逐级填表,避免递归
🧪 二、Java实战:四种解法完整代码
解法1:暴力递归(反面教材)
java
Copy
// 时间复杂度O(2^n) n=40时需计算1万亿次!
public int climbStairs(int n) {
if (n <= 2) return n;
return climbStairs(n - 1) + climbStairs(n - 2);
}
解法2:备忘录法(空间换时间)
java
Copy
// 时间复杂度O(n) 空间O(n)
public int climbMemo(int n) {
int[] memo = new int[n + 1]; // 备忘录数组
return dp(n, memo);
}
private int dp(int n, int[] memo) {
if (n <= 2) return n;
if (memo[n] != 0) return memo[n]; // 已计算则直接读档
memo[n] = dp(n - 1, memo) + dp(n - 2, memo); // 存档
return memo[n];
}
解法3:DP表法(推荐⭐)
java
Copy
// 时间复杂度O(n) 空间O(n)
public int climbDP(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];
}
解法4:DP空间优化(滚动存档)
java
Copy
// 时间复杂度O(n) 空间O(1)
public int climbOpt(int n) {
if (n <= 2) return n;
int a = 1, b = 2, c = 0;
for (int i = 3; i <= n; i++) {
c = a + b; // 当前状态=前两状态之和
a = b; // 滚动更新存档
b = c;
}
return c;
}
📊 性能对比表:
方法 时间复杂度 空间复杂度 适用场景 暴力递归 O(2^n) O(n) ❌ 永远别用 备忘录法 O(n) O(n) ✅ 树形问题 DP表法 O(n) O(n) ✅ 常规一维DP 滚动存档 O(n) O(1) ✅ 斐波那契变种
🔍 三、DP灵魂:状态转移方程
在搬金币问题中,我们总结出:
dp[i] = dp[i-1] + dp[i-2]
如何自己写出状态方程?三步法:
- 定义状态:
dp[i]表示i级台阶的走法数 - 思考选择:最后一步是走1阶还是2阶?
- 关联子问题:走1阶→剩余i-1阶;走2阶→剩余i-2阶
🌰 案例扩展:若允许走1/2/3阶?
方程变为:dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
🏰 四、多维DP实战:国王挖金矿
问题:10个工人挖5座金矿,每座金矿需不同人数,求最大收益
1. 状态定义
dp[i][w] = 用w个工人挖前i座矿的最大收益
2. 状态转移方程
java
Copy
// 两种情况:挖第i座矿 或 不挖
dp[i][w] = max(
dp[i-1][w], // 不挖i矿→沿用前i-1座矿收益
dp[i-1][w - need[i]] + gold[i] // 挖i矿→剩余工人挖前i-1座
);
3. Java代码实现
java
Copy
public int maxGold(int workers, int[] gold, int[] need) {
int n = gold.length;
int[][] dp = new int[n + 1][workers + 1];
for (int i = 1; i <= n; i++) {
for (int w = 0; w <= workers; w++) {
if (w < need[i-1]) {
dp[i][w] = dp[i-1][w]; // 工人不够挖当前矿
} else {
int notDig = dp[i-1][w];
int dig = dp[i-1][w - need[i-1]] + gold[i-1];
dp[i][w] = Math.max(notDig, dig);
}
}
}
return dp[n][workers];
}
执行过程可视化(工人=10,金矿收益=[500,400,350,300,200],需人力=[5,5,3,4,3]):
| 金矿数\人数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 1座矿 | 0 | 0 | 0 | 0 | 0 | 500 | 500 | 500 | 500 | 500 | 500 |
| 2座矿 | 0 | 0 | 0 | 0 | 0 | 500 | 500 | 500 | 500 | 500 | 900 |
| 3座矿 | 0 | 0 | 0 | 350 | 350 | 500 | 500 | 500 | 850 | 850 | 900 |
| 最终解→ dp[5][10]=900 |
💎 五、DP解题框架:六步通关法
-
判题型:求最值/计数/可行性+重叠子问题→想DP
-
定状态:定义
dp[i]或dp[i][j]的含义 -
建方程:思考如何用
dp[<i]推导dp[i] -
初始化:设置起点值(如
dp[0]=1) -
定方向:自底向上(推荐)or自顶向下
-
空间优化:用滚动数组/变量压缩空间
🚀 高频DP题型:
- 一维:爬楼梯、打家劫舍、单词拆分
- 二维:最长公共子序列、编辑距离、背包问题
- 树形:二叉树偷钱、监控二叉树
💡 终极总结:DP思维导图
面试速记口诀:
重叠子,最优子,状态转移要搞清。
暴力递归重复多,备忘录里记分明。
自底向上效率高,滚动数组更聪明!
理解后尝试用DP解决LeetCode 70题(爬楼梯)和 322题(零钱兑换),体验存档点破解重复计算的魔力。掌握动态规划,算法面试的50%难题将迎刃而解!