揭开均线系统的神秘面纱
by Alaina Kafkes
由Alaina Kafkes
揭开动态规划的神秘面纱 (Demystifying Dynamic Programming)
如何构造和编码动态编程算法 (How to construct & code dynamic programming algorithms)
Maybe you’ve heard about it in preparing for coding interviews. Maybe you’ve struggled through it in an algorithms course. Maybe you’re trying to learn how to code on your own, and were told somewhere along the way that it’s important to understand dynamic programming. Using dynamic programming (DP) to write algorithms is as essential as it is feared.
也许您在准备编写编程采访时就听说过它。 也许您在算法课程中苦苦挣扎。 也许您正在尝试学习如何自己编写代码,并且在学习过程中被告知要了解动态编程很重要。 使用动态编程(DP)编写算法至关重要。
And who can blame those who shrink away from it? Dynamic programming seems intimidating because it is ill-taught. Many tutorials focus on the outcome — explaining the algorithm, instead of the process — finding the algorithm . This encourages memorization, not understanding.
谁能责怪那些退缩的人呢? 动态编程似乎是令人生畏的,因为它没有教书。 很多教程注重结果- 解释算法,而不是过程- 寻找算法。 这鼓励记忆,而不是理解。
During my algorithms class this year, I pieced together my own process for solving problems that require dynamic programming. Parts of it come from my algorithms professor (to whom much credit is due!), and parts from my own dissection of dynamic programming algorithms.
在今年的算法课程中,我拼凑了自己的过程来解决需要动态编程的问题。 它的一部分来自我的算法教授 (应归功于他!),一部分来自我自己对动态编程算法的研究。
But before I share my process, let’s start with the basics. What is dynamic programming, anyway?
但是在分享我的过程之前,让我们从基础开始。 无论如何,动态编程是什么?
动态编程定义 (Dynamic Programming Defined)
Dynamic programming amounts to breaking down an optimization problem into simpler sub-problems, and storing the solution to each sub-problem so that each sub-problem is only solved once.
动态编程相当于将优化问题分解为更简单的子问题, 并将解决方案存储到每个子问题中,以便每个子问题仅被解决一次。
To be honest, this definition may not make total sense until you see an example of a sub-problem. That’s okay, it’s coming up in the next section.
老实说,在您看到一个子问题的示例之前,此定义可能没有什么意义。 没关系,将在下一节中介绍。
What I hope to convey is that DP is a useful technique for optimization problems, those problems that seek the maximum or minimum solution given certain constraints, because it looks through all possible sub-problems and never recomputes the solution to any sub-problem. This guarantees correctness and efficiency, which we cannot say of most techniques used to solve or approximate algorithms. This alone makes DP special.
我希望传达的是,DP是一种用于优化问题的有用技术,这些优化问题在给定特定约束的情况下寻求最大或最小解,因为它会仔细研究所有可能的子问题,并且永远不会将解决方案重新计算为任何子问题。 这保证了正确性和效率,我们不能说大多数用于求解或近似算法的技术。 仅此一点就使DP特别。
In the next two sections, I’ll explain what a sub-problem is, and then motivate why storing solutions — a technique known as memoization — matters in dynamic programming.
在接下来的两部分中,我将解释什么是子问题 ,然后说明为什么在动态编程中存储解决方案(一种称为备忘录的技术)很重要的原因。
子问题上的子问题子问题上的子问题 (Sub-problems on Sub-problems on Sub-problems)
Sub-problems are smaller versions of the original problem. In fact, sub-problems often look like a reworded version of the original problem. If formulated correctly, sub-problems build on each other in order to obtain the solution to the original problem.
子问题是原始问题的较小版本。 实际上,子问题通常看起来像原始问题的改写版本。 如果表述正确,则子问题会相互叠加,以便获得原始问题的解决方案。
To give you a better idea of how this works, let’s find the sub-problem in an example dynamic programming problem.
为了让您更好地了解它是如何工作的,让我们在示例动态编程问题中找到子问题。
Pretend you’re back in the 1950s working on an IBM-650 computer. You know what this means — punchcards! Your job is to man, or woman, the IBM-650 for a day. You’re given a natural number n punchcards to run. Each punchcard i must be run at some predetermined start time s_i and stop running at some predetermined finish time f_i. Only one punchcard can run on the IBM-650 at once. Each punchcard also has an associated value v_i based on how important it is to your company.
假设您回到了1950年代在IBM-650计算机上工作。 您知道这意味着什么-打Kong卡! 您的工作是一天要让男人或女人,IBM-650。 您会得到一个自然数n个打Kong卡来运行。 每个打Kong卡i必须在某个预定的开始时间s_i运行,并在某个预定的结束时间f_i停止运行。 一次只能在IBM-650上运行一个打Kong卡。 每个穿Kong卡还具有关联值v_i,这取决于它对贵公司的重要性。
Problem: As the person in charge of the IBM-650, you must determine the optimal schedule of punchcards that maximizes the total value of all punchcards run.
问题 :作为IBM-650的负责人,您必须确定最佳的打Kong卡时间表,以使所有打Kong卡的总价值最大化。
Because I’ll go through this example in great detail throughout this article, I’ll only tease you with its sub-problem for now:
因为在整篇文章中我将详细介绍该示例,所以我现在仅向您介绍其子问题:
Sub-problem: The maximum value schedule for punchcards i through n such that the punchcards are sorted by start time.
子问题 :打Kong卡i到n的最大值计划,以使打Kong卡按开始时间排序。
Notice how the sub-problem breaks down the original problem into components that build up the solution. With the sub-problem, you can find the maximum value schedule for punchcards n-1 through n, and then for punchcards n-2 through n, and so on. By finding the solutions for every single sub-problem, you can then tackle the original problem itself: the maximum value schedule for punchcards 1 through n. Since the sub-problem looks like the original problem, sub-problems can be used to solve the original problem.
请注意,子问题如何将原始问题分解为构成解决方案的组件。 对于子问题,您可以找到打Kong卡n-1至n的最大值计划,然后是打Kong卡n-2至n的最大值计划,依此类推。 通过找到每个子问题的解决方案,您便可以解决原始问题本身:打Kong卡1到n的最大值计划。 由于子问题看起来像原始问题,因此可以使用子问题来解决原始问题。
In dynamic programming, after you solve each sub-problem, you must memoize, or store it. Let’s find out why in the following section.
在动态编程中,解决每个子问题后,必须记忆或存储它。 让我们在下一节中找出原因。
用斐波那契数来激励记忆 (Motivating Memoization with Fibonacci Numbers)
When told to implement an algorithm that calculates the Fibonacci value for any given number, what would you do? Most people I know would opt for a recursive algorithm that looks something like this in Python:
当被告知实施针对任何给定数字计算斐波那契值的算法时,您会怎么做? 我认识的大多数人都会选择一个递归算法 ,在Python中看起来像这样:
def fibonacciVal(n): if n == 0: return 0 elif n == 1: return 1 else: return fibonacciVal(n-1) + fibonacciVal(n-2)
This algorithm accomplishes its purpose, but at a huge cost. For example, let’s look at what this algorithm must calculate in order to solve for n = 5 (abbreviated as F(5)):
该算法实现了其目的,但是代价巨大 。 例如,让我们看一下该算法必须求解什么才能求解n = 5(缩写为F(5)):
F(5) / \ / \ / \ F(4) F(3) / \ / \ F(3) F(2) F(2) F(1) / \ / \ / \ F(2) F(1) F(1) F(0) F(1) F(0) / \ F(1) F(0)
The tree above represents each computation that must be made in order to find the Fibonacci value for n = 5. Notice how the sub-problem for n = 2 is solved thrice. For a relatively small example (n = 5), that’s a lot of repeated , and wasted, computation!
上面的树表示为找到n = 5的斐波那契值而必须进行的每个计算。请注意,n = 2的子问题是如何三次求解的。 对于一个相对较小的示例(n = 5),这是很多重复的,浪费了计算!
What if, instead of calculating the Fibonacci value for n = 2 three times, we created an algorithm that calculates it once, stores its value, and accesses the stored Fibonacci value for every subsequent occurrence of n = 2? That’s exactly what memoization does.
如果不是创建n次计算n = 2的斐波那契值而不是三次的算法,而是创建了一次计算一次,存储其值并在以后每次出现n = 2时访问存储的斐波那契值的算法,该怎么办? 这正是回忆的作用。
With this in mind, I’ve written a dynamic programming solution to the Fibonacci value problem:
考虑到这一点,我为斐波那契值问题编写了动态编程解决方案:
def fibonacciVal(n): memo = [0] * (n+1) memo[0], memo[1] = 0, 1 for i in range(2, n+1): memo[i] = memo[i-1] + memo[i-2] return memo[n]
Notice how the solution of the return value comes from the memoization array memo[ ], which is iteratively filled in by the for loop. By “iteratively,” I mean that memo[2] is calculated and stored before memo[3], memo[4], …, and memo[n]. Because memo[ ] is filled in this order, the solution for each sub-problem (n = 3) can be solved by the solutions to its preceding sub-problems (n = 2 and n = 1) because these values were already stored in memo[ ] at an earlier time.
请注意,返回值的解决方案如何来自记忆数组memo [],该数组由for循环迭代填充。 “迭代”是指在备忘[3],备忘[4],…和备忘[ n ]之前计算并存储备忘[2]。 因为memo []是按此顺序填充的,所以每个子问题(n = 3)的解决方案都可以通过其先前子问题(n = 2和n = 1)的解决方案来解决,因为这些值已经存储在memo []在较早的时间。
Memoization means no re-computation, which makes for a more efficient algorithm. Thus, memoization ensures that dynamic programming is efficient, but it is choosing the right sub-problem that guarantees that a dynamic program goes through all possibilities in order to find the best one.
备注意味着无需重新计算,这使得算法更加有效。 因此,记忆可确保动态编程是有效的,但它选择的是正确的子问题,该子问题可确保动态程序遍历所有可能性以找到最佳方案。
Now that we’ve addressed memoization and sub-problems, it’s time to learn the dynamic programming process. Buckle in.
现在我们已经解决了记忆和子问题,现在该学习动态编程过程了。 扣上。
我的动态编程过程 (My Dynamic Programming Process)
步骤1:以字词识别子问题。 (Step 1: Identify the sub-problem in words.)
Too often, programmers will turn to writing code before thinking critically about the problem at hand. Not good. One strategy for firing up your brain before you touch the keyboard is using words, English or otherwise, to describe the sub-problem that you have identified within the original problem.
通常,程序员在认真思考眼前的问题之前会转向编写代码。 不好。 在触摸键盘之前激发您的大脑的一种策略是使用单词(英语)或其他方式来描述您在原始问题中发现的子问题。
If you’re solving a problem that requires dynamic programming, grab a piece of paper and think about the information that you need to solve this problem. Write out the sub-problem with this in mind.
如果要解决需要动态编程的问题,请抓一张纸,考虑解决该问题所需的信息。 请牢记这一点写出子问题。
For example, in the punchcard problem, I stated that the sub-problem can be written as “the maximum value schedule for punchcards i through n such that the punchcards are sorted by start time.” I found this sub-problem by realizing that, in order to determine the maximum value schedule for punchcards 1 through n such that the punchcards are sorted by start time, I would need to find the answer to the following sub-problems:
例如,在打Kong卡问题中,我说过子问题可以写成“打Kong卡i到n的最大值计划,以便打Kong卡按开始时间排序。” 我发现此子问题的原因是,为了确定打Kong卡1到n的最大值计划,以使打Kong卡按开始时间排序,我需要找到以下子问题的答案:
The maximum value schedule for punchcards n-1 through n such that the punchcards are sorted by start time
打Kong卡n-1至n的最大值计划,以使打Kong卡按开始时间排序
The maximum value schedule for punchcards n-2 through n such that the punchcards are sorted by start time
打Kong卡n-2至n的最大值计划,以使打Kong卡按开始时间排序
The maximum value schedule for punchcards n-3 through n such that the punchcards are sorted by start time
打Kong卡n-3至n的最大值计划,以使打Kong卡按开始时间排序
- (Et cetera) (等等)
The maximum value schedule for punchcards 2 through n such that the punchcards are sorted by start time
打Kong卡2到n的最大值计划,以使打Kong卡按开始时间排序
If you can identify a sub-problem that builds upon previous sub-problems to solve the problem at hand, then you’re on the right track.
如果您可以确定一个基于先前子问题来解决当前问题的子问题,那么您的方向正确。
步骤2:将子问题写出作为重复的数学决策。 (Step 2: Write out the sub-problem as a recurring mathematical decision.)
Once you’ve identified a sub-problem in words, it’s time to write it out mathematically. Why? Well, the mathematical recurrence, or repeated decision, that you find will eventually be what you put into your code. Besides, writing out the sub-problem mathematically vets your sub-problem in words from Step 1. If it is difficult to encode your sub-problem from Step 1 in math, then it may be the wrong sub-problem!
一旦确定了单词中的子问题,就可以用数学方法将其写出。 为什么? 好吧,您发现的数学递归或重复决策最终将是您放入代码中的内容。 此外,用数学方法写出子问题可以用步骤1的单词来审查子问题。如果很难用数学方法编码第1步中的子问题,那么这可能是错误的子问题!
There are two questions that I ask myself every time I try to find a recurrence:
每次尝试重现时,我都会问自己两个问题:
- What decision do I make at every step? 我在每一步都做出什么决定?
If my algorithm is at step i, what information would it need to decide what to do in step i+1? (And sometimes: If my algorithm is at step i, what information did it need to decide what to do in step i-1?)
如果我的算法在步骤i中 ,则它需要什么信息来决定在步骤i + 1中该做什么? (有时:如果我的算法在步骤i中 ,则它需要什么信息来决定在步骤i-1中做什么?)
Let’s return to the punchcard problem and ask these questions.
让我们回到打Kong卡问题并提出这些问题。
What decision do I make at every step? Assume that the punchcards are sorted by start time, as mentioned previously. For each punchcard that is compatible with the schedule so far (its start time is after the finish time of the punchcard that is currently running), the algorithm must choose between two options: to run, or not to run the punchcard.
我在每一步都做出什么决定? 如前所述,假设打Kong卡按开始时间排序。 对于到目前为止与时间表兼容的每个打Kong卡(其开始时间在当前运行的打Kong卡的完成时间之后),算法必须在两个选项之间进行选择:运行或不运行。
If my algorithm is at step i, what information would it need to decide what to do in step i+1? To decide between the two options, the algorithm needs to know the next compatible punchcard in the order. The next compatible punchcard for a given punchcard p is the punchcard q such that s_q (the predetermined start time for punchcard q) happens after f_p (the predetermined finish time for punchcard p) and the difference between s_q and f_p is minimized. Abandoning mathematician-speak, the next compatible punchcard is the one with the earliest start time after the current punchcard finishes running.
如果我的算法在步骤 i中 ,则它需要什么信息来决定在步骤 i + 1中 该做什么 ? 为了在两个选项之间做出决定,算法需要知道顺序中的下一个兼容打Kong卡。 对于给定的穿Kong卡片P中的下一个兼容穿Kong卡片是穿Kong卡片Q如下s_q(预定开始时间穿Kong卡片Q)f_p(预定结束时间为穿Kong卡片p)和s_q和f_p之间的差之后发生最小化。 抛弃数学家的口号,下一个兼容的打Kong卡是在当前打Kong卡完成运行后开始时间最早的打Kong卡。
If my algorithm is at step i, what information did it need to decide what to do in step i-1? The algorithm needs to know about future decisions: the ones made for punchcards i through n in order to decide to run or not to run punchcard i-1.
如果我的算法在步骤 i中 ,则它需要什么信息来决定在步骤 i-1中 该做什么 ? 该算法需要知道将来的决策:为打Kong卡i到n做出的决定,以便决定是否运行打Kong卡i-1 。
Now that we’ve answered these questions, perhaps you’ve started to form a recurring mathematical decision in your mind. If not, that’s also okay, it becomes easier to write recurrences as you get exposed to more dynamic programming problems.
既然我们已经回答了这些问题,也许您已经开始在脑海中形成一个反复出现的数学决定。 如果没有,那也没关系,当您遇到更多动态编程问题时,编写重复代码将变得更加容易。
Without further ado, here’s our recurrence:
事不宜迟,以下是我们的复述:
OPT(i) = max(v_i + OPT(next[i]), OPT(i+1))
This mathematical recurrence requires some explaining, especially for those who haven’t written one before. I use OPT(i) to represent the maximum value schedule for punchcards i through n such that the punchcards are sorted by start time. Sounds familiar, right? OPT(•) is our sub-problem from Step 1.
这种数学递归需要一些解释,特别是对于那些以前没有写过的人。 我使用OPT( i )表示打Kong卡i到n的最大值计划,以便打Kong卡按开始时间排序。 听起来很熟悉,对吧? OPT(•)是步骤1中的子问题。
In order to determine the value of OPT(i), we consider two options, and we want to take the maximum of these options in order to meet our goal: the maximum value schedule for all punchcards. Once we choose the option that gives the maximum result at step i, we memoize its value as OPT(i).
为了确定OPT( i )的值,我们考虑两个选项,并且我们希望采用这些选项中的最大值以实现我们的目标:所有打Kong卡的最大值计划。 一旦选择了在步骤i中获得最大结果的选项,我们便将其值记为OPT( i )。
The two options — to run or not to run punchcard i — are represented mathematically as follows:
数学上表示两个选项(运行或不运行打Kong卡i) :
v_i + OPT(next[i])
This clause represents the decision to run punchcard i. It adds the value gained from running punchcard i to OPT(next[i]), where next[i] represents the next compatible punchcard following punchcard i. OPT(next[i]) gives the maximum value schedule for punchcards next[i] through n such that the punchcards are sorted by start time. Adding these two values together produces maximum value schedule for punchcards i through n such that the punchcards are sorted by start time if punchcard i is run.
此子句表示运行穿Kong卡i的决定。 它将从运行打Kong卡i获得的值添加到OPT(next [ i ]),其中next [ i ]表示打Kong卡i之后的下一个兼容打Kong卡。 OPT(next [ i ])给出了打Kong卡next [ i ]到n的最大值调度,以便按开始时间对打Kong卡进行排序。 将这两个值相加会生成打Kong卡i到n的最大值计划,以便在运行打Kong卡i时按开始时间对打Kong卡进行排序。
OPT(i+1)
Conversely, this clause represents the decision to not run punchcard i. If punchcard i is not run, its value is not gained. OPT(i+1) gives the maximum value schedule for punchcards i+1 through n such that the punchcards are sorted by start time. So, OPT(i+1) gives the maximum value schedule for punchcards i through n such that the punchcards are sorted by start time if punchcard i is not run.
相反,此子句表示不运行穿Kong卡i的决定。 如果打Kong卡i没有运行,则不会获得其值。 OPT( i + 1 )给出了打Kong卡i + 1到n的最大值计划,以使打Kong卡按开始时间排序。 因此,OPT( i + 1 )给出了打Kong卡i到n的最大值调度,使得如果未运行打Kong卡i,则打Kong卡将按开始时间排序。
In this way, the decision made at each step of the punchcard problems is encoded mathematically to reflect the sub-problem in Step 1.
这样,对打Kong卡问题的每个步骤所做出的决定进行数学编码,以反映步骤1中的子问题。
步骤3:使用步骤1和2解决原始问题。 (Step 3: Solve the original problem using Steps 1 and 2.)
In Step 1, we wrote down the sub-problem for the punchcard problem in words. In Step 2, we wrote down a recurring mathematical decision that corresponds to these sub-problems. How can we solve the original problem with this information?
在第1步中,我们用单词写下了穿Kong卡问题的子问题。 在步骤2中,我们写下了与这些子问题相对应的重复数学决策。 我们如何利用这些信息解决原始问题?
OPT(1)
It’s that simple. Since the sub-problem we found in Step 1 is the maximum value schedule for punchcards i through n such that the punchcards are sorted by start time, we can write out the solution to the original problem as the maximum value schedule for punchcards 1 through n such that the punchcards are sorted by start time. Since Steps 1 and 2 go hand in hand, the original problem can also be written as OPT(1).
就这么简单。 由于我们在步骤1中发现的子问题是打Kong卡i到n的最大值调度,以便按开始时间对打Kong卡进行排序,因此我们可以写出原始问题的解决方案,作为打Kong卡1到n的最大值调度这样,打Kong卡将按开始时间排序。 由于步骤1和步骤2并存,因此原始问题也可以写为OPT(1)。
步骤4:确定备忘录数组的尺寸和填充方向。 (Step 4: Determine the dimensions of the memoization array and the direction in which it should be filled.)
Did you find Step 3 deceptively simple? It sure seems that way. You may be thinking, how can OPT(1) be the solution to our dynamic program if it relies on OPT(2), OPT(next[1]), and so on?
您发现第3步看似简单吗? 看起来确实是这样。 您可能在想,如果OPT(1)依赖于OPT(2),OPT(next [1])等,该如何成为动态程序的解决方案?
You’re correct to notice that OPT(1) relies on the solution to OPT(2). This follows directly from Step 2:
您正确地注意到OPT(1)依赖于OPT(2)的解决方案。 这直接来自步骤2:
OPT(1) = max(v_1 + OPT(next[1]), OPT(2))
But this is not a crushing issue. Think back to Fibonacci memoization example. To find the Fibonacci value for n = 5, the algorithm relies on the fact that the Fibonacci values for n = 4, n = 3, n = 2, n = 1, and n = 0 were already memoized. If we fill in our memoization table in the correct order, the reliance of OPT(1) on other sub-problems is no big deal.
但这不是一个令人毛骨悚然的问题。 回想一下斐波那契记忆示例。 为了找到n = 5的斐波那契值,该算法依赖于以下事实: n = 4, n = 3, n = 2, n = 1和n = 0的斐波那契值。 如果我们以正确的顺序填写我们的记忆表,那么OPT(1)对其他子问题的依赖就没什么大不了的。
How can we identify the correct direction to fill the memoization table? In the punchcard problem, since we know OPT(1) relies on the solutions to OPT(2) and OPT(next[1]), and that punchcards 2 and next[1] have start times after punchcard 1 due to sorting, we can infer that we need to fill our memoization table from OPT(n) to OPT(1).
我们如何确定正确的方向来填写备注表? 在打Kong卡问题中,由于我们知道OPT(1)依赖于OPT(2)和OPT(next [1])的解,并且由于排序,打Kong卡2和next [1]在打Kong卡1之后具有开始时间,因此可以推断出我们需要从OPT( n )到OPT(1)填充我们的记忆表。
How do we determine the dimensions of this memoization array? Here’s a trick: the dimensions of the array are equal to the number and size of the variables on which OPT(•) relies. In the punchcard problem, we have OPT(i), which means that OPT(•) only relies on variable i, which represents the punchcard number. This suggest that our memoization array will be one-dimensional and that its size will be n since there are n total punchcards.
我们如何确定此记忆阵列的尺寸? 这是个技巧:数组的维数等于OPT(•)所依赖的变量的数量和大小。 在打Kong卡问题中,我们具有OPT( i ),这意味着OPT(•)仅依赖于变量i ,该变量代表打Kong卡号。 这表明我们的记忆数组将是一维的,并且由于总共有n个打Kong卡,因此其大小将为n 。
If we know that n = 5, then our memoization array might look like this:
如果我们知道n = 5,那么我们的记忆数组可能看起来像这样:
memo = [OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]
However, because many programming languages start indexing arrays at 0, it may be more convenient to create this memoization array so that its indices align with punchcard numbers:
但是,由于许多编程语言都从0开始对数组进行索引 ,因此创建此备忘录数组以使其索引与穿Kong卡编号对齐可能会更方便:
memo = [0, OPT(1), OPT(2), OPT(3), OPT(4), OPT(5)]
步骤5:编码! (Step 5: Code it!)
To code our dynamic program, we put together Steps 2–4. The only new piece of information that you’ll need to write a dynamic program is a base case, which you can find as you tinker with your algorithm.
要编写动态程序的代码,我们将步骤2-4组合在一起。 编写动态程序所需的唯一一条新信息就是基本案例,您可以在修改算法时找到它。
A dynamic program for the punchcard problem will look something like this:
打Kong卡问题的动态程序将如下所示:
def punchcardSchedule(n, values, next): # Initialize memoization array - Step 4 memo = [0] * (n+1) # Set base case memo[n] = values[n] # Build memoization table from n to 1 - Step 2 for i in range(n-1, 0, -1): memo[i] = max(v_i + memo[next[i]], memo[i+1]) # Return solution to original problem OPT(1) - Step 3 return memo[1]
Congrats on writing your first dynamic program! Now that you’ve wet your feet, I’ll walk you through a different type of dynamic program.
恭喜您编写了第一个动态程序! 现在您已经弄湿了,我将引导您完成另一种类型的动态程序。
选择的悖论:多个选项DP示例 (Paradox of Choice: Multiple Options DP Example)
Although the previous dynamic programming example had a two-option decision — to run or not to run a punchcard — some problems require that multiple options be considered before a decision can be made at each step.
尽管前面的动态编程示例有两个选项的决定(运行还是不运行打Kong卡),但某些问题需要在考虑每个选项之前才能考虑多个选项。
Time for a new example.
是时候提出一个新的例子了。
Pretend you’re selling the friendship bracelets to n customers, and the value of that product increases monotonically. This means that the product has prices {p_1, …, p_n} such that p_i ≤ p_j if customer j comes after customer i. These n customers have values {v_1, …, v_n}. A given customer i will buy a friendship bracelet at price p_i if and only if p_i ≤ v_i; otherwise the revenue obtained from that customer is 0. Assume prices are natural numbers.
假设您将友谊手镯卖给n个客户,并且该产品的价值单调增加。 这意味着,该产品具有价格{P_1,...,P_N}这样P_I如果客户j来自客户i≤后p_j。 这n个客户的值分别为{ v_1 ,…, v_n }。 一个给定的客户我会买在价格P_I友谊手镯当且仅当P_I≤V-I; 否则,从该客户获得的收入为0。假设价格为自然数。
Problem: You must find the set of prices that ensure you the maximum possible revenue from selling your friendship bracelets.
问题 :您必须找到一套价格,以确保从出售友谊手链中获得最大的收益。
Take a second to think about how you might address this problem before looking at my solutions to Steps 1 and 2.
在查看我对步骤1和步骤2的解决方案之前,请花点时间考虑一下如何解决此问题。
步骤1:以字词识别子问题。 (Step 1: Identify the sub-problem in words.)
Sub-problem: The maximum revenue obtained from customers i through n such that the price for customer i-1 was set at q.
子问题 :从客户i到n获得的最大收入,从而将客户i-1的价格设置为q 。
I found this sub-problem by realizing that to determine the maximum revenue for customers 1 through n, I would need to find the answer to the following sub-problems:
通过认识到确定客户1到n的最大收入,我找到了这个子问题,我需要找到以下子问题的答案:
The maximum revenue obtained from customers n-1 through n such that the price for customer n-2 was set at q.
从客户n-1到n获得的最大收入,从而将客户n-2的价格设置为q 。
The maximum revenue obtained from customers n-2 through n such that the price for customer n-3 was set at q.
从客户n-2到n获得的最大收入,从而将客户n-3的价格设置为q 。
- (Et cetera) (等等)
Notice that I introduced a second variable q into the sub-problem. I did this because, in order to solve each sub-problem, I need to know the price I set for the customer before that sub-problem. Variable q ensures the monotonic nature of the set of prices, and variable i keeps track of the current customer.
注意,我在子问题中引入了第二个变量q 。 我这样做是因为,为了解决每个子问题,我需要知道在该子问题之前为客户设置的价格。 变量q确保价格集的单调性,变量i跟踪当前客户。
步骤2:将子问题写出作为重复的数学决策。 (Step 2: Write out the sub-problem as a recurring mathematical decision.)
There are two questions that I ask myself every time I try to find a recurrence:
每次尝试重现时,我都会问自己两个问题:
- What decision do I make at every step? 我在每一步都做出什么决定?
If my algorithm is at step i, what information would it need to decide what to do in step i+1? (And sometimes: If my algorithm is at step i, what information would it need to decide what to do in step i-1?)
如果我的算法在步骤i中 ,则它需要什么信息来决定在步骤i + 1中该做什么? (有时:如果我的算法在步骤i中 ,则它需要什么信息来决定在步骤i-1中做什么?)
Let’s return to the friendship bracelet problem and ask these questions.
让我们回到友谊手镯问题,并提出以下问题。
What decision do I make at every step? I decide at which price to sell my friendship bracelet to the current customer. Since prices must be natural numbers, I know that I should set my price for customer i in the range from q — the price set for customer i-1 — to v_i — the maximum price at which customer i will buy a friendship bracelet.
我在每一步都做出什么决定? 我决定以什么价格将我的友谊手镯卖给当前客户。 由于价格必须是自然数,因此我知道我应该将客户i的价格设置在q(为客户i-1设置的价格)到v_i (客户i将购买友谊手镯的最高价格)的范围内。
If my algorithm is at step i, what information would it need to decide what to do in step i+1? My algorithm needs to know the price set for customer i and the value of customer i+1 in order to decide at what natural number to set the price for customer i+1.
如果我的算法在步骤 i中 ,则它需要什么信息来决定在步骤 i + 1中 该做什么 ? 我的算法需要知道为客户i设置的价格和客户i + 1的值,以便决定以哪个自然数为客户i + 1设置价格。
With this knowledge, I can mathematically write out the recurrence:
有了这些知识,我可以用数学方式写出复发:
OPT(i,q) = max~([Revenue(v_i, a) + OPT(i+1, a)])
such that max~ finds the maximum over all a in the range q ≤ a ≤ v_i
Once again, this mathematical recurrence requires some explaining. Since the price for customer i-1 is q, for customer i, the price a either stays at integer q or it changes to be some integer between q+1 and v_i. To find the total revenue, we add the revenue from customer i to the maximum revenue obtained from customers i+1 through n such that the price for customer i was set at a.
再一次,这种数学递归需要一些解释。 由于客户i-1的价格为q ,对于客户i而言 ,价格a保持为整数q或变为q + 1和v_i之间的某个整数。 要查找的总收入,我们从客户i添加收入从客户获得最大的收益我+ 1到n,从而为客户的价格我是设定在 。
In other words, to maximize the total revenue, the algorithm must find the optimal price for customer i by checking all possible prices between q and v_i. If v_i ≤ q, then the price a must remain at q.
换句话说,为了使总收入最大化,该算法必须通过检查q和v_i之间的所有可能价格为客户i找到最佳价格。 如果V-I≤Q,则价格必须保持在Q值 。
那其他步骤呢? (What about the other steps?)
Working through Steps 1 and 2 is the most difficult part of dynamic programming. As an exercise, I suggest you work through Steps 3, 4, and 5 on your own to check your understanding.
完成第1步和第2步是动态编程中最困难的部分。 作为练习,我建议您自己完成第3、4和5步,以检查您的理解。
动态程序的运行时分析 (Runtime Analysis of Dynamic Programs)
Now for the fun part of writing algorithms: runtime analysis. I’ll be using big-O notation throughout this discussion . If you’re not yet familiar with big-O, I suggest you read up on it here.
现在开始编写算法的有趣部分:运行时分析。 在整个讨论中,我将使用big-O表示法。 如果您还不熟悉big-O,建议您在这里继续阅读。
Generally, a dynamic program’s runtime is composed of the following features:
通常,动态程序的运行时由以下功能组成:
- Pre-processing 前处理
- How many times the for loop runs for循环运行多少次
- How much time it takes the recurrence to run in one for loop iteration 循环一次循环执行需要多少时间
- Post-processing 后期处理
Overall, runtime takes the following form:
总体而言,运行时采用以下形式:
Pre-processing + Loop * Recurrence + Post-processing
Let’s perform a runtime analysis of the punchcard problem to get familiar with big-O for dynamic programs. Here is the punchcard problem dynamic program:
让我们对穿Kong卡问题进行运行时分析,以熟悉动态程序的big-O。 这是打Kong卡问题动态程序:
def punchcardSchedule(n, values, next): # Initialize memoization array - Step 4 memo = [0] * (n+1) # Set base case memo[n] = values[n] # Build memoization table from n to 1 - Step 2 for i in range(n-1, 0, -1): memo[i] = max(v_i + memo[next[i]], memo[i+1]) # Return solution to original problem OPT(1) - Step 3 return memo[1]
Let’s break down its runtime:
让我们分解一下它的运行时:
Pre-processing: Here, this means building the the memoization array. O(n).
预处理:在这里,这意味着构建记忆数组。 O( n )。
How many times the for loop runs: O(n).
for循环运行多少次:O( n )。
- How much time it takes the recurrence to run in one for loop iteration: The recurrence takes constant time to run because it makes a decision between two options in each iteration. O(1). 一次循环执行一次循环需要多少时间:循环需要一定的时间才能运行,因为循环在每次迭代的两个选项之间做出决定。 O(1)。
- Post-processing: None here! O(1). 后处理:这里没有! O(1)。
The overall runtime of the punchcard problem dynamic program is O(n) O(n) * O(1) + O(1), or, in simplified form, O(n).
打Kong卡问题动态程序的总体运行时间为O( n )O( n )* O(1)+ O(1),或者简化形式为O( n )。
你做到了! (You Did It!)
Well, that’s it — you’re one step closer to becoming a dynamic programming wizard!
就是这样-距离成为动态编程向导仅一步之遥!
One final piece of wisdom: keep practicing dynamic programming. No matter how frustrating these algorithms may seem, repeatedly writing dynamic programs will make the sub-problems and recurrences come to you more naturally. Here’s a crowdsourced list of classic dynamic programming problems for you to try.
最后一个智慧: 继续练习动态编程 。 不管这些算法看起来多么令人沮丧,重复编写动态程序都会使子问题和重复出现更自然。 这是众包的经典动态编程问题列表,供您尝试。
So get out there and take your interviews, classes, and life (of course) with your newfound dynamic programming knowledge!
因此,走出去,带着您新获得的动态编程知识(当然)参加您的访谈,课程和生活 !
Many thanks to Steven Bennett, Claire Durand, and Prithaj Nath for proofreading this post. Thank you to Professor Hartline for getting me so excited about dynamic programming that I wrote about it at length.
非常感谢Steven Bennett , Claire Durand和Prithaj Nath校对了这篇文章。 感谢Hartline教授让我对动态编程感到非常兴奋,以至于我写了很多关于动态编程的文章。
Enjoy what you read? Spread the love by liking and sharing this piece. Have thoughts or questions? Reach out to me on Twitter or in the comments below.
享受阅读的内容吗? 通过分享和分享这段爱情来传播爱。 有想法或问题吗? 在Twitter或以下评论中与我联系。
翻译自: https://www.freecodecamp.org/news/demystifying-dynamic-programming-3efafb8d4296/
揭开均线系统的神秘面纱