动态规划算法
动态规划算法
你想了解的动态规划(Dynamic Programming,简称DP),是编程中解决多阶段决策最优解问题的核心算法,核心思路是“化整为零、记忆复用”——把复杂的大问题拆解成可重复解决的小问题,记录小问题的解(避免重复计算),最终组合出大问题的最优解。
相比于贪心算法“只看当下”的局部最优,动态规划更注重“全局规划”,能解决贪心算法处理不了的、存在“后效性”的问题(比如硬币找零的非标准面额、最长公共子序列等)。
一、动态规划的核心原理
1. 核心思想
动态规划的本质是:
- 分解子问题:将原问题拆分为若干个重叠的子问题(子问题的解会被多次用到);
- 记忆化存储:用数组/哈希表记录子问题的最优解(称为“DP表”),避免重复计算;
- 状态转移:通过子问题的最优解,推导更大问题的最优解(核心是“状态转移方程”)。
可以用生活例子理解:你要爬10级台阶(每次走1或2级),求有多少种走法。直接算“10级”很难,但可以先算“1级”“2级”的走法,再通过“3级=2级+1级”“4级=3级+2级”……逐步推导出10级的解——这就是动态规划的“递推”思路。
2. 关键前提(适用条件)
动态规划只适用于满足以下两个条件的问题:
- 最优子结构:原问题的最优解包含其子问题的最优解(和贪心算法相同);
- 无后效性:子问题的解一旦确定,不受后续决策的影响(比如“爬台阶到第5级的走法数”,不会因为后续走6级/7级而改变);
- 子问题重叠:子问题会被重复计算(这是用DP的意义——避免重复计算,提升效率)。
二、动态规划的通用步骤
动态规划没有固定代码模板,但解决问题的思路可总结为5步,其中“状态定义”和“状态转移方程”是核心:
步骤1:定义状态(DP表的含义)
明确“DP表的维度+每个位置的值代表什么”。
- 一维DP:
dp[i]通常表示“前i个元素/第i个位置的最优解”(如爬台阶:dp[i]=爬i级台阶的走法数); - 二维DP:
dp[i][j]通常表示“前i个元素和前j个元素的最优解”(如最长公共子序列:dp[i][j]=字符串A前i位和字符串B前j位的最长公共子序列长度)。
步骤2:确定初始条件(DP表的起点)
初始化DP表中“最小子问题”的解(无法再拆分的子问题)。
- 例:爬台阶中,
dp[1] = 1(1级台阶只有1种走法),dp[2] = 2(2级台阶有“1+1”“2”两种走法)。
步骤3:推导状态转移方程(核心)
这是动态规划的“灵魂”——描述如何通过子问题的解推导当前问题的解。
- 例:爬台阶中,
dp[i] = dp[i-1] + dp[i-2](第i级台阶的走法数=第i-1级的走法数(最后走1级) + 第i-2级的走法数(最后走2级))。
步骤4:确定DP表的遍历顺序
保证计算dp[i]时,dp[i-1]/dp[i-2]等子问题的解已经被计算完成。
- 例:爬台阶需从左到右遍历(先算
dp[1]→dp[2]→…→dp[n])。
步骤5:计算最终结果
根据DP表的定义,找到对应位置的值作为答案。
- 例:爬台阶的答案是
dp[n](n为总台阶数)。
三、经典示例1:爬台阶问题(一维DP)
问题描述
有n级台阶,每次可以走1级或2级,求走到第n级台阶有多少种不同的走法。
完整代码实现(Python)
def climb_stairs(n):
"""
动态规划解决爬台阶问题
:param n: 台阶总数
:return: 走法总数
"""
# 处理边界情况
if n <= 2:
return n
# 步骤1:定义状态 - dp[i]表示爬i级台阶的走法数
dp = [0] * (n + 1)
# 步骤2:初始化初始条件
dp[1] = 1
dp[2] = 2
# 步骤3+4:状态转移 + 遍历顺序(从左到右)
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # 状态转移方程
# 步骤5:返回最终结果
return dp[n]
# 测试用例
if __name__ == "__main__":
print(climb_stairs(3)) # 输出:3(1+1+1、1+2、2+1)
print(climb_stairs(5)) # 输出:8
代码解释
- 状态定义:
dp[i]代表爬i级台阶的走法数; - 初始条件:
dp[1]=1、dp[2]=2(最小子问题的解); - 状态转移:
dp[i] = dp[i-1] + dp[i-2](第i级的走法=前1级走1步 + 前2级走2步); - 遍历顺序:从3到n(保证计算
dp[i]时,dp[i-1]和dp[i-2]已算出)。
四、经典示例2:硬币找零问题(贪心失效时用DP)
问题描述
给定硬币面额coins = [1,3,4],目标金额amount = 6,求凑出目标金额所需的最少硬币数(贪心选最大面额会得到4+1+1=3枚,而最优解是3+3=2枚)。
完整代码实现(Python)
def coin_change(coins, amount):
"""
动态规划解决硬币找零问题(最少硬币数)
:param coins: 硬币面额列表
:param amount: 目标金额
:return: 最少硬币数(无法凑出返回-1)
"""
# 步骤1:定义状态 - dp[i]表示凑出金额i所需的最少硬币数
# 初始化DP表:大小为amount+1,初始值设为amount+1(表示“不可达”,因为最多需要amount枚1元硬币)
dp = [amount + 1] * (amount + 1)
# 步骤2:初始化初始条件 - 凑出0元需要0枚硬币
dp[0] = 0
# 步骤3+4:状态转移 + 遍历顺序(从小到大遍历金额)
for i in range(1, amount + 1):
# 遍历所有硬币面额,尝试用当前硬币更新dp[i]
for coin in coins:
if coin <= i: # 硬币面额不超过当前金额时才可用
# 状态转移方程:dp[i] = min(原来的dp[i], 凑i-coin的最少硬币数 + 1枚当前硬币)
dp[i] = min(dp[i], dp[i - coin] + 1)
# 步骤5:返回结果(若dp[amount]仍为amount+1,说明无法凑出)
return dp[amount] if dp[amount] <= amount else -1
# 测试用例
if __name__ == "__main__":
coins = [1, 3, 4]
amount = 6
print(coin_change(coins, amount)) # 输出:2(3+3)
代码解释
- 状态定义:
dp[i]代表凑出金额i的最少硬币数; - 初始条件:
dp[0] = 0(0元无需硬币),其余初始为amount+1(标记为“暂时不可达”); - 状态转移:对每个金额
i,遍历所有硬币,若硬币面额coin ≤ i,则dp[i] = min(dp[i], dp[i-coin]+1)(用i-coin的解+1枚当前硬币,更新最优解); - 结果判断:若最终
dp[amount]仍大于amount,说明无法凑出,返回-1。
五、动态规划与贪心算法的对比
| 特性 | 动态规划 | 贪心算法 |
|---|---|---|
| 核心思路 | 全局规划,复用子问题解 | 局部最优,一步到位 |
| 适用场景 | 子问题重叠、有后效性 | 无后效性、贪心选择性质 |
| 时间复杂度 | 通常O(n²)/O(nm)(需遍历子问题) | 通常O(n)/O(n log n)(效率更高) |
| 结果 | 保证全局最优 | 仅满足条件时全局最优 |
总结
- 核心逻辑:动态规划的关键是状态定义和状态转移方程,通过拆分重叠子问题、记忆化存储解,避免重复计算,最终得到全局最优;
- 适用条件:问题需满足“最优子结构”“无后效性”“子问题重叠”;
- 实现步骤:定义状态→初始化初始条件→推导状态转移方程→确定遍历顺序→计算最终结果。
动态规划的难点在于“状态转移方程的推导”,需要多练习经典问题(如最长公共子序列、01背包、编辑距离)来掌握核心思路。

浙公网安备 33010602011771号