动态规划算法

动态规划算法

你想了解的动态规划(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

代码解释

  1. 状态定义dp[i] 代表爬i级台阶的走法数;
  2. 初始条件dp[1]=1dp[2]=2(最小子问题的解);
  3. 状态转移dp[i] = dp[i-1] + dp[i-2](第i级的走法=前1级走1步 + 前2级走2步);
  4. 遍历顺序:从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)

代码解释

  1. 状态定义dp[i] 代表凑出金额i的最少硬币数;
  2. 初始条件dp[0] = 0(0元无需硬币),其余初始为amount+1(标记为“暂时不可达”);
  3. 状态转移:对每个金额i,遍历所有硬币,若硬币面额coin ≤ i,则dp[i] = min(dp[i], dp[i-coin]+1)(用i-coin的解+1枚当前硬币,更新最优解);
  4. 结果判断:若最终dp[amount]仍大于amount,说明无法凑出,返回-1。

五、动态规划与贪心算法的对比

特性 动态规划 贪心算法
核心思路 全局规划,复用子问题解 局部最优,一步到位
适用场景 子问题重叠、有后效性 无后效性、贪心选择性质
时间复杂度 通常O(n²)/O(nm)(需遍历子问题) 通常O(n)/O(n log n)(效率更高)
结果 保证全局最优 仅满足条件时全局最优

总结

  1. 核心逻辑:动态规划的关键是状态定义状态转移方程,通过拆分重叠子问题、记忆化存储解,避免重复计算,最终得到全局最优;
  2. 适用条件:问题需满足“最优子结构”“无后效性”“子问题重叠”;
  3. 实现步骤:定义状态→初始化初始条件→推导状态转移方程→确定遍历顺序→计算最终结果。

动态规划的难点在于“状态转移方程的推导”,需要多练习经典问题(如最长公共子序列、01背包、编辑距离)来掌握核心思路。

posted @ 2026-01-22 10:37  aisuanfa  阅读(7)  评论(0)    收藏  举报