微信登录

动态规划概述 - 适用条件 - 动态规划的适用情况

动态规划概述 - 适用条件 - 动态规划的适用情况

一、动态规划概述

动态规划(Dynamic Programming,简称 DP)是一种通过把原问题分解为相对简单的子问题,并保存子问题的解来避免重复计算,从而解决复杂问题的算法策略。它的核心思想在于将大问题拆分成小问题,通过求解小问题的最优解来逐步构建出大问题的最优解。

动态规划并非是一种具体的算法,而是一种解决问题的思路和方法,它广泛应用于计算机科学、运筹学、经济学等多个领域,如背包问题、最长公共子序列问题、矩阵链乘法问题等。

动态规划的基本要素

  1. 最优子结构:一个问题的最优解包含其子问题的最优解。也就是说,大问题的最优解可以由小问题的最优解组合而成。
  2. 子问题重叠:在求解过程中,会多次重复计算相同的子问题。动态规划通过保存这些子问题的解,避免了重复计算,从而提高了算法的效率。

二、动态规划的适用条件

1. 最优子结构性质

最优子结构是动态规划适用的首要条件。当一个问题可以分解为若干个子问题,并且原问题的最优解包含了子问题的最优解时,就具备了最优子结构性质。

例如,在计算斐波那契数列时,我们知道斐波那契数列的定义为:$F(n) = F(n - 1) + F(n - 2)$,其中 $F(0) = 0$,$F(1) = 1$。要求 $F(n)$ 的值,我们可以先求出 $F(n - 1)$ 和 $F(n - 2)$ 的值,而 $F(n)$ 的最优解(这里就是其准确值)是由 $F(n - 1)$ 和 $F(n - 2)$ 的最优解(即它们各自的值)组合而成的,所以斐波那契数列问题具有最优子结构性质。

2. 子问题重叠性质

子问题重叠意味着在求解过程中会多次遇到相同的子问题。如果不进行优化,这些子问题会被重复计算,导致算法效率低下。动态规划通过使用表格(数组)来保存已经计算过的子问题的解,当再次遇到相同的子问题时,直接从表格中获取结果,避免了重复计算。

以斐波那契数列为例,如果我们使用递归方法来计算 $F(n)$,会发现很多子问题被重复计算。比如计算 $F(5)$ 时,会计算 $F(3)$ 和 $F(4)$,而计算 $F(4)$ 时又会再次计算 $F(3)$。如果使用动态规划,我们可以创建一个数组来保存已经计算过的 $F(i)$ 的值,这样就避免了重复计算。

3. 无后效性

无后效性是指某个阶段的状态一旦确定,就不受这个状态以后决策的影响。也就是说,某状态以后的过程不会影响以前的状态,只与当前状态有关。

例如,在背包问题中,我们在考虑放入第 $i$ 个物品时,只需要考虑当前背包的剩余容量和前 $i - 1$ 个物品的放置情况,而不需要考虑后续物品的放置情况对当前状态的影响。

三、动态规划的适用情况

1. 背包问题

背包问题是动态规划的经典应用之一,它有多种变体,如 0 - 1 背包问题、完全背包问题、多重背包问题等。

0 - 1 背包问题:有一个容量为 $C$ 的背包和 $n$ 个物品,每个物品有自己的重量 $w_i$ 和价值 $v_i$,要求在不超过背包容量的前提下,选择一些物品放入背包,使得背包中物品的总价值最大。每个物品只能选择放入或不放入背包(即 0 - 1 选择)。

我们可以定义一个二维数组 $dp[i][j]$ 表示前 $i$ 个物品放入容量为 $j$ 的背包中所能获得的最大价值。状态转移方程为:
[
dp[i][j] =
\begin{cases}
dp[i - 1][j] & \text{if } w_i > j \
\max(dp[i - 1][j], dp[i - 1][j - w_i] + v_i) & \text{if } w_i \leq j
\end{cases}
]

以下是一个简单的 Python 代码示例:

  1. def knapsack(weights, values, capacity):
  2. n = len(weights)
  3. dp = [[0 for _ in range(capacity + 1)] for _ in range(n + 1)]
  4. for i in range(1, n + 1):
  5. for j in range(1, capacity + 1):
  6. if weights[i - 1] > j:
  7. dp[i][j] = dp[i - 1][j]
  8. else:
  9. dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1])
  10. return dp[n][capacity]
  11. weights = [2, 3, 4, 5]
  12. values = [3, 4, 5, 6]
  13. capacity = 8
  14. print(knapsack(weights, values, capacity))

2. 最长公共子序列问题

给定两个序列 $X = {x_1, x_2, \cdots, x_m}$ 和 $Y = {y_1, y_2, \cdots, y_n}$,求它们的最长公共子序列的长度。

我们可以定义一个二维数组 $dp[i][j]$ 表示 $X$ 的前 $i$ 个元素和 $Y$ 的前 $j$ 个元素的最长公共子序列的长度。状态转移方程为:
[
dp[i][j] =
\begin{cases}
dp[i - 1][j - 1] + 1 & \text{if } x_i = y_j \
\max(dp[i - 1][j], dp[i][j - 1]) & \text{if } x_i \neq y_j
\end{cases}
]

以下是一个简单的 Python 代码示例:

  1. def longest_common_subsequence(X, Y):
  2. m = len(X)
  3. n = len(Y)
  4. dp = [[0 for _ in range(n + 1)] for _ in range(m + 1)]
  5. for i in range(1, m + 1):
  6. for j in range(1, n + 1):
  7. if X[i - 1] == Y[j - 1]:
  8. dp[i][j] = dp[i - 1][j - 1] + 1
  9. else:
  10. dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
  11. return dp[m][n]
  12. X = "AGGTAB"
  13. Y = "GXTXAYB"
  14. print(longest_common_subsequence(X, Y))

3. 矩阵链乘法问题

给定 $n$ 个矩阵 $A1, A_2, \cdots, A_n$,其中 $A_i$ 的维度为 $p{i - 1} \times p_i$,要求确定矩阵链的乘法顺序,使得总的标量乘法次数最少。

我们可以定义一个二维数组 $dp[i][j]$ 表示矩阵链 $Ai A{i + 1} \cdots Aj$ 的最少标量乘法次数。状态转移方程为:
[
dp[i][j] =
\begin{cases}
0 & \text{if } i = j \
\min
{i \leq k < j} {dp[i][k] + dp[k + 1][j] + p_{i - 1}p_k p_j} & \text{if } i < j
\end{cases}
]

四、总结

适用条件 解释
最优子结构性质 原问题的最优解包含子问题的最优解
子问题重叠性质 求解过程中会多次重复计算相同的子问题
无后效性 某个阶段的状态一旦确定,不受后续决策影响

动态规划是一种强大的算法策略,但并非适用于所有问题。在面对一个问题时,我们需要判断它是否满足动态规划的适用条件,如果满足,则可以尝试使用动态规划来解决,通过合理定义状态和状态转移方程,往往能够高效地解决复杂问题。