01_Introduction to DP 动态规划简介

动态规划简介

使用备忘录优化朴素递归解法

Pro Tip: 通常先编写一个较慢的解法是个好主意。例如,如果获得全分的复杂度要求是 $O(N)$,而你想到一个简单的 $O(N^2)$ 解法,那么你首先应该编写这个解法并获得部分分数。之后,你可以逐步修改慢速解法的部分,直到达到所需的复杂度。慢速解法也可以作为压力测试的基准。


示例 - 青蛙 1 (Frog 1)

Focus Problem: 在继续之前,请尽力解决这个问题!

题目描述

题目要求我们计算青蛙从石头 $1$ 跳到石头 $N$ ($N \le 10^5$) 的最小总成本,已知青蛙只能跳一或两的距离。任意两个石头 $i$ 和 $j$ 之间的旅行成本由 $|h_i - h_j|$ 给出,其中 $h_i$ 表示石头 $i$ 的高度。


不使用动态规划

时间复杂度: $O(2^N)$

由于只有两种选择,我们可以使用递归来计算如果跳 1 块石头或 2 块石头会发生什么。有两种可能性,因此递归计算需要计算左右子树。因此,对于每增加一次跳跃,每个分支都会分裂成两个,这导致时间复杂度呈指数级。

然而,通过动态规划跟踪”最优状态”可以加快这一过程,以避免多次计算状态。例如,递归计算长度为 $1,2,1$ 和 $2,1,2$ 的跳跃会复用石头 $3$ 的状态。动态规划提供了缓存这些状态的机制。


使用动态规划

时间复杂度: $O(N)$

主要有两种动态规划方法:

  • 推动态规划 (Push DP): 根据当前状态更新未来状态
  • 拉动态规划 (Pull DP): 根据过去状态计算当前状态

推动态规划 (Push DP)

只有两种选择:跳一次或跳两次。定义 $dp[i]$ 为到达石头 $i$ 的最小成本。那么,我们的状态转移如下:

跳一个石头,成本为 $|heighti - height{i+1}|$:

跳两块石头,成本为 $|heighti - height{i+2}|$:

我们可以从基准情况 $dp[0] = 0$ 开始,因为青蛙已经在这个方格上,然后继续计算 $dp[1], dp[2], \dots, dp[N-1]$。

stone_num = int(input())
# height is 1-indexed so it can match up with dp
height = [0] + [int(s) for s in input().split()]
assert stone_num == len(height) - 1

"""
dp[N] is the minimum cost to get to the Nth stone
(we initially set all values to INF)
"""
dp = [float("inf") for _ in range(stone_num + 1)]
# dp[1] = 0 is our base case since we're already at the first stone
dp[1] = 0

for i in range(1, stone_num + 1):
    if i + 1 <= stone_num:
        dp[i + 1] = min(dp[i + 1], dp[i] + abs(height[i] - height[i + 1]))
    if i + 2 <= stone_num:
        dp[i + 2] = min(dp[i + 2], dp[i] + abs(height[i] - height[i + 2]))

print(dp[stone_num])

拉动态规划 (Pull DP)

到达石头 $i$ 有两种方式:从石头 $i-1$ 和石头 $i-2$ 来。

从石头 $i-1$ 跳跃,成本为 $|heighti - height{i-1}|$:

从石头 $i-2$ 跳跃,成本为 $|heighti - height{i-2}|$:

我们可以从基准情况 $dp[0] = 0$ 开始,因为青蛙已经在这个方格上,然后继续计算 $dp[1], dp[2], \dots, dp[N-1]$。

N = int(input())
height = [int(s) for s in input().split()]

"""
dp[N] is the minimum cost to get to the Nth stone
(we initially set all values to INF)
"""
dp = [float("inf") for _ in range(N)]
# dp[0] = 0 is our base case since we're already at the first stone
dp[0] = 0

# for each state, process the states that lead to it
for i in range(1, N):
    # jump one stone
    if i - 1 >= 0:
        dp[i] = min(dp[i], dp[i - 1] + abs(height[i] - height[i - 1]))
    # jump two stones
    if i - 2 >= 0:
        dp[i] = min(dp[i], dp[i - 2] + abs(height[i] - height[i - 2]))

print(dp[N - 1])

经典问题

接下来的几个模块将提供一些经典问题的例子:众所周知动态规划问题。然而,经典并不意味着常见。由于许多竞争者都了解这些问题,题目设计者很少直接使用它们。

注意 - DP 模块的顺序: 在开始其他 DP 模块之前,你不需要完成下面所有的问题。特别是,如果你的第一次接触 DP,我们建议你从背包模块的”简单”问题开始。


01_Introduction to DP 动态规划简介
https://mingsm17518.github.io/2026/03/21/algorithm/03_Gold/02_动态规划/01_Introduction to DP 动态规划简介/
作者
Ming
发布于
2026年3月21日
许可协议