动态规划简介
动态编程通过将解决方案与子问题相结合来解决问题。它可以类似于分而治之的方法,其中问题被划分为不相交的子问题,子问题被递归地求解,然后组合以找到原始问题的解。相反,动态编程在子问题重叠时适用 - 也就是说,当子问题共享子问题时。在这种情况下,分而治之的算法比必要的工作更多,重复解决常见的子问题。动态编程算法只解决每个子子问题一次,然后将其答案保存在表中,从而避免每次解决每个子子问题时重新计算答案的工作。
我们来看一个例子。我们通常称为 Fibonacci 的意大利数学家 Leonardo Pisano Bigollo 通过考虑兔子种群的理想化增长发现了一系列。该系列是:
1, 1, 2, 3, 5, 8, 13, 21, ......
我们可以注意到前两个后面的每个数字都是前面两个数字的总和。现在,让我们制定一个函数 F(n) ,它将返回第 n 个 Fibonacci 数,这意味着,
F(n) = nth Fibonacci Number
到目前为止,我们已经知道,
F(1) = 1
F(2) = 1
F(3) = F(2) + F(1) = 2
F(4) = F(3) + F(2) = 3
我们可以通过以下方式概括:
F(1) = 1
F(2) = 1
F(n) = F(n-1) + F(n-2)
现在,如果我们想将它写为递归函数,我们将 F(1) 和 F(2) 作为基本情况。所以我们的 Fibonacci 函数将是:
Procedure F(n): //A function to return nth Fibonacci Number
if n is equal to 1
Return 1
else if n is equal to 2
Return 1
end if
Return F(n-1) + F(n-2)
现在,如果我们调用 F(6),它将调用 F(5) 和 F(4),这将调用更多。让我们以图形方式表示:
https://i.stack.imgur.com/ngSbS.jpg
从图中可以看出,F(6) 将调用 F(5) 和 F(4)。现在 F(5) 将调用 F(4) 和 F(3)。在计算 F(5) 之后,我们可以肯定地说已经计算了 F(5) 调用的所有函数。这意味着,我们已经计算了 F(4)。但我们再次计算 F(4),因为 F(6) 的右孩子指示。我们真的需要重新计算吗?我们可以做的是,一旦我们计算了 F(4) 的值,我们就将它存储在一个名为 dp 的数组中,并在需要时重用它。我们将使用 -1 (或我们计算中不会出现的任何值) 初始化我们的 dp 数组。然后我们将调用 F(6) ,其中我们修改的 F(n) 将如下所示: **** **** ****
Procedure F(n):
if n is equal to 1
Return 1
else if n is equal to 2
Return 1
else if dp[n] is not equal to -1 //That means we have already calculated dp[n]
Return dp[n]
else
dp[n] = F(n-1) + F(n-2)
Return dp[n]
end if
我们完成了与以前相同的任务,但只进行了简单的优化。也就是说,我们使用了 memoization 技术。首先, dp 数组的所有值都是 -1 。当调用 F(4) 时,我们检查它是否为空。如果它存储 -1 ,我们将计算其值并将其存储在 dp [4]中。如果它存储除 -1 以外的任何东西,那意味着我们已经计算了它的值。所以我们只需返回值。
使用 memoization 的这种简单优化称为动态编程。
如果动态编程具有某些特征,则可以解决该问题。这些是:
- 子问题:
DP 问题可以分为一个或多个子问题。例如:F(4)可以分为更小的子问题F(3)和F(2)。由于子问题类似于我们的主要问题,这些可以使用相同的技术来解决。 - 重叠子问题:
DP 问题必须具有重叠的子问题。这意味着必须有一些共同的部分,不止一次调用相同的函数。例如:F(5)和F(6)共有F(3)和F(4)。这就是我们将值存储在数组中的原因。
https://i.stack.imgur.com/7OVJm.jpg
- 最佳子结构:
假设你被要求最小化功能g(x)。你知道g(x)的价值取决于g(y)和g(z)。现在,如果我们可以通过最小化g(y)和g(z)来最小化g(x),那么我们就可以说问题具有最优的子结构。如果通过仅最小化g(y)来最小化g(x)并且如果最小化或最大化g(z)对g(x)没有任何影响,则该问题不具有最佳子结构。简单来说,如果问题的最优解可以从其子问题的最优解中找到,那么我们可以说问题具有最优的子结构性质。