掘金 后端 ( ) • 2024-04-20 15:35

动态规划算法的入门指南

动态规划(Dynamic Programming)是一种解决复杂问题的算法思想,它通常用于优化问题,通过将问题分解成子问题并保存其解,避免了重复计算,从而提高了算法的效率。本文将介绍动态规划算法的基本概念、解决方法和实际应用,并提供一些代码实例以帮助读者更好地理解。

什么是动态规划?

动态规划是一种在计算机科学中使用的算法思想,它通常用于解决优化问题,这类问题可以被分解成若干子问题,并且这些子问题之间存在重叠。动态规划通过将问题分解成子问题,逐步求解这些子问题,并将结果保存起来,以避免重复计算。

img

动态规划的基本原理

动态规划的基本原理可以归纳为以下几点:

  1. 分解问题:将原始问题分解成若干个子问题,这些子问题通常是相互独立的。
  2. 定义状态:确定问题的状态,即描述问题特征的变量。这些状态是动态规划算法中的关键,它们用于表示问题的不同阶段或状态。
  3. 确定状态转移方程:找到各个状态之间的转移关系,即如何通过已知状态得到新的状态。这通常是动态规划问题的核心,也是最难解决的部分。
  4. 计算最优解:通过解决子问题,逐步计算出原始问题的最优解。在计算过程中,利用状态转移方程更新状态,并保存子问题的解,以便后续使用。

动态规划的应用场景

动态规划算法在许多领域都有广泛的应用,包括但不限于:

  • 最短路径问题:如 Dijkstra 算法和 Floyd-Warshall 算法。
  • 背包问题:如 0-1 背包问题和多重背包问题。
  • 字符串匹配:如最长公共子序列(LCS)问题和编辑距离问题。
  • 数值计算:如斐波那契数列和最大子数组和问题。

动态规划算法示例

下面我们通过一个简单的示例来演示动态规划算法的应用,以求解斐波那契数列为例。

def fibonacci(n):
    if n <= 1:
        return n
    fib = [0] * (n + 1)
    fib[1] = 1
    for i in range(2, n + 1):
        fib[i] = fib[i - 1] + fib[i - 2]
    return fib[n]
​
# 测试
print(fibonacci(5))  # 输出:5
print(fibonacci(10)) # 输出:55

在上面的代码中,我们定义了一个 fibonacci 函数,该函数接受一个整数 n 作为参数,并返回斐波那契数列的第 n 个元素。我们利用动态规划的思想,使用列表 fib 来保存已经计算过的斐波那契数值,从而避免了重复计算。

image-20240420022813201

动态规划代码案例

1. 最长递增子序列(Longest Increasing Subsequence)

问题描述:给定一个整数序列,找到其中最长的严格递增子序列的长度。

def lengthOfLIS(nums):
    if not nums:
        return 0
    dp = [1] * len(nums)
    for i in range(1, len(nums)):
        for j in range(i):
            if nums[i] > nums[j]:
                dp[i] = max(dp[i], dp[j] + 1)
    return max(dp)
​
# 测试
print(lengthOfLIS([10, 9, 2, 5, 3, 7, 101, 18]))  # 输出:4

2. 最大子序和(Maximum Subarray)

问题描述:给定一个整数数组,找到一个具有最大和的连续子数组(子数组要求至少包含一个元素),返回其最大和。

def maxSubArray(nums):
    if not nums:
        return 0
    max_sum = nums[0]
    curr_sum = nums[0]
    for num in nums[1:]:
        curr_sum = max(num, curr_sum + num)
        max_sum = max(max_sum, curr_sum)
    return max_sum
​
# 测试
print(maxSubArray([-2, 1, -3, 4, -1, 2, 1, -5, 4]))  # 输出:6

3. 硬币找零(Coin Change)

image-20240420022843771

问题描述:给定不同面额的硬币和一个总金额,计算出可以凑成总金额所需的最少硬币数量。如果无法凑成,则返回 -1。

def coinChange(coins, amount):
    dp = [float('inf')] * (amount + 1)
    dp[0] = 0
    for coin in coins:
        for i in range(coin, amount + 1):
            dp[i] = min(dp[i], dp[i - coin] + 1)
    return dp[amount] if dp[amount] != float('inf') else -1
​
# 测试
print(coinChange([1, 2, 5], 11))  # 输出:3

这些代码示例展示了动态规划在不同类型问题上的应用,包括求最长递增子序列长度、最大子序和以及硬币找零问题。通过理解这些示例,读者可以更深入地掌握动态规划算法,并在解决实际问题时应用它们。

4. 最长公共子序列(Longest Common Subsequence)

问题描述:给定两个字符串 s1 和 s2,找到它们的最长公共子序列的长度。

def longestCommonSubsequence(text1, text2):
    m, n = len(text1), len(text2)
    dp = [[0] * (n + 1) for _ in range(m + 1)]
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            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]
​
# 测试
print(longestCommonSubsequence("abcde", "ace"))  # 输出:3

5. 0-1 背包问题(0-1 Knapsack Problem)

问题描述:给定一个背包容量和一组物品,每个物品有重量和价值,要求在背包容量限制下,选择物品使得背包内物品的总价值最大。

