算法中的动态规划

36 阅读4分钟

动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为更小的子问题,并利用子问题的解来高效解决原问题的算法设计方法。它通过存储中间结果避免重复计算,显著优化时间复杂度。


动态规划的核心思想

  1. 重叠子问题 (Overlapping Subproblems)
    问题可以被分解为多个重复出现的子问题。例如,计算斐波那契数列时,fib(5) 需要 fib(4) 和 fib(3),而 fib(4) 又需要 fib(3) 和 fib(2),子问题被多次重复计算。
  2. 最优子结构 (Optimal Substructure)
    问题的最优解可以通过其子问题的最优解组合得到。例如,最短路径问题中,若 A→B→C 是 A→C 的最短路径,则 A→B 和 B→C 也必须是各自段的最短路径。

动态规划的两种实现方式

1. 自顶向下(记忆化递归)

  • 从原问题出发,递归分解为子问题,并用**缓存(记忆化)**存储已计算的子问题结果。

  • 示例(斐波那契数列)

    const memo = new Map();
    
    function fib(n) {
        if (n <= 1) return n;
        if (memo.has(n)) return memo.get(n); // 直接使用缓存
        const res = fib(n - 1) + fib(n - 2);
        memo.set(n, res); // 缓存结果
        return res;
    }
    

2. 自底向上(迭代填表)

  • 从最小的子问题开始,逐步迭代求解并填充表格,直到解决原问题。

  • 示例(斐波那契数列)

    function fib(n) {
        const dp = [0, 1];
        for (let i = 2; i <= n; i++) {
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }
    

动态规划的典型步骤

  1. 定义状态
    明确 dp[i] 或 dp[i][j] 表示的含义(如:dp[i] 表示前 i 项的最优解)。

  2. 建立状态转移方程
    找到子问题之间的关系。例如,爬楼梯问题:

    dp[i]=dp[i−1]+dp[i−2]dp[i]=dp[i−1]+dp[i−2]

    (到达第 i 阶的方式数 = 从第 i-1 阶走一步 + 从第 i-2 阶走两步)

  3. 初始化边界条件
    设置初始值(如 dp[0] = 1dp[1] = 1)。

  4. 确定计算顺序
    自底向上时,需保证计算 dp[i] 时所需的子问题已解。

  5. 优化空间复杂度(可选)
    若状态仅依赖前几个子问题,可压缩存储空间。例如,斐波那契数列只需保存前两个状态。


动态规划的经典问题

问题名称状态定义状态转移方程
斐波那契数列dp[i] 表示第 i 项的值dp[i] = dp[i-1] + dp[i-2]
背包问题dp[i][w] 表示前 i 个物品、容量 w 的最大价值dp[i][w] = max(dp[i-1][w], dp[i-1][w-w_i] + v_i)
最长公共子序列 (LCS)dp[i][j] 表示 s1[0..i] 和 s2[0..j] 的 LCS 长度if (s1[i] == s2[j]) dp[i][j] = dp[i-1][j-1] + 1 else dp[i][j] = max(dp[i-1][j], dp[i][j-1])
最短路径 (Floyd-Warshall)dp[k][i][j] 表示经过前 k 个节点的 i→j 最短路径dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

动态规划 vs. 其他算法

方法特点
分治法子问题独立且不重叠(如归并排序),无记忆化。
贪心算法每一步选择当前局部最优,无法保证全局最优(如 Dijkstra 最短路径)。
动态规划通过子问题组合得到全局最优解,需满足最优子结构。

适用场景

  • 最优化问题(最大/最小值、最长/最短路径等)。
  • 问题可分解为重叠子问题(如斐波那契、背包问题)。
  • 需利用历史状态避免重复计算(如股票买卖问题)。

代码示例(爬楼梯问题)

javascript

function climbStairs(n) {
    if (n <= 2) return n;
    let a = 1, b = 2; // 仅保存前两个状态,空间复杂度 O(1)
    for (let i = 3; i <= n; i++) {
        [a, b] = [b, a + b]; // 状态转移
    }
    return b;
}

总结

动态规划通过状态定义状态转移方程,将复杂问题转化为可管理的子问题。关键在于识别重叠子问题和最优子结构,并合理设计存储与计算顺序。掌握动态规划,能高效解决许多看似棘手的算法难题。