动态编程
动态编程指的是一种解决问题的方法,在这种方法中,我们预先计算和存储更简单的、类似的子问题,以建立一个复杂问题的解。它类似于递归,计算基本情况允许我们感应地确定最终值。当新值仅取决于先前计算的值时,此自下而上的方法很好。
通过动态编程解决的问题的一个重要属性是它应该具有重叠的子问题。这是区分DP的形式划分和征服其中存储更简单的值并不需要。
为了表明该技术的强大,这里是通过动态编程通常接近的一些最着名的问题:
- 背包问题:给出了一套具有已知价值观和权重的宝藏,您应该选择哪一个,以最大限度地提高您的利润,而不会损坏您的背包有固定的容量?
- 鸡蛋滴:下降的最佳方式是什么? 来自A.的鸡蛋 - 下台建筑物弄清楚爆裂时鸡蛋的最低高度?
- 最长的常见子序列:考虑到两个序列,这是两者中共同的最长的子序列?
- 子集合问题:给定一个和一个值 是否存在一个子集,其元素的和是
- 斐波纳契号:是否有更好的方法来计算比平原递归的斐波纳契数?
在一个比赛环境中,无论参赛者如何熟悉,动态编程几乎总是出现(通常以令人惊讶的方式)。
动机例子:改变硬币
什么是值的最小数量 需要总共的金额
您可以多次使用面额。
最佳子结构
这一问题的最重要方面鼓励我们通过动态编程来解决这个问题是它可以简化为较小的子问题。
让 表示价值所需的最小硬币数量 .
可视化 作为一堆硬币。堆栈顶部的硬币是什么?它可能是任何一个 .如果它是 ,剩下的堆栈将相当于 或者如果是 ,剩下的堆栈将相当于 , 等等。
我们如何决定是什么?果然,我们还不知道。我们需要查看哪些最小化所需的硬币数量。
根据上述论点,我们可以将问题表述如下:
因为堆叠顶部的硬币也被称为一个硬币,然后我们可以看看其余部分。
重叠的子问题
很容易看到子问题可以重叠。
例如,如果我们试图使用$1、$2和$5来创建一个由$11组成的堆栈,我们的查找模式将是这样的:
很清楚,我们需要使用价值 几次。
优化我们的算法的最重要方面之一是我们不会重新计算这些值。为此,我们计算并存储所有值 从1开始,潜在的未来使用。
边缘案例
递归在某处换句话说,换句话说,在它可以开始的已知价值。
对于这个问题,我们需要照顾两件事:
零:这很清楚 由于我们不需要任何硬币,以使堆栈相当于0。
负值和不可达值:处理此类价值的一种方法是将它们标记为Sentinel值,以便我们的代码以特殊方式处理它们。一个好的选择哨兵是 ,由于可达值之间的最小值和 永远不会是无限的。
算法
让我们总结一下这些想法,看看我们如何将其作为一个实际的算法来实现:
1 2 3 4 5 6 7 8 9 |
|
递归与备忘录
我们已经声称,Naive递归是解决重叠子标数问题的不良方式。这是为什么?主要是因为所涉及的所有重新计算。
另一种避免此问题的方法是首次计算数据并以自上而下的方式将其存储在一起。
让我们来看看如何以备忘方式解决先前的硬币改变问题。
1 2 3 4 5 6 7 8 9 10 11 12defcoinschange.(V.那V.):备忘录={}def改变(V.):如果V.在备忘录:返回备忘录[V.]如果V.==.0.:返回0.如果V.<0.:返回漂浮(“inf”)备忘录[V.]=闵([1+改变(V.-VI.)为了VI.在V.])返回备忘录[V.]返回改变(V.)
通过缓存动态编程vs递归
动态编程 | 递归与缓存 |
如果访问了许多子问题,因为递归调用没有开销。 | 直观的方法 |
程序的复杂性更容易看到 | 仅计算必要的那些子问题 |
BIDimensional动态编程:示例
有 括号的类型,每个类型都有自己的开口支架和关闭支架。我们假设第一对用数字1表示 第二个是2和 等等。因此,开口括号由 并且相应的关闭括号表示 分别。
一些与元素的序列 形成良好的序列,而其他人则没有。如果我们可以以下列的方式匹配或配对相同类型的打开括号,则序列是良好的括号。
- 每个支架都配对。
- 在每个匹配的对中,开口支架发生在关闭支架之前。
- 对于匹配对,任何其他匹配的对都是完全在它们之间或外部之间的。
在这个问题中,给你一个长度为括号的序列 : ,每个人 是一个括号之一。您也给出了一个值数组: .
在Values数组中的所有子序列中,如果B数组中对应的方括号子序列是用括号括起来的序列,则需要找到最大和。
任务:解决此输入的上述问题。
输入格式
一行,包含 空间分隔整数。第一个整数表示 下一个整数是 下一个 整数是 最后 整数是
约束
- , 对全部
- , 对全部
说明的例子
对于这里讨论的例子,让我们假设 .序列1,1,3没有被很好地括起来,因为两个1中的一个不能配对。序列3,1,3,1没有很好地用括号括起来,因为没有办法将第二个1与其后出现的右括号匹配。序列1,2,3,4没有很好地括起来,因为匹配的对2,4既不是完全在匹配的对1,3之间,也不是完全在匹配的对1,3之外。也就是说,匹配的对不能重叠。序列1,2,4,3,1,3用括号括起来。我们将第一个1与第一个3匹配,第二个2与第一个4匹配,第二个1与第二个3匹配,满足所有3个条件。如果您使用[,{,],}而不是分别使用1,2,3,4来重写这些序列,这将非常清楚。
认为 和价值观 和 如下面所述:然后,位置1,3中的括号形成良好的括号序列(1,4),并且这些位置中的值的总和为2(4 +(-2)= 2)。位置1,3,4,5的支架形成良好的括号序列(1,4,2,5),并且这些位置的值的总和为4.最后,位置2,4,5中的括号,6形成良好的括号序列(3,2,5,6),并且这些位置中的值的总和为13.位置1,2,5,6的值的总和为16,但是这些位置中的括号(1,3,5,6)不形成均匀括号的序列。您可以检查括号内括号序列的位置的最佳总和是13。
我们会尝试在动态程序的帮助下解决这个问题,其中状态或描述问题的参数,包括两个变量。
首先,我们设置了一系列二维数组DP [开始] [结束]
每个条目都解决了所指示的序列的问题开始
和结尾
包括的。
我们试着想想当我们遇到一个新的结尾
价值,并需要在先前解决的子问题方面解决新问题。以下是所有可能性:
- 什么时候
结束<=开始
,没有有效的子序列。 - 什么时候
b [end] <= k
,即,最后一个条目是一个开放式括号,没有有效的子序列可以以其结束。有效地,如果我们根本没有包含最后一个条目,结果是相同的。 - 什么时候
b [end]> k
,即,最后一个条目是一个结束括号,一个人必须找到最佳匹配,或者只是忽略它,以最大化总和的方式。
你可以用这些想法来解决这个问题吗?
示例:最大路径
通常,动态编程有助于解决要求我们在隐式图形设置中找到最有利可图(或最低昂贵)路径的问题。让我们尝试用一个例子来说明这一点。
您应该从数字三角形的顶部开始,并通过在向左或向右下方的数字之间选择您的数字来选择您的通道。
您的目标是最大化符合您路径中的元素的总和。
例如,在下面的三角形中,红色路径最大化总和。
看看最佳子结构和重叠的子问题请注意,每次我们从顶部到右下方或左下方移动时,我们仍然留下较小的数字三角形,就像这样:
我们可以以类似的方式打破每个子问题,直到我们到达边缘案例在底部:
在这种情况下,解决方案是a + max(b,c)
.
自下而上的动态编程解决方案是分配一个数字三角形,该三角形存储最大可达金额如果我们从该职位开始.使用该事实可以轻松地从底行计算数字三角形。
让我通过迭代来证明这一原则。
迭代1:
18 5 9 3
迭代2:
1 210 13 15 8 5 9 3
迭代3:
1 2 3.20 19 10 13 15 8 5 9 3
迭代4:
1 2 3 423 20 19 10 13 15 8 5 9 3
所以,我们可以从顶部做的有效最好的是23,这是我们的答案。