动态规划介绍

动态规划是把大问题拆成会重复出现的子问题,每个子问题只算一次,再按依赖关系拼出答案。核心就四件事:状态、转移、边界、答案落在哪。

怎么拆一道题

  1. 状态:一个子问题要记哪些信息。通常是「考虑到第 i 步、还剩多少资源」。
  2. 转移:当前状态由哪些更小的状态推出来。这一步就是递推式。
  3. 边界:最小的子问题,答案可以直接写出来,例如容量为 0、金额为 0。
  4. 计算顺序:先算不依赖别人的格子,再算依赖它们的格子。
  5. 答案:往往是表的最后一格,或整表里的最大值、最小值。

两种写法结果一样:

  • 自顶向下:从原问题递归下去,算过的放进字典,再遇到直接查。也叫记忆化。
  • 自底向上:开一张表,从小到大填满。面试和工程里更常见,因为顺序清楚、不容易爆栈。

时间大致是「状态个数 × 每个状态的转移次数」。能把二维表压成一维时,空间可以再降一截。

例子一:斐波那契

定义:F(0) = 0,F(1) = 1,F(n) = F(n-1) + F(n-2)。

直接递归算 F(5) 时,F(3)、F(2) 会在不同分支里各算多遍:

F(5)

├─ F(4)

│ ├─ F(3)

│ │ ├─ F(2)

│ │ └─ F(1)

│ └─ F(2)

└─ F(3)

├─ F(2)

└─ F(1)

状态就是「第 n 项」。转移是前两项相加。边界是 F(0)、F(1)。自底向上只留最近两项:

n 0 1 2 3 4 5
F 0 1 1 2 3 5

每个 n 只算一次,时间 O(n),空间 O(1)。直接递归不加记忆的话,时间大约是 O(2^n)。

def fib(n: int) -> int:

if n <= 1:

return n

a, b = 0, 1

for _ in range(2, n + 1):

a, b = b, a + b

return b

例子二:0/1 背包

有三件物品,每件最多拿一次。背包容量是 5。

物品 重量 价值
A 2 3
B 3 4
C 4 5

状态 dp[i][w]:只考虑前 i 件物品、容量正好用到不超过 w 时,能拿到的最大价值。

转移:第 i 件要么不拿,价值等于 dp[i-1][w];要么拿,前提是 w 够重,价值等于 dp[i-1][w - 重量] + 价值。两者取最大。

边界:没有物品,或容量为 0,价值都是 0。

按物品一行行填(行是「考虑到第几件」,列是容量 0 到 5):

容量 0 1 2 3 4 5

无 0 0 0 0 0 0

+A 0 0 3 3 3 3

+B 0 0 3 4 4 7

+C 0 0 3 4 5 7

读最后一行最后一列:最大价值是 7,对应拿 A 和 B(重量 2+3,价值 3+4)。容量 4 那一格是 5,对应只拿 C。容量 5 时拿 C 只有 5,不如 A+B 的 7。

def knapsack(weights: listint, values: listint, capacity: int) -> int:

n = len(weights)

dp = \[0 * (capacity + 1) for _ in range(n + 1)]

for i in range(1, n + 1):

weight, value = weightsi - 1, valuesi - 1

for w in range(capacity + 1):

dpiw = dpi - 1w

if w >= weight:

dpiw = max(dpiw, dpi - 1w - weight + value)

return dpncapacity

print(knapsack(2, 3, 4, 3, 4, 5, 5)) # 7

0/1 背包填表时,当前行只能用上一行的结果。如果改成一维数组从容量大到小更新,同一件物品就不会被用两次。

例子三:零钱兑换

硬币面额 [1, 2, 5],凑出金额 11,求最少硬币数。和背包不同,这里同一面额可以用多次,求的是最小值。

状态 dp[x]:凑出金额 x 的最少硬币数。

转移:dp[x] = min(dp[x - coin] + 1),coin 是不超过 x 的面额。

边界:dp[0] = 0。凑不出的金额先记成一个很大的数。

金额 0 1 2 3 4 5 6 7 8 9 10 11
枚数 0 1 1 2 2 1 2 2 3 3 2 3

dp[11] = 3,一种凑法是 5+5+1。例如 dp[6] 可以是 dp[5]+1(再加一枚 1)或 dp[4]+1(再加一枚 2)或 dp[1]+1(再加一枚 5),最小值是 2。

def coin_change(coins: listint, amount: int) -> int:

inf = amount + 1

dp = inf * (amount + 1)

dp0 = 0

for x in range(1, amount + 1):

for coin in coins:

if coin <= x:

dpx = min(dpx, dpx - coin + 1)

return dpamount if dpamount <= amount else -1

print(coin_change(1, 2, 5, 11)) # 3

看到题目时怎么判断

可以沿这个顺序问自己:

  • 要求的是最大、最小、方案数,还是能否达到。
  • 走到这一步,必须记住哪些信息,漏一个维度答案就会错。
  • 当前这一步只依赖更早的状态,不依赖后面的选择。
  • 同一个子问题会在不同路径里再次出现。

最长上升子序列、编辑距离、不同路径、股票买卖区间,都是同一套:先定状态,再写「这一步选或不选」的转移。状态定准了,式子通常就能跟着写出来。

相关推荐
Nuanyt17 小时前
LeetCode Hot 100 链表专题2 3 题 (Java)
java·笔记·算法·leetcode·链表
海绵宝宝转agent18 小时前
LeetCodehot100题解
java·笔记·学习·算法·leetcode·深度优先
青少儿编程课堂18 小时前
2026蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:堆排序与优先队列解析
c++·python·算法·bfs·信息学竞赛
SZYSK19 小时前
OpenHarmony 上读卡、推理和双屏刷新抢 CPU:核验机与消费机的调度差
物联网·算法·harmonyos·1024程序员节
Ivanqhz19 小时前
MoE + LLM 应用
数据库·人工智能·pytorch·python·深度学习·算法·1024程序员节
lpfasd12319 小时前
推荐算法进化史
算法·机器学习·推荐算法
Pniubi19 小时前
力扣543二叉树的直径(深度优先搜索法(递归))
算法·leetcode·职场和发展
UIU1141 天前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
论文复现现场1 天前
WRF、FEA 能用 L40S 加速吗?求解器检查、断点恢复与长期运行
java·数据库·算法·wrf·有限元分析·gpu计算·断点续算
ZhengEnCi1 天前
用 AI 写代码为什么拼到最后还是拼人:算力瓶颈与领域专家提示词的本质差异
算法·架构