动态规划介绍

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

怎么拆一道题

  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

看到题目时怎么判断

可以沿这个顺序问自己:

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

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

相关推荐
怪奇云呼军3 小时前
教育试听预约后没到课,闪电智能 Voice Agent 如何做分层回访?
人工智能·python·算法·云计算·音视频
Nil2083 小时前
leetcode 416分割等和子集
算法·leetcode·职场和发展
云淡风轻~窗明几净3 小时前
角谷猜想的进展之三 移动了群山/沸騰的群山 2026-10-10
人工智能·算法·dubbo·图论
ForeverYang20153 小时前
一种无需训练缺陷数据生成算法--O2MAG
人工智能·深度学习·算法
xuxigifxfh4 小时前
牛客:HJ170 01序列
java·开发语言·算法·华为机考
朝朝辞暮i4 小时前
C++ 第 33 课:事件循环——spin() 到底在干什么
开发语言·c++·算法
殷色玫瑰4 小时前
AVL树:从平衡因子到四种旋转的完整实现
c语言·数据结构·c++·算法·visualstudio
jidaowansui5 小时前
B3643 图的存储———>数组模拟邻接矩阵
算法
y1su5 小时前
Leetcode 二分模板
java·数据结构·算法·leetcode·排序算法