【Hot100】10.动态规划

这一类的核心思想是:将复杂问题分解为重叠子问题,通过记录子问题的解(状态)来避免重复计算

  • 解题四步曲
    1. 定义状态 (dp 数组)dp[i] 代表什么含义?(例如:爬到第 i 阶的方法数、前 i 个元素的最大和)。
    2. 状态转移方程:如何从已知状态推导出未知状态?(例如:dp[i] = dp[i-1] + dp[i-2])。
    3. 初始化 (Base Case):最简单的情况是什么?(例如:dp[0]=1, dp[1]=1)。
    4. 遍历顺序:从小到大?还是其他顺序?确保计算 dp[i] 时,所需的前置状态已计算完毕。
  • 优化技巧
    • 空间压缩:如果 dp[i] 只依赖前几个状态,可以用变量代替数组(如爬楼梯)。
    • 多状态 DP:当单一状态不足以描述情境时(如股票问题),需要定义多个状态数组(持有/不持有/冷冻期)。

📝 本类题型总结

题目 核心技巧 关键差异点 (vs 爬楼梯)
70. 爬楼梯 线性递推 基准dp[i] = dp[i-1] + dp[i-2]
198. 打家劫舍 间隔选择 dp[i] = max(dp[i-1], dp[i-2] + nums[i])。选或不选。
121. 买卖股票 极值记录 记录 minPriceprofit = price - min。非典型递推。
152. 乘积最大 双状态维护 需同时维护 maxmin,防止负数翻转。
300. LIS 双重循环 dp[i] 依赖所有 j<inums[j]<nums[i]。$O(N^2)$。
322. 零钱兑换 完全背包 dp[i] = min(dp[i], dp[i-coin] + 1)。求最小值。
118. 杨辉三角 二维递推 dp[i][j] = dp[i-1][j-1] + dp[i-1][j]

🏆 母题:LeetCode 70. 爬楼梯 (Climbing Stairs)

题目内容:假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
核心逻辑

  1. 状态定义dp[i] 表示爬到第 i 阶的方法数。
  2. 转移方程:要到第 i 阶,只能从 i-1 爬 1 步,或从 i-2 爬 2 步。即 dp[i] = dp[i-1] + dp[i-2]
  3. 空间优化:只需要前两个状态,用 ab 滚动更新即可。
class Solution {
    public int climbStairs(int n) {
        if (n <= 2) return n;
        
        // ⚠️ [空间优化] 用两个变量代替 dp 数组
        int a = 1; // 代表 dp[i-2]
        int b = 2; // 代表 dp[i-1]
        
        for (int i = 3; i <= n; i++) {
            // ⚠️ [核心逻辑] 状态转移:当前 = 前一步 + 前两步
            int temp = a + b;
            a = b;      // 更新 dp[i-2] 为原来的 dp[i-1]
            b = temp;   // 更新 dp[i-1] 为当前的 dp[i]
        }
        
        return b;
    }
}

🔄 变体 1:LeetCode 198. 打家劫舍 (House Robber)

题目内容:你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,但相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
相比母题的变化

  1. 状态定义dp[i] 表示偷窃到第 i 个房屋(不一定偷第 i 个)时的最大金额。
  2. 转移方程
    • 偷第 i:则不能偷 i-1,金额为 dp[i-2] + nums[i]
    • 不偷第 i:金额为 dp[i-1]
    • 取最大值:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        
        int a = nums[0]; // dp[i-2]
        int b = Math.max(nums[0], nums[1]); // dp[i-1]
        
        for (int i = 2; i < nums.length; i++) {
            // ⚠️ [变化点] 转移方程:max(不偷当前, 偷当前+前前个)
            int temp = Math.max(b, a + nums[i]);
            a = b;
            b = temp;
        }
        
        return b;
    }
}

🔄 变体 2:LeetCode 121. 买卖股票的最佳时机 (Best Time to Buy and Sell Stock)

题目内容:给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
相比母题的变化

  1. 思维转换:这不是典型的 dp[i] 依赖 dp[i-1] 的递推,而是贪心 + 状态记录
  2. 核心逻辑
    • 维护一个 minPrice:记录历史最低价格。
    • 每天计算:profit = price - minPrice
    • 更新 maxProfit
    • 本质上是 DP 的状态压缩:dp[i] (第 i 天卖出的最大利润) = prices[i] - min(prices[0...i-1])
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        
        int minPrice = prices[0];
        int maxProfit = 0;
        
        for (int i = 1; i < prices.length; i++) {
            // ⚠️ [变化点 1] 更新历史最低价
            if (prices[i] < minPrice) {
                minPrice = prices[i];
            } 
            // ⚠️ [变化点 2] 计算当前卖出的利润,并更新最大利润
            else if (prices[i] - minPrice > maxProfit) {
                maxProfit = prices[i] - minPrice;
            }
        }
        
        return maxProfit;
    }
}

