1. 动态规划的解题五步?(套模板破题)

  1. 定义 dp 数组:dp[i] 表示什么(明确下标含义)
  2. 找状态转移方程:dp[i] 如何由更小的状态推出(核心)
  3. 初始化:dp[0]/dp[1] 等边界值
  4. 确定遍历顺序:从前到后/从后到前、单层/双层循环
  5. 验证:拿小例子手算对照 dp 表

什么时候用 DP:最优子结构(子问题最优→全局最优)+ 重叠子问题(子问题重复计算)。对比递归:递归 + 备忘录(记忆化搜索)是自顶向下版 DP,等价。

爬楼梯:最简 DP 入门
// 每次爬 1 或 2 阶,到 n 阶有几种方法
// dp[i] = dp[i-1] + dp[i-2](斐波那契变体)
public int climbStairs(int n) {
    int a = 1, b = 2;          // dp[1]=1, dp[2]=2
    for (int i = 3; i <= n; i++) {
        int c = a + b;             // 滚动变量省数组
        a = b; b = c;
    }
    return n <= 2 ? n : b;
}

🎯 面试要点

  • 很多 dp 可以滚动数组优化空间(O(n) → O(1)),面试主动提是加分项
  • 先写二维 dp 再考虑压缩,别一上来就写滚动(易错)
  • 能递归的题大多能 DP;"记忆化搜索"代码结构更接近题意,可以先给这个再优化

2. 01 背包和完全背包?

01 背包一维写法(背模板)
// 容量 W,n 件物品(重量 w[i]、价值 v[i]),求最大价值
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++)
    for (int c = W; c >= w[i]; c--)      // 倒序:每个物品只能用一次
        dp[c] = Math.max(dp[c], dp[c - w[i]] + v[i]);
return dp[W];

// 完全背包:把内层改成正序 for (c = w[i]; c <= W; c++)
// 变体:凑满 W 的方案数 → dp[0]=1,dp[c] += dp[c-w[i]]

🎯 面试要点

  • 常见变体:分割等和子集(01 背包:dp 判断能否凑满 sum/2)、零钱兑换(完全背包最少硬币)、组合总和 IV(排列 vs 组合——外层循环物品种类 vs 容量)
  • 先答"二维 + 转移方程",再展示一维优化——展示思考过程

3. 子序列类 DP:LIS 和 LCS?

最长公共子序列
public int lcs(String a, String b) {
    int m = a.length(), n = b.length();
    int[][] dp = new int[m + 1][n + 1];
    for (int i = 1; i <= m; i++)
        for (int j = 1; j <= n; j++)
            dp[i][j] = a.charAt(i-1) == b.charAt(j-1)
                ? dp[i-1][j-1] + 1
                : Math.max(dp[i-1][j], dp[i][j-1]);
    return dp[m][n];
}

🎯 面试要点

  • 回文类题 dp 填表顺序特殊:从 i 大到小(依赖左下角),或按区间长度遍历
  • 编辑距离(最少操作把 a 变 b)= LCS 的近亲:dp[i][j] 三种操作(删/插/换)取最小
  • 子序列 vs 子串:子串要求连续,用滑动窗口/双指针往往更简单

4. 贪心、DP、回溯怎么选?

回溯模板(全排列)
List<List<Integer>> res = new ArrayList<>();
int n = nums.length;                    // 先取长度
boolean[] used = new boolean[n];

void backtrack(int[] nums, List<Integer> path) {
    if (path.size() == nums.length) { res.add(new ArrayList<>(path)); return; }
    for (int i = 0; i < nums.length; i++) {
        if (used[i]) continue;        // 剪枝
        used[i] = true;
        path.add(nums[i]);
        backtrack(nums, path);          // 递归
        path.remove(path.size() - 1);  // 撤销选择
        used[i] = false;
    }
}

🎯 面试要点

  • 回溯复杂度通常指数级,必须先讲"剪枝"再写代码(排序去重、used 数组、start 下标)
  • 组合/子集用 start 下标避免回头;排列用 used 数组
  • 贪心题面试喜欢让你"证明正确性":反例论证是核心

🎤 常见面试追问

  1. DP 和贪心的区别?——贪心每一步做局部最优且不用回头看(需证明全局最优);DP 保留所有子问题最优解,通过状态转移方程递推。
  2. 怎么识别一道题可以用 DP?——两个特征:最优子结构(大问题最优 = 子问题最优的组合)+ 重叠子问题(子问题被重复计算)。"最值/方案数"类题优先想 DP。
  3. 01 背包一维优化为什么容量要倒序遍历?——倒序保证每个物品只用一次(正序会重复使用当前物品,变成完全背包)。这是背模板时最该理解的点。
  4. 记忆化搜索和 DP 的关系?——记忆化搜索 = 递归 + 缓存(自顶向下);DP = 迭代填表(自底向上)。两者等价,记忆化搜索代码更贴近题意。
  5. 回文类 DP 为什么填表顺序特殊?——dp[i][j] 依赖 dp[i+1][j-1](左下角),所以要从下往上、从左往右填,或按区间长度遍历。

📖 名词解释(本页术语)

术语 大白话解释
动态规划(DP)把大问题拆成有重叠的小问题,每个小问题只算一次并记下来(dp 表),从小到大递推。经典:背包、最长子序列。
状态转移方程dp 的"递推公式":当前状态怎么由之前的状态算出来。如 dp[i] = dp[i-1] + dp[i-2]。
重叠子问题同一个子问题被反复计算(如斐波那契的 fib(3) 被算多次)——DP 用表避免重复计算。
最优子结构全局最优解包含子问题的最优解(大问题的最优 = 小问题最优的组合)。
贪心算法每一步都选当前看起来最好的,不回头。快(O(n))但需要证明局部最优=全局最优。如跳跃游戏、区间调度。
回溯算法穷举所有可能 + 剪枝:做选择 → 递归 → 撤销选择。用于找"所有方案"(全排列、组合)。
记忆化搜索递归版 DP:先递归算,算过的结果存缓存,下次直接用。和填表 DP 殊途同归。
01 背包 / 完全背包背包问题的两种:01 = 每件物品最多选一次;完全 = 每件可以无限选。区别只在遍历方向(倒序/正序)。
滚动数组dp 只依赖前几行时,用几个变量循环覆盖,把 O(n) 空间压成 O(1)。如爬楼梯只需两个变量。
⚠️ 本页面由 AI 生成,内容仅供参考,请以官方文档和实际源码为准。