第 22 站 · 动态规划
动态规划入门与状态设计
一维 DP + 滚动变量 时间 O(n) · 空间 O(1)
- 先学
- 面试频次
- 9 家公司考过 · 面经出现 10 次Snap 2、Intuit 1、Microsoft 1、Morgan Stanley 1、Paradromics 1
先做这道
LC 509斐波那契数卡住再看
用递归、记忆化和滚动变量对照理解重复子问题;能够说明时间与空间复杂度。
要点
- 先用一句话定义状态,比如 dp[i] = 只看前 i 个元素时的最优值;状态说清楚了,转移通常就出来了。
- 转移只看「最后一步做了什么」:第 i 个选还是不选,最后一跳是 1 步还是 2 步。
- dp[i] 只依赖前一两项时,用两个变量滚动,空间从 O(n) 降到 O(1)。
易错点
- dp 和输入的下标错位:dp 多开一位时 dp[0] 表示「什么都没选」,答案在 dp[n]。
- 滚动更新时先覆盖了 prev1 再拿它算新值,用到的已经是新数据了。
- 方案数很快会超出 64 位;题目要求取模时,每一步都要取模。
看动画
一步一步播放这个模式在小例子上的执行过程。可以暂停、单步前进或后退,拖动进度条跳到任意一步。
动态规划 · 零钱兑换
硬币 1、3、4,每种无限枚,凑出 6
换个输入
- 目标金额
- 6
- 当前金额
- —
- 最少硬币
- 尚不可达
dp[0] = 0,其余状态为 ∞(不可达)。dp[x] 表示凑出金额 x 的最少硬币数。
始终成立按金额递增计算;更新 dp[x] 时,所有更小金额的最优解都已确定。
模板
选一种语言展开。四种写法实现的是同一个模板,每一份都编译运行过,并用测试数据核对了输出。看完用它重写热身题 LC 509。
Python
def rob(nums: list[int]) -> int:
"""Largest sum of elements with no two adjacent."""
prev2 = prev1 = 0 # best totals up to i-2 and up to i-1
for x in nums:
prev2, prev1 = prev1, max(prev1, prev2 + x) # skip x, or take it after i-2
return prev1C++
// Largest sum of elements with no two adjacent.
long long rob(const vector<int>& nums) {
long long prev2 = 0, prev1 = 0; // best totals up to i-2 and up to i-1
for (int x : nums) {
long long cur = max(prev1, prev2 + x); // skip x, or take it after i-2
prev2 = prev1;
prev1 = cur;
}
return prev1;
}Java
class Solution {
// Largest sum of elements with no two adjacent.
long rob(int[] nums) {
long prev2 = 0, prev1 = 0; // best totals up to i-2 and up to i-1
for (int x : nums) {
long cur = Math.max(prev1, prev2 + x); // skip x, or take it after i-2
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
}Go
// rob returns the largest sum of elements with no two adjacent.
func rob(nums []int) int {
prev2, prev1 := 0, 0 // best totals up to i-2 and up to i-1
for _, x := range nums {
prev2, prev1 = prev1, max(prev1, prev2+x) // skip x, or take it after i-2
}
return prev1
}常见变体
这一站的题还会用到下面 2 种写法。展开看什么时候换用它,代码同样四种语言都跑过测试。
自顶向下记忆化递归时间 O(n) · 空间 O(n)(备忘表 + 递归栈)
先写出「从 i 开始还有几种 / 最少要多少」的递归,再加一张备忘表,就是 DP 的另一种写法:转移不好排顺序、或者只有一部分状态会被用到时更顺手。LC 91 解码方法、LC 70 爬楼梯、LC 343 整数拆分都能这样写;Python 用 functools.cache。
Python
from functools import cache
def num_decodings(s: str) -> int:
"""Ways to read a digit string as letters, 1 = A ... 26 = Z (LC 91)."""
@cache
def ways(i: int) -> int: # ways to decode s[i:]
if i == len(s):
return 1
if s[i] == "0":
return 0 # no letter's code starts with 0
total = ways(i + 1) # read one digit
if i + 1 < len(s) and int(s[i : i + 2]) <= 26:
total += ways(i + 2) # or two
return total
return ways(0)C++
// Ways to read a digit string as letters, 1 = A ... 26 = Z (LC 91).
long long numDecodings(const string& s) {
int n = s.size();
vector<long long> memo(n + 1, -1); // memo[i]: ways to decode s[i..], -1 = not computed yet
function<long long(int)> ways = [&](int i) -> long long {
if (i == n) return 1;
if (memo[i] >= 0) return memo[i];
if (s[i] == '0') return memo[i] = 0; // no letter's code starts with 0
long long total = ways(i + 1); // read one digit
if (i + 1 < n && stoi(s.substr(i, 2)) <= 26) total += ways(i + 2); // or two
return memo[i] = total;
};
return ways(0);
}Java
class Solution {
private Long[] memo; // memo[i]: ways to decode s[i..], null = not computed yet
private String s;
// Ways to read a digit string as letters, 1 = A ... 26 = Z (LC 91).
long numDecodings(String s) {
this.s = s;
memo = new Long[s.length() + 1];
return ways(0);
}
private long ways(int i) {
if (i == s.length()) return 1;
if (memo[i] != null) return memo[i];
long total = 0;
if (s.charAt(i) != '0') { // no letter's code starts with 0
total = ways(i + 1); // read one digit
if (i + 1 < s.length() && Integer.parseInt(s.substring(i, i + 2)) <= 26) total += ways(i + 2); // or two
}
return memo[i] = total;
}
}Go
// numDecodings returns the ways to read a digit string as letters, 1 = A ... 26 = Z (LC 91).
func numDecodings(s string) int {
n := len(s)
memo := make([]int, n+1) // memo[i]: ways to decode s[i:], -1 = not computed yet
for i := range memo {
memo[i] = -1
}
var ways func(i int) int
ways = func(i int) int {
if i == n {
return 1
}
if memo[i] >= 0 {
return memo[i]
}
total := 0
if s[i] != '0' { // no letter's code starts with 0
total = ways(i + 1) // read one digit
if i+1 < n && (s[i]-'0')*10+(s[i+1]-'0') <= 26 {
total += ways(i + 2) // or two
}
}
memo[i] = total
return total
}
return ways(0)
}环形拆成两条链时间 O(n) · 空间 O(1)
数组首尾相连、首尾不能同时选(环形打家劫舍):拆成「不要最后一个」和「不要第一个」两条普通链,各跑一遍线性 DP,取较大者。LC 213 打家劫舍 II;环形子数组最大和是另一种处理方式,见状态机 DP 那一站。
Python
def rob_circle(nums: list[int]) -> int:
"""House robber with the houses in a circle: the first and last are neighbours (LC 213)."""
def rob_line(lo: int, hi: int) -> int: # the plain robber on nums[lo:hi]
prev2 = prev1 = 0
for x in nums[lo:hi]:
prev2, prev1 = prev1, max(prev1, prev2 + x)
return prev1
if len(nums) == 1:
return nums[0]
return max(rob_line(0, len(nums) - 1), rob_line(1, len(nums))) # without the last, or without the firstC++
// House robber with the houses in a circle: the first and last are neighbours (LC 213).
int robCircle(const vector<int>& nums) {
auto robLine = [&](int lo, int hi) { // the plain robber on nums[lo, hi)
int prev2 = 0, prev1 = 0;
for (int i = lo; i < hi; i++) {
int cur = max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = cur;
}
return prev1;
};
int n = nums.size();
if (n == 1) return nums[0];
return max(robLine(0, n - 1), robLine(1, n)); // without the last, or without the first
}Java
class Solution {
// House robber with the houses in a circle: the first and last are neighbours (LC 213).
int robCircle(int[] nums) {
int n = nums.length;
if (n == 1) return nums[0];
return Math.max(robLine(nums, 0, n - 1), robLine(nums, 1, n)); // without the last, or without the first
}
// The plain robber on nums[lo, hi).
private int robLine(int[] nums, int lo, int hi) {
int prev2 = 0, prev1 = 0;
for (int i = lo; i < hi; i++) {
int cur = Math.max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = cur;
}
return prev1;
}
}Go
// robCircle is the house robber with the houses in a circle: the first and last are neighbours (LC 213).
func robCircle(nums []int) int {
robLine := func(line []int) int { // the plain robber on a line of houses
prev2, prev1 := 0, 0
for _, x := range line {
prev2, prev1 = prev1, max(prev1, prev2+x)
}
return prev1
}
n := len(nums)
if n == 1 {
return nums[0]
}
return max(robLine(nums[:n-1]), robLine(nums[1:])) // without the last, or without the first
}为什么对
- 最优子结构:前 i 个元素的最优解去掉最后一个决策,剩下的必须是对应子问题的最优解;否则换成更优的子解,整体会更好,矛盾。
- 按最后一个决策分类(偷不偷第 i 家、最后一步跨 1 级还是 2 级),把所有方案分成互不相交、又覆盖全部的几类,所以取 max 或求和不漏也不重。
- 滚动变量只在 dp[i] 只依赖 dp[i−1] 和 dp[i−2] 时成立,而且要先用旧值算出新值,再整体前移。
30 秒口述稿
面试官让你讲思路时,按做法、为什么对、复杂度的顺序说。中英文各一版,可以对着读几遍。
动态规划我先用一句话定义状态,比如 dp[i] 表示用前 i 个元素能得到的最优值。然后按最后一个决策分类写转移:第 i 个选或不选,这几类互不重叠又覆盖所有方案,所以取最大值不会漏。正确性靠最优子结构:最优解去掉最后一步,剩下的也一定是子问题的最优解。如果只依赖前两项,就用两个变量滚动,空间降到 O(1)。For DP I first define the state in one sentence, such as dp[i] being the best value using the first i elements. Then I write the transition by the last decision, take element i or not, and since those cases are disjoint and cover every plan, the max misses nothing. It is correct by optimal substructure: remove the last step from a best answer and what remains is a best answer to the smaller problem. When dp[i] needs only the previous two values, I roll two variables and use O(1) space.自评一下
对照上面的答案给自己打个分。没掌握的题会按 1、3、7、21 天的间隔回来,到期的题在「我的进度 · 到期复习」里。 只存在这台设备上;登录后会合并到你的账号。
小测
4 道概念题和复杂度题。答过的题会和 OA 选择题练习一起按间隔回来复习。 去 OA 选择题练习
作答记录只保存在这台设备的浏览器里;登录后打开OA 选择题练习页,会存进账号。
模板练习
用上面的模板和变体出题:把打乱的行排回原序,把挖掉的边界条件填回去,或者在空白编辑器里整段默写,跑本站的测试并逐行对比。每次「换一题」都会重新出题。
下面几行被打乱了。点一行选中,再点另一行和它交换,排回模板原来的顺序;缩进是提示。
分档练习
本站在 500 路里的 10 道题,按基础、核心、进阶分三档。每题附一句话思路,先自己想,想不出再看。
基础 3
核心 5
| # | 题目 | 一句话思路 | 难度分 |
|---|---|---|---|
| 198 | 打家劫舍 | 建立选或不选的相邻排斥模型,并将完整表压缩为常数空间。 | |
| 213 | 打家劫舍 II | 通过固定首尾选择拆解环形依赖,检查单元素和区间边界。 | |
| 740 | 删除并获得点数 | 把值之间的冲突转成打家劫舍,训练聚合计数后的模型转换。 | |
| 91 | 解码方法 | 按末尾一位或两位划分解码方案,严谨处理零和非法编码。 | |
| 2369 | 检查数组是否存在有效划分 | 将固定长度分组规则转成前缀可达性,分清连续递增与相等三元组。 |
本站 OA 真题 59
- Get Maximum ChargeAmazon
- Calculate Max Quality ScoreAmazon
- Find Time (Mutation Removal)Amazon
- Move Units (Protect Cities)Amazon
- Get Time To Stabilize (Pipeline Service Outage)Amazon
- Calculate Maximum Quality ScoreAmazon
- Find Ideal DaysAmazon
- Find Maximum ScoreAmazon
- Get Maximum CountAmazon
- Get Number of Perfect PackagingAmazon