什么是动态编程?


33

抱歉,这个问题听起来很愚蠢。

据我所知,使用动态编程构建算法的工作方式如下:

  1. 将问题表达为递归关系;
  2. 通过备忘或自下而上的方法来实现递归关系。

据我所知,我已经说了有关动态编程的一切。我的意思是:动态编程不会提供工具/规则/方法/定理来表达递归关系,也不会将其转化为代码。

那么,动态编程有什么特别之处呢?除了解决某些问题的模糊方法之外,它还能给您带来什么?


11
历史事实(此注释不会给您带来帮助,但是如果您想对动态编程进行深入研究,则Bellman实际上是一个很好的线索):当Bellman提出现在称为动态编程的内容时,他称其为“动态编程”。 “因为当时纯粹的理论工作不会随他的雇主一起进行,所以他需要一些更时髦的词汇,不能用贬义的方式使用
G. Bach

3
据我所知,您提到的正是这两点。当避免由于子问题重叠而导致指数爆炸时,它变得特别。就这样。啊,顺便说一句,我的教授更喜欢“算法范式”而不是“模糊方法”。
Hendrik 2015年

“动态编程”似乎主要是个时髦的词(从那以后就失去了它的嗡嗡声)。当然这并不意味着它没有用。
user253751

3
没有一个答案,但是对我来说,动态编程绝对是“当您尝试递归解决问题时所用的东西,但最终却浪费了时间来反复地重复同样的子问题。”
hobbs 2015年

@hobbs:的确如此,但是技巧是找到浪费时间的最初方式;)
j_random_hacker

Answers:


27

动态编程为您提供了一种思考算法设计的方式。这通常非常有帮助。

记忆和自下而上的方法为您提供了将递归关系转换为代码的规则/方法。记忆化是一个相对简单的想法,但是最好的想法通常是!

