Skip to content

动态规划(Dynamic Programming,简称 DP)是一种用于求解具有最优子结构和重叠子问题的算法技术。它通常通过分治法递归地求解问题的子问题,并保存子问题的结果以避免重复计算。动态规划的核心是将原问题分解为子问题,利用子问题的解构建原问题的解。

动态规划的步骤:

  1. 定义状态:确定子问题的解应该用什么样的变量表示。
  2. 状态转移方程:找到每个状态之间的关系,即如何通过之前的状态来推导当前状态。
  3. 初始化:确定最初的状态(通常是边界条件)。
  4. 计算顺序:按照合理的顺序求解每个子问题,通常是从小问题到大问题。
  5. 求解最终结果:通过状态转移的过程,得到原问题的解。

动态规划的时间复杂度分析:

  1. 时间复杂度(Big O):动态规划的时间复杂度通常取决于状态的数量和每个状态的转移操作。若问题有 n 个子问题,每个子问题有 k 种状态转移,则时间复杂度为 O(n * k)。但具体的时间复杂度需要根据具体问题的复杂度来分析。

动态规划的经典问题:

  1. 斐波那契数列: 计算第 n 个斐波那契数,经典的动态规划问题。

    递归方式

    go
    func fibonacci(n int) int {
        if n <= 1 {
            return n
        }
        return fibonacci(n-1) + fibonacci(n-2)
    }

    动态规划方式

    go
    func fibonacci(n int) int {
        if n <= 1 {
            return n
        }
        dp := make([]int, n+1)
        dp[0], dp[1] = 0, 1
        for i := 2; i <= n; i++ {
            dp[i] = dp[i-1] + dp[i-2]
        }
        return dp[n]
    }

    时间复杂度

    • 递归方式:O(2^n),因为每个问题都被重复计算了。
    • 动态规划方式:O(n),因为每个子问题只计算一次。
  2. 背包问题(0/1 背包问题): 给定一个背包容量和一组物品,每个物品有一个重量和价值,求在不超过背包容量的情况下,能获得的最大价值。

    动态规划解法

    go
    func knapsack(weights, values []int, capacity int) int {
        n := len(weights)
        dp := make([]int, capacity+1)
        for i := 0; i < n; i++ {
            for w := capacity; w >= weights[i]; w-- {
                dp[w] = max(dp[w], dp[w-weights[i]]+values[i])
            }
        }
        return dp[capacity]
    }
    
    func max(a, b int) int {
        if a > b {
            return a
        }
        return b
    }

    时间复杂度

    • O(n * W),其中 n 是物品数量,W 是背包的容量。
  3. 最长公共子序列(LCS): 给定两个字符串,求它们的最长公共子序列(不要求连续)。

    动态规划解法

    go
    func longestCommonSubsequence(text1, text2 string) int {
        m, n := len(text1), len(text2)
        dp := make([][]int, m+1)
        for i := range dp {
            dp[i] = make([]int, n+1)
        }
        for i := 1; i <= m; i++ {
            for j := 1; j <= n; j++ {
                if text1[i-1] == text2[j-1] {
                    dp[i][j] = dp[i-1][j-1] + 1
                } else {
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1])
                }
            }
        }
        return dp[m][n]
    }

    时间复杂度

    • O(m * n),其中 mn 分别是两个字符串的长度。

总结:

动态规划的关键是识别出具有最优子结构的问题,并通过状态转移来避免重复计算。每个动态规划问题的时间复杂度往往与状态数量和每个状态的转移方式密切相关。

动态规划在很多问题中都能显著提高性能,避免暴力递归的重复计算,从而降低时间复杂度。如果你有具体的算法问题,可以进一步探讨如何应用动态规划优化。