【Java实习面试算法冲刺】动态规划

第10类题型:动态规划

问题背景:为什么动态规划题你明明背过公式,一到面试还是容易卡在第一步

很多同学刷动态规划时,最常见的错觉是"这题我见过,公式我也记得差不多"。但真正到面试现场,动态规划最容易暴露的问题,不是你没见过题,而是你没有在写代码前先把这三件事说清楚:

  • dp[i]dp[i][j] 到底表示什么。
  • 当前状态为什么能从前面的状态转移过来。
  • 初始化为什么必须这样设,而不是随手拍一个值。

如果这三件事有一件没想清楚,后面代码通常就会越写越虚。你可能能背出 dp[i] = dp[i - 1] + dp[i - 2],但一换成 322. 零钱兑换198. 打家劫舍,就很容易不知道状态含义该怎么落地。

如果你现在正处在"基础 DP 题大概会做,但一换题型就容易忘初始化和转移"的阶段,这一类题必须单独练透。读完这篇,你至少要把 4 件事固定下来:先定义状态、再看最后一步来自哪里、初始化必须和状态含义配套、最后确认答案位置到底在哪。


文章目录

  • 第10类题型:动态规划
    • 问题背景:为什么动态规划题你明明背过公式,一到面试还是容易卡在第一步
    • [1. 核心知识点](#1. 核心知识点)
    • [2. 动态规划题型选择表:线性 DP / 路径 DP / 完全背包味道什么时候用](#2. 动态规划题型选择表:线性 DP / 路径 DP / 完全背包味道什么时候用)
    • [3. 动画辅助理解:动态规划不是背公式,而是先定义状态](#3. 动画辅助理解:动态规划不是背公式,而是先定义状态)
    • [4. 用样例手推一维 DP / 完全背包味道 DP](#4. 用样例手推一维 DP / 完全背包味道 DP)
      • [先手推一维 DP:`70. 爬楼梯`](#先手推一维 DP:70. 爬楼梯)
      • [再手推完全背包味道 DP:`322. 零钱兑换`](#再手推完全背包味道 DP:322. 零钱兑换)
    • [5. 这类题在面试里考什么](#5. 这类题在面试里考什么)
    • [6. 高频题清单](#6. 高频题清单)
    • [7. 这类题最容易犯的 4 个错误](#7. 这类题最容易犯的 4 个错误)
    • [8. 代表题精讲 1](#8. 代表题精讲 1)
    • [9. 代表题精讲 2](#9. 代表题精讲 2)
    • [10. 其余题模板与关键片段](#10. 其余题模板与关键片段)
    • [11. 动态规划题先判断什么](#11. 动态规划题先判断什么)
    • [12. 错题本记录方式](#12. 错题本记录方式)
    • [13. 面试前 3 分钟速记](#13. 面试前 3 分钟速记)
    • [14. 快速验证:同一套判断能不能落到 70 / 198 / 322](#14. 快速验证:同一套判断能不能落到 70 / 198 / 322)
    • [15. 本地样例验证结果](#15. 本地样例验证结果)
    • [16. 最后总结:动态规划真正难的不是公式,而是状态定义](#16. 最后总结:动态规划真正难的不是公式,而是状态定义)

1. 核心知识点

动态规划最关键的不是背题,而是固定一条判断链路:

  1. 先定义状态。
  2. 再思考当前状态由哪些更小状态转移而来。
  3. 然后补初始化。
  4. 最后确认答案在 dp[n]dp[m][n],还是某个遍历过程里的最大值。

实习面试里,先掌握最基础的 3 类就够了:

  • 一维线性 DP:爬楼梯、打家劫舍。
  • 二维路径 DP:不同路径。
  • 完全背包味道:零钱兑换。

2. 动态规划题型选择表:线性 DP / 路径 DP / 完全背包味道什么时候用

题目特征 常见问题 优先解法 你在面试里要先说清楚什么
当前答案只依赖前 1-2 个位置 方法数、最大收益 一维线性 DP dp[i] 表示什么,最后一步来自哪里
当前位置由上方或左方转移 路径数、网格累计 二维路径 DP dp[i][j] 的业务含义,第一行第一列怎么初始化
当前金额由更小金额加一枚硬币得到 最少个数、是否可达 完全背包味道 DP "不可达"怎么表示,为什么要从小金额往上推

真正高频的失误不是公式写不出来,而是分类没分清:

  • 70. 爬楼梯 本质是线性递推,不是背包。
  • 62. 不同路径 本质是二维路径累加,不是普通一维数组。
  • 322. 零钱兑换 本质是"当前金额如何由更小金额转移",不能把不可达状态和可达状态混在一起。

3. 动画辅助理解:动态规划不是背公式,而是先定义状态

你可以先打开这页分步动画:

动态规划:定义状态、初始化、转移、返回答案分步动画

这个动画最值得盯住的是 4 步:

  1. 先用一句话说清 dp 的含义。
  2. 再把最小可用状态初始化好。
  3. 根据依赖方向写转移。
  4. 最后确认答案到底落在哪个位置。

4. 用样例手推一维 DP / 完全背包味道 DP

动态规划题如果只记公式,不手推数组状态,面试里还是会卡。

先手推一维 DP:70. 爬楼梯

样例:

text 复制代码
n = 5

定义 dp[i] 表示走到第 i 级台阶的方法数。

手推过程:

i 含义 结果
dp[1] 走到第 1 级,只能走 1 步 1
dp[2] 走到第 2 级,可以 1+12 2
dp[3] 最后一步来自 dp[2]dp[1] 3
dp[4] dp[3] + dp[2] 5
dp[5] dp[4] + dp[3] 8

所以答案是 8

你在面试里要说清楚的关键点是:

  • 这题不是死记斐波那契,而是"最后一步只能从 i - 1i - 2 上来"。
  • 初始化 dp[1] = 1dp[2] = 2,本质上是在给转移提供最小合法状态。

再手推完全背包味道 DP:322. 零钱兑换

样例:

text 复制代码
coins = [1, 2, 5]
amount = 11

定义 dp[i] 表示凑出金额 i 所需的最少硬币数。初始化时:

  • dp[0] = 0
  • 其余都先记成不可达的大值

手推关键状态:

金额 最优转移 dp[i]
1 dp[0] + 1 1
2 min(dp[1] + 1, dp[0] + 1) 1
5 min(dp[4] + 1, dp[3] + 1, dp[0] + 1) 1
6 dp[5] + 1 2
10 dp[5] + 1 2
11 dp[10] + 1 3

所以金额 11 的最优答案是 3,即 5 + 5 + 1

你在面试里要强调:

  • dp[i] 不是"能不能凑",而是"最少几枚硬币能凑出来"。
  • 不可达状态必须先区分出来,否则后面的 min 会被脏数据污染。
  • 这题本质不是背包术语本身,而是"当前金额由更小金额加一枚硬币转移而来"。

5. 这类题在面试里考什么

动态规划考的不是你背了多少题,而是你能不能把抽象过程说得稳定。

面试官通常会看:

  • 你能不能先定义清楚状态。
  • 你是否能解释转移公式怎么推出来。
  • 你是否知道初始化的业务含义。
  • 你会不会只背模板,但一追问"为什么"就讲不下去。

真正的区分度在于:你不只是把公式写出来,还能说明"为什么当前状态依赖的是这些旧状态,而不是别的状态"。

6. 高频题清单

题目 来源 难度 高频属性
70. 爬楼梯 LeetCode 热题 100 Easy 基础高频
198. 打家劫舍 LeetCode 热题 100 Medium 高频
62. 不同路径 面试经典 150 Medium 高频
322. 零钱兑换 LeetCode 热题 100 Medium 高频 Medium

7. 这类题最容易犯的 4 个错误

  1. 一上来就写代码,没有先定义 dp 含义。
  2. 只会背转移公式,不会解释"最后一步来自哪里"。
  3. 初始化随手乱写,导致边界全错。
  4. 没把不可达状态和可达状态区分开,结果 min / max 都不可信。

8. 代表题精讲 1

题目

70. 爬楼梯

思路

这是最基础的一维 DP。

定义 dp[i] 表示到达第 i 级台阶的方法数。那么最后一步只有两种可能:

  • i - 1 跨 1 步上来。
  • i - 2 跨 2 步上来。

所以状态转移就是:

dp[i] = dp[i - 1] + dp[i - 2]

这题最容易讲清楚的地方在于,转移公式不是凭空背出来的,而是直接来自"最后一步来自哪里"。

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(n)
  • 空间复杂度:O(n)

边界提醒

这题最容易错的地方有两个:

  • n = 1n = 2 时要提前返回,否则初始化会越界或语义不清。
  • 如果你把 dp[i] 的含义说不清楚,后面虽然能写出代码,但面试官一问为什么是 + 而不是 max,就容易卡住。

如果这是面试现场,你可以这样说

这题我会定义 dp[i] 表示走到第 i 级台阶的方法数。因为到第 i 级时,最后一步只能来自 i - 1i - 2,所以状态转移就是 dp[i] = dp[i - 1] + dp[i - 2]。初始化时 dp[1] = 1dp[2] = 2,这样后面就能顺推到 dp[n]

9. 代表题精讲 2

题目

322. 零钱兑换

思路

这题是非常典型的"完全背包味道" DP。

定义 dp[i] 表示凑出金额 i 所需的最少硬币数。对于每个金额 i,尝试使用每一种硬币 coin

  • 如果 i - coin 可以被凑出来,那么当前方案可以由 dp[i - coin] + 1 转移过来。
  • 在所有可行方案里取最小值。

初始化上要注意:

  • dp[0] = 0
  • 其余位置先设为一个大值,表示当前还不可达

这个初始化不是技巧,而是为了区分"真的能凑出来"和"还没被合法转移到"。

Java 代码

java 复制代码
import java.util.Arrays;

class Solution {
    public int coinChange(int[] coins, int amount) {
        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) {
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        return dp[amount] == amount + 1 ? -1 : dp[amount];
    }
}

复杂度

  • 时间复杂度:O(amount * coins.length)
  • 空间复杂度:O(amount)

边界提醒

这题最容易错的地方是:

  • 没有用大值初始化不可达状态,导致 min 过程混入错误结果。
  • 最后忘了判断 dp[amount] 是否真的被更新过。
  • 把这题写成"尽量凑大"的贪心,结果在很多样例上直接失效。

如果这是面试现场,你可以这样说

这题我会定义 dp[i] 表示凑出金额 i 所需的最少硬币数。对于每个金额,我都尝试最后使用一枚硬币 coin,如果 i - coin 已经可达,那么当前方案可以由 dp[i - coin] + 1 转移过来。因为目标是最少硬币数,所以在所有转移里取最小值。初始化时要先把不可达状态设成大值,最后再判断 amount 是否真的可达。

10. 其余题模板与关键片段

198. 打家劫舍

这题是一维收益型 DP。完整代码如下:

java 复制代码
class Solution {
    public int rob(int[] nums) {
        if (nums.length == 1) {
            return nums[0];
        }

        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);

        for (int i = 2; i < nums.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }

        return dp[nums.length - 1];
    }
}

核心判断是:偷当前房子,就接 dp[i - 2] + nums[i];不偷当前房子,就沿用 dp[i - 1]

62. 不同路径

这题是二维路径 DP,核心转移:

java 复制代码
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];

因为到 (i, j) 位置时,最后一步只可能来自上边或左边。

11. 动态规划题先判断什么

真正动手写代码前,你可以先问自己 4 个问题:

  1. 我的状态到底是什么。
  2. 当前状态由哪些旧状态转移而来。
  3. 初始化必须给哪些最小状态。
  4. 最终答案在哪个位置。

这 4 个问题一旦先说清楚,动态规划题的大部分思路就已经定下来了。

12. 错题本记录方式

DP 题建议重点记:

  • 这题的状态定义是什么。
  • 我转移公式是怎么推出来的。
  • 初始化为什么这样设。
  • 这题是一维、二维,还是完全背包味道。

13. 面试前 3 分钟速记

  • DP 先定义状态,再写转移。
  • 别先背公式,先解释最后一步来自哪里。
  • 初始化和状态定义是一体的。
  • 线性 DP 先练爬楼梯、打家劫舍。
  • 完全背包味道题,重点看"当前金额如何由更小金额转移"。

14. 快速验证:同一套判断能不能落到 70 / 198 / 322

你在面试前可以用下面这张表做最后自检:

题目 先识别什么 正确做法 为什么不是别的做法
70. 爬楼梯 最后一步来自哪 一维线性 DP 它不是贪心,也不是搜索所有路径
198. 打家劫舍 当前房子偷或不偷 一维收益 DP 关键是比较两种选择,不是单纯累加
322. 零钱兑换 当前金额如何由更小金额转移 完全背包味道 DP 贪心在很多样例上不成立,必须用全局最优状态转移

如果你能在 30 秒内把这三题归到正确模型,并说明为什么不能直接套别的模板,那这类 DP 高频题基本就已经掌握到位了。

15. 本地样例验证结果

为了避免"思路看起来对,但代码细节有误",我按文中的 70322 思路做了本地样例验证,输出如下:

text 复制代码
climbStairs(5)=8
coinChange([1,2,5],11)=3

这两行结果分别对应:

  • 70. 爬楼梯 的手推样例最终得到 8 种走法。
  • 322. 零钱兑换 的样例最终最少使用 3 枚硬币。

如果你自己复盘时结果和这里不一致,优先检查 3 个地方:状态定义是不是说反了、初始化是否和状态含义匹配、不可达状态是否被错误参与了转移。

16. 最后总结:动态规划真正难的不是公式,而是状态定义

动态规划题最容易让人慌的地方,不是代码长,而是你在开头没把状态定义清楚。但只要你养成固定顺序,很多题都会变简单:

  1. 先定义状态。
  2. 再看最后一步来自哪里。
  3. 然后补初始化。
  4. 最后确认答案位置。

面试里你只要能把这条链路说顺,动态规划题就不会再只是"背过几个经典题",而是真正掌握了这一类题的公共骨架。


上期速递:【Java实习面试算法冲刺】图论

感谢阅读,记得点赞、关注、收藏,欢迎各位评论区交流!!!

相关推荐
cfm_29141 小时前
SpringBoot自定义Starter
java·spring boot·后端
小蒋学算法1 小时前
算法-滑动窗口最大值
数据结构·算法
胡萝卜术1 小时前
V040:RAG 检索增强生成的工程链路解构与生产级系统设计
面试·架构·github
Demon_Hao1 小时前
Spring Boot 4、JDK 25 与 GraalVM:Java 云原生进化,能否撼动 Go 的地位?
java·spring boot·云原生
冷小鱼1 小时前
AI Agent的核心算法:自主学习与反思(Self-Reflection / Critique)
人工智能·学习·算法·自主学习·self-reflection·critique·反思
wuqingshun31415910 小时前
什么是责任链模式,一般用在什么场景?
java·责任链模式
:-)11 小时前
算法-归并排序
java·开发语言·数据结构·算法·排序算法
wuqingshun31415911 小时前
说一下消息队列的模型有哪些?
java
fīɡЙtīиɡ ℡12 小时前
布隆过滤器
java