def knapsack(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    for i in range(1, n + 1):
        for j in range(1, capacity + 1):
            if weights[i - 1] <= j:
                dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
            else:
                dp[i][j] = dp[i - 1][j]
    return dp[n][capacity]
​
# 测试
weights = [1, 2, 3]
values = [6, 10, 12]
capacity = 5
print(knapsack(weights, values, capacity))  # 输出:22

这些代码示例展示了动态规划在更加复杂的问题上的应用,包括求最长公共子序列长度和解决0-1背包问题。通过理解这些示例,读者可以更深入地掌握动态规划算法,并在解决实际问题时应用它们。

image-20240420022852163

6.最长回文子序列(Longest Palindromic Subsequence)

问题描述:给定一个字符串,找到它的最长回文子序列的长度。

def longestPalindromeSubseq(s):
    n = len(s)
    dp = [[0] * n for _ in range(n)]
    for i in range(n - 1, -1, -1):
        dp[i][i] = 1
        for j in range(i + 1, n):
            if s[i] == s[j]:
                dp[i][j] = dp[i + 1][j - 1] + 2
            else:
                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1])
    return dp[0][n - 1]
​
# 测试
print(longestPalindromeSubseq("bbbab"))  # 输出:4

7. 最小路径和(Minimum Path Sum)

问题描述:给定一个二维网格,其中每个格子包含一个非负整数,找到一条从左上角到右下角的路径,使得路径上的数字总和最小。

def minPathSum(grid):
    m, n = len(grid), len(grid[0])
    dp = [[0] * n for _ in range(m)]
    dp[0][0] = grid[0][0]
    for i in range(1, m):
        dp[i][0] = dp[i - 1][0] + grid[i][0]
    for j in range(1, n):
        dp[0][j] = dp[0][j - 1] + grid[0][j]
    for i in range(1, m):
        for j in range(1, n):
            dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
    return dp[m - 1][n - 1]
​
# 测试
grid = [
  [1, 3, 1],
  [1, 5, 1],
  [4, 2, 1]
]
print(minPathSum(grid))  # 输出:7

这些代码示例展示了动态规划在更加复杂的问题上的应用,包括求解最长回文子序列和最小路径和问题。通过理解这些示例,读者可以更深入地掌握动态规划算法,并在解决实际问题时应用它们。

动态规划的进阶应用

image-20240420022902484

除了基本的动态规划问题外,还存在一些更为复杂的情况和进阶应用。下面我们将简要介绍一些常见的动态规划进阶技巧和应用场景。

1. 状态压缩

在一些问题中,状态的数量可能非常庞大,导致内存消耗过高。为了减少内存使用,可以使用状态压缩技巧。例如,可以使用位运算来表示状态,从而将状态空间压缩到一个整数中。

2. 滚动数组

滚动数组是一种优化动态规划算法的常用技巧。它通过只保存必要的历史状态,来降低内存消耗。在某些问题中,我们只需要利用最近的几个状态就可以计算出当前状态,因此可以只保存这些必要的状态,而不必保存所有状态的历史记录。

3. 区间动态规划

区间动态规划是一种用于解决区间型问题的动态规划方法。在这类问题中,通常需要找到一个区间或多个区间,使得某种目标函数达到最大值或最小值。典型的例子包括最长回文子序列、最长上升子序列等。

4. 带权并查集

带权并查集是一种用于解决动态规划问题的数据结构。在一些问题中,除了维护元素之间的关系外,还需要维护每个集合的权值。带权并查集可以高效地支持这类操作,从而简化动态规划问题的求解过程。

动态规划的实际应用

动态规划算法在实际应用中有着广泛的应用,涵盖了各个领域。下面我们列举一些常见的实际应用场景:

  • 路径规划:在机器人导航、交通流量优化等领域中,动态规划被广泛应用于路径规划和最短路径搜索。
  • 资源分配:在生产调度、资源分配等问题中,动态规划可用于优化资源的分配方案,以最大化效益或最小化成本。
  • 金融领域:在股票交易、投资组合优化等领域中,动态规划可用于制定最优的投资策略,以最大化收益或最小化风险。
  • 自然语言处理:在文本分析、机器翻译等领域中,动态规划可用于解决字符串匹配、序列标注等问题。

image-20240420022927384

总结

本文介绍了动态规划算法的基本原理、应用场景以及常见的代码示例。动态规划是一种解决优化问题的强大算法思想,通过将问题分解成子问题并保存其解,避免了重复计算,从而提高了算法的效率。主要内容如下:

  1. 动态规划的基本原理:分解问题、定义状态、确定状态转移方程和计算最优解是动态规划算法的核心步骤。通过这些步骤,我们可以有效地解决复杂的优化问题。
  2. 动态规划的应用场景:动态规划算法在各个领域都有广泛的应用,包括最短路径问题、背包问题、字符串匹配、数值计算等。它们为问题的求解提供了有效的解决方案。
  3. 动态规划的代码示例:本文提供了多个动态规划问题的代码示例,包括最长递增子序列、最大子序和、硬币找零、最长公共子序列、0-1背包问题、最长回文子序列和最小路径和等。通过这些示例,读者可以更深入地理解动态规划算法的应用。

综上所述,动态规划算法是一种十分强大的算法思想,通过将问题分解成子问题并利用状态转移方程求解,可以高效地解决各种复杂的优化问题。希望本文能够帮助读者更好地理解动态规划算法,并在实践中灵活运用它们解决问题。