🔄 变体 3:LeetCode 152. 乘积最大子数组 (Maximum Product Subarray)

题目内容:给你一个整数数组 nums ,请你找出数组中乘积最大的非空连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
相比母题的变化

  1. 难点:负数的存在。负数乘以负数会变成正数(最大)。
  2. 双状态 DP
    • maxDP[i]:以 i 结尾的最大乘积。
    • minDP[i]:以 i 结尾的最小乘积(可能是很大的负数,等待下一个负数翻身)。
  3. 转移方程
    • maxDP[i] = max(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])
    • minDP[i] = min(nums[i], maxDP[i-1]*nums[i], minDP[i-1]*nums[i])
class Solution {
    public int maxProduct(int[] nums) {
        int maxSoFar = nums[0];
        int currentMax = nums[0];
        int currentMin = nums[0];
        
        for (int i = 1; i < nums.length; i++) {
            int tempMax = currentMax;
            int tempMin = currentMin;
            
            // ⚠️ [变化点] 同时更新最大值和最小值
            // 当前最大值可能是:当前数本身、前最大*当前、前最小*当前(负负得正)
            currentMax = Math.max(nums[i], Math.max(tempMax * nums[i], tempMin * nums[i]));
            currentMin = Math.min(nums[i], Math.min(tempMax * nums[i], tempMin * nums[i]));
            
            maxSoFar = Math.max(maxSoFar, currentMax);
        }
        
        return maxSoFar;
    }
}

🔄 变体 4:LeetCode 300. 最长递增子序列 (Longest Increasing Subsequence)

题目内容:给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
相比母题的变化

  1. 定义变化dp[i] 表示nums[i] 结尾的最长递增子序列长度。
  2. 转移方程:需要遍历 j < i 的所有情况。
    • 如果 nums[j] < nums[i],则 dp[i] = max(dp[i], dp[j] + 1)
  3. 复杂度:$O(N^2)$。(注:最优解可用二分查找优化到 $O(N \log N)$,但 DP 思路更基础)
class Solution {
    public int lengthOfLIS(int[] nums) {
        if (nums.length == 0) return 0;
        
        int[] dp = new int[nums.length];
        // ⚠️ [初始化] 每个位置至少长度为 1 (自己)
        java.util.Arrays.fill(dp, 1);
        
        int maxLen = 1;
        
        for (int i = 1; i < nums.length; i++) {
            // ⚠️ [变化点] 内层循环检查所有前面的元素
            for (int j = 0; j < i; j++) {
                if (nums[j] < nums[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        
        return maxLen;
    }
}

🔄 变体 5:LeetCode 322. 零钱兑换 (Coin Change)

题目内容:给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1
相比母题的变化

  1. 模型:完全背包问题(硬币可以无限使用)。
  2. 状态定义dp[i] 表示凑成金额 i 所需的最少硬币数。
  3. 初始化dp[0] = 0,其他初始化为 amount + 1 (代表无穷大)。
  4. 转移方程dp[i] = min(dp[i], dp[i - coin] + 1)
import java.util.Arrays;

class Solution {
    public int coinChange(int[] coins, int amount) {
        // ⚠️ [初始化] 最大值设为 amount + 1,因为最多也就 amount 个 1 元硬币
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, amount + 1);
        
        dp[0] = 0;
        
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                if (i - coin >= 0) {
                    // ⚠️ [变化点] 状态转移:取最小值
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        
        return dp[amount] > amount ? -1 : dp[amount];
    }
}

🔄 变体 6:LeetCode 118. 杨辉三角 (Pascal's Triangle)

题目内容:给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
相比母题的变化

  1. 二维 DPdp[i][j] 表示第 i 行第 j 列的值。
  2. 转移方程:每个数等于它上方两数之和。dp[i][j] = dp[i-1][j-1] + dp[i-1][j]
  3. 边界处理:每行的第一个和最后一个元素都是 1。
import java.util.ArrayList;
import java.util.List;

class Solution {
    public List<List<Integer>> generate(int numRows) {
        List<List<Integer>> triangle = new ArrayList<>();
        
        for (int i = 0; i < numRows; i++) {
            List<Integer> row = new ArrayList<>();
            // ⚠️ [变化点] 初始化当前行,先填满 1
            for (int j = 0; j <= i; j++) {
                row.add(1);
            }
            
            // ⚠️ [变化点] 更新中间的值 (除了首尾)
            for (int j = 1; j < i; j++) {
                // 状态转移:上一行的左上方 + 上一行的正上方
                int val = triangle.get(i - 1).get(j - 1) + triangle.get(i - 1).get(j);
                row.set(j, val);
            }
            
            triangle.add(row);
        }
        
        return triangle;
    }
}
posted @ 2026-03-08 21:57  scales123  阅读(51)  评论(0)    收藏  举报