动态规划(DP)之“搬金币的智慧”故事

70 阅读5分钟

🧩 ​​故事背景:金币搬运工的优化之路​

假设你是一名金币搬运工,每天要把阶梯上的金币搬到仓库(阶梯共n级)。规则:

  1. ​每次只能搬1或2级​​(防止闪腰)
  2. ​目标​​:计算搬完所有金币的​​不同方式总数​
  3. ​噩梦场景​​:直接递归的灾难(见下图👇)

image.png

💡 ​​问题核心​​:计算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]

​如何自己写出状态方程?三步法​​:

  1. ​定义状态​​:dp[i]表示i级台阶的走法数
  2. ​思考选择​​:最后一步是走1阶还是2阶?
  3. ​关联子问题​​:走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]):

​金矿数\人数​012345678910
​1座矿​00000500500500500500500
​2座矿​00000500500500500500900
​3座矿​000350350500500500850850900
​最终解​​→ ​​dp[5][10]=900​

💎 ​​五、DP解题框架:六步通关法​

  1. ​判题型​​:求最值/计数/可行性+重叠子问题→想DP

  2. ​定状态​​:定义dp[i]dp[i][j]的含义

  3. ​建方程​​:思考如何用dp[<i]推导dp[i]

  4. ​初始化​​:设置起点值(如dp[0]=1

  5. ​定方向​​:自底向上(推荐)or自顶向下

  6. ​空间优化​​:用滚动数组/变量压缩空间

🚀 ​​高频DP题型​​:

  • 一维:爬楼梯、打家劫舍、单词拆分
  • 二维:最长公共子序列、编辑距离、背包问题
  • 树形:二叉树偷钱、监控二叉树

💡 ​​终极总结:DP思维导图​

image.png

​面试速记口诀​​:

重叠子,最优子,状态转移要搞清。
暴力递归重复多,备忘录里记分明。
自底向上效率高,滚动数组更聪明!

理解后尝试用DP解决LeetCode 70题(爬楼梯)和 322题(零钱兑换),体验存档点破解重复计算的魔力。掌握动态规划,算法面试的50%难题将迎刃而解!