跳转到内容

自底向上法(动态规划)

来自代码酷


自底向上法(Bottom-Up Approach)是动态规划中解决子问题依赖关系的核心方法之一,与自顶向下法(记忆化递归)形成对比。该方法通过从最小子问题开始逐步构建最终解,避免了递归带来的栈开销,通常具有更优的空间效率。

核心思想[编辑 | 编辑源代码]

自底向上法的核心是:

  1. 定义基础状态:解决最小规模的子问题(通常是递归终止条件)。
  2. 递推关系:用已解决的子问题结果推导更大规模问题的解。
  3. 顺序填充:按问题规模从小到大计算并存储结果(通常使用数组或表格)。

数学表达为: dp[i]=F(dp[i1],dp[i2],) 其中F表示状态转移方程。

与自顶向下法的对比[编辑 | 编辑源代码]

方法对比
特性 自底向上法 自顶向下法
计算顺序 从小问题到大问题 从大问题分解到小问题
存储结构 通常用数组/矩阵 递归调用+记忆化
栈空间 无递归栈开销 可能栈溢出
适用场景 明确依赖顺序的问题 依赖关系复杂的问题

经典示例:斐波那契数列[编辑 | 编辑源代码]

问题描述[编辑 | 编辑源代码]

计算第n个斐波那契数: F(n)={0n=01n=1F(n1)+F(n2)n>1

自底向上实现[编辑 | 编辑源代码]

def fibonacci(n):
    if n == 0:
        return 0
    dp = [0] * (n + 1)
    dp[1] = 1
    for i in range(2, n + 1):
        dp[i] = dp[i - 1] + dp[i - 2]
    return dp[n]

# 示例
print(fibonacci(5))  # 输出: 5

空间优化[编辑 | 编辑源代码]

观察到只需前两个状态:

def fibonacci_optimized(n):
    if n < 2:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

实际应用:零钱兑换[编辑 | 编辑源代码]

问题描述[编辑 | 编辑源代码]

给定硬币面额coins=[c1,c2,,cm]和目标金额amount,求凑成金额的最少硬币数。

解法[编辑 | 编辑源代码]

定义dp[i]为金额i的最小硬币数: 解析失败 (语法错误): {\displaystyle dp[i] = \min_{\substack{c \in coins \\ i \geq c}}(dp[i - c] + 1) }

代码实现[编辑 | 编辑源代码]

def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for i in range(1, amount + 1):
        for c in coins:
            if i >= c:
                dp[i] = min(dp[i], dp[i - c] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1

# 示例
print(coinChange([1, 2, 5], 11))  # 输出: 3 (5+5+1)

动态规划表可视化[编辑 | 编辑源代码]

graph LR A[金额 0] -->|0| B[金额 1] B -->|1| C[金额 2] C -->|1| D[金额 3] D -->|2| E[金额 4] E -->|2| F[金额 5] F -->|1| G[...]

复杂度分析[编辑 | 编辑源代码]

  • 时间复杂度:通常为O(n×k)n为问题规模,k为状态转移成本)
  • 空间复杂度:通常为O(n),可优化到O(1)(如斐波那契数列)

适用问题特征[编辑 | 编辑源代码]

1. 最优子结构:问题的最优解包含子问题的最优解 2. 无后效性:当前状态只与之前状态有关 3. 重叠子问题:子问题被重复计算

进阶技巧[编辑 | 编辑源代码]

  • 滚动数组:将空间复杂度从O(n)降至O(1)
  • 维度分解:处理多维动态规划时逐维度计算
  • 状态压缩:用位运算表示状态(如旅行商问题)

常见错误[编辑 | 编辑源代码]

页面模块:Message box/ambox.css没有内容。