动态编程为您提供了一种结构化的方式来考虑算法的运行时间。运行时间基本上由两个数字确定:必须解决的子问题数量,以及解决每个子问题所需的时间。这提供了一种考虑算法设计问题的便捷方法。当您拥有候选递归关系时,您可以查看一下它,并很快了解运行时间(例如,您通常可以非常迅速地知道会有多少子问题,这是运行时间;如果您必须解决许多指数级的子问题,那么递归可能不是一个好方法。这也有助于您排除候选子问题分解。例如,如果我们有一个字符串,由前缀限定了子问题小号[ 1 .. ]或后缀小号[ Ĵ n ]或子串 S [ i j ]可能是合理的(子问题的数目是 n中的多项式),但是用 S的子序列定义子问题不太可能是一个好方法(子问题的数目在 n中是指数的)。这使您可以修剪可能重复的“搜索空间”。S[1..n]S[1..i]S[j..n]S[i..j]nSn

动态编程为您提供了一种结构化的方法来查找候选递归关系。根据经验,这种方法通常是有效的。特别是,根据输入的类型,您可以识别出一些启发式/通用模式,这些通用模式可以定义子问题。例如:

  • 如果输入是一个正整数 ,一个候选的方式来定义一个子问题是通过更换Ñ具有较小整数Ñ '(ST 0 Ñ 'Ñ)。nnn0nn

  • 如果输入是一个字符串 ,一些候选方式来定义一个子问题包括:更换小号[ 1 .. Ñ ]用一个前缀小号[ 1 .. ] ; 替换小号[ 1 .. Ñ ]带有后缀小号[ Ĵ n ] ; 将S [ 1 .. n ]替换为子串S [ i Ĵ ]S[1..n]S[1..n]S[1..i]S[1..n]S[j..n]S[1..n]S[i..j]。(此处,子问题由的选择确定。)i,j

  • 如果输入是list,请执行与字符串相同的操作。

  • 如果输入是一个 ,一个候选的方式来定义一个子问题是更换Ť与任何子树Ť(即,选择一个节点X和替换Ť与根的子树X ;子问题是由选择决定X)。TTTxTxx

  • 如果输入是一对,则递归地查看x的类型和y的类型,以确定为每个子问题选择子问题的方法。换句话说,一个候选的方式来定义一个子问题是更换X Ý X 'ÿ ',其中X '为子问题Xÿ '为子问题ÿ。(您还可以考虑以下形式的子问题:x y(x,y)xy(x,y)(x,y)xxyyX 'ÿ 。)(x,y)(x,y)

等等。这为您提供了一个非常有用的启发式方法:只需查看方法的类型签名,您就可以列出定义子问题的候选方法的列表。换句话说,仅通过查看问题陈述(仅查看输入的类型),您就可以提出一些定义子问题的候选方法。

这通常非常有帮助。它不会告诉您递归关系是什么,但是当您对如何定义子问题有特定选择时,通常很难算出相应的递归关系。因此,它常常使动态编程算法的设计变成结构化的体验。您在废纸上写下了定义子问题的候选方法的列表(使用上面的启发式方法)。然后,对于每个候选人,您尝试写下一个递归关系,并通过计算子问题的数量和每个子问题花费的时间来评估其运行时间。在尝试每位候选人之后,您将保留能够找到的最好的候选人。为算法设计过程提供一些结构是一个主要的帮助,否则算法设计会令人生畏(


So you confirm that dynamic programming does not provide with concrete "procedures" to follow. It's just "a way to think", as you said. Note that I'm not arguing that DP is useless (on the contrary!), I'm just trying to understand if there is something that I am missing or if I should just practice more.
hey hey

@heyhey, well, yes... and no. See my revised answer for more elaboration. It's not a silver bullet, but it does provide some semi-concrete procedures that are often helpful (not guaranteed to work, but often do prove helpful).
D.W.

Many thanks! By practicing I am getting more and more familiar with some of those "semi-concrete procedures" you are describing.
hey hey

"if there are exponentially many subproblems you have to solve, then the recurrence probably won't be a good approach". For many problems there is no known polynomial time algorithm. Why should this be a criterion for using DP?
Chiel ten Brinke

@Chiel, it's not a criterion for using DP. If you have a problem where you would be happy with an exponential-time algorithms, then you can ignore that particular parenthetical remark. It's just an example to try to illustrate the general point I was making -- not something you should take too seriously or interpret as a hard-and-fast rule.
D.W.

9

Your understanding of dynamic programming is correct (afaik), and your question is justified.

I think the additional design space we get from the kind of recurrences we call "dynamic programming" can best be seen in comparison to other schemata of recursive approaches.

Let's pretend our inputs are arrays A[1..n] for the sake of highlighting the concepts.

  1. Inductive Approach

    Here the idea is to make your problem smaller, solve the smaller version and derive a solution for the original one. Schematically,

    f(A)=g(f(A[1..nc]),A)

    with g the function/algorithm that translates the solution.

    Example: Finding superstars in linear time

  2. Divide & Conquer

    Partition the input into several smaller parts, solve the problem for each and combine. Schematically (for two parts),

    f(A)=g(f(A[1..c]),f(A[c+1..n]),A).

    Examples: Merge-/Quicksort, Shortest pairwise distance in the plane

  3. Dynamic Programming

    Consider all ways of partitioning the problem into smaller problems and pick the best. Schematically (for two parts),

    f(A)=best{g(f(A[1..c]),f(A[c+1..n]))|1cn1}.

    Examples: Edit distance, Change-making problem

    Important side note: dynamic programming is not brute force! The application of best in every step reduces the search space considerably.

In a sense, you know less and less statically going from top to bottom, and have to make more and more decisions dynamically.

The lesson from learning about dynamic programming is that it is okay to try all possible partitionings (well, it's required for correctness) because it can still be efficient using memoization.


"Pruned Dynamic Programming" (when it applies) proves that trying all possibilities is NOT required for correctness.
Ben Voigt

@BenVoigt Of course. I remained deliberately vague about what "all ways to partition" means; you want to rule out as many as possible, of course! (However, even if you try all ways of partitioning you don't get brute force since you only ever investigate combinations of optimal solutions to subproblems, whereas brute-force would investigate all combinations of all solutions.)
Raphael


5

Dynamic Programming allows you to trade memory for computation time. Consider the classic example, Fibonacci.

Fibonacci is defined by the recurrence Fib(n)=Fib(n1)+Fib(n2). If you solve using this recursion, you end up doing O(2n) calls to Fib(), since the recursion tree is a binary tree with height n.

Instead, you want to calculate Fib(2), then use this to find Fib(3), use that to find Fib(4), etc. This only takes O(n) time.

DP also provides us with basic techniques for translating a recurrence relation into a bottom-up solution, but these are relatively straightforward (and generally involve using an m dimensional matrix, or a frontier of such a matrix, where m is the number of parameters in the recurrence relation). These are well explained in any text about DP.


1
You talk only about the memoization part, which misses the point of the question.
Raphael

1
"Dynamic Programming allows you to trade memory for computation time" is not something I heard when doing undergrad, and it's a great way to look at this subject. This is an intuitive answer with a succinct example.
trueshot

@trueshot: Except that sometimes dynamic programming (and particularly, "Pruned Dynamic Programming") is able to reduce both time and space requirements.
Ben Voigt

@Ben I didn't say it was a one-to-one trade. You can prune a recurrence tree as well. I posit that I did answer the question, which was, "What does DP get us?" It gets us faster algorithms by trading space for time. I agree that the accepted answer is more thorough, but this is valid as well.
Kittsil

2

Here is another slightly different way of phrasing what dynamic programming gives you. Dynamic programming collapses an exponential number of candidate solutions into a polynomial number of equivalence classes, such that the candidate solutions in each class are indistinguishable in some sense.

Let me take as an example the problem of finding the number of increasing subsequences of length k in an array A of lenght n. It is useful to partition the set of all subsequences into equivalence classes such that two subsequences belong to the same class if and only if they have the same length and end in the same index. All of the 2n possible subsequences belong to exactly one of the O(n2) equivalence classes. This partitioning preserves enough information so that we can define a recurrence relation for the sizes of the classes. If f(i,) gives the number of subsequences which end in index i and have length , then we have:

f(i,)=j<i such thatA[j]<A[i]f(j,1)
f(i,1)=1 for all i=1n

This recurrence solves the problem in time O(n2k).

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.