动态规划是一种有用的算法类型,可通过将难题分解为更小的子问题来进行优化。通过存储和重复使用部分解决方案,它成功避免了使用贪心算法的缺点。有两种类型的动态规划:自底向上和自顶向下。
为了能够使用动态规划解决问题,该问题必须具备所谓的最优子结构属性。这意味着,如果将问题分解为一系列子问题,并找到每个子问题的最优解,则通过这些子问题的解决方案可以实现结果解。没有这种结构的问题无法使用动态规划解决。
自顶向下更为人所知的是记忆化。这是一种存储过去计算结果的想法,以避免每次重新计算。
给定一个递归函数,例如:
fib(n) = 0 if n = 0
1 if n = 1
fib(n - 1) + fib(n - 2) if n >= 2
我们可以从它的数学形式中轻松地以递归方式编写:
function fib(n)
if(n == 0 || n == 1)
n
else
fib(n-1) + fib(n-2)
现在,任何有一定编程经验或对算法效率了解一点的人都会告诉你这是个糟糕的主意。原因在于,在每一步中,你必须重新计算 fib(i) 的值,其中 i 是 2..n-2。
一个更高效的例子是存储这些值,创建自顶向下的动态规划算法。
m = map(int, int)
m[0] = 0
m[1] = 1
function fib(n)
if(m[n] does not exist)
m[n] = fib(n-1) + fib(n-2)
通过这种方式,我们最多只计算fib(i)一次。
自底向上使用了与自顶向下相同的记忆化技术。然而,不同之处在于自底向上使用了比较子问题(称为递推)来优化最终结果。
在大多数自底向上的动态规划问题中,你通常要么试图最小化,要么试图最大化一个决策。在任何给定点上,你会得到两个(或更多)选项,你必须决定哪个对于你正在解决的问题更加优化。然而,这些决策是基于你先前做出的选择。
通过在每个子问题中做出最优决策,你可以确保整体结果最优。
这些问题最困难的部分是找到递推关系来解决你的问题。
为了支付一堆算法教材的费用,你计划抢劫一个有n个物品的商店。问题是你的微型背包最多只能装W千克的东西。知道每个物品的重量(w[i])和价值(v[i]),你想最大化所有合在一起重量不超过W的被盗物品的价值。对于每个物品,你必须做出二进制选择——取或不取。
现在,你需要找到子问题是什么。作为一个非常聪明的小偷,你意识到给定物品i、最大重量w的情况下,一个给定物品的最大价值可以用m[i,w]来表示。此外,m[0,w](最多装w千克的0件物品)和m[i,0](i个物品重量最大为0)的价值将始终等于0。
因此,
m[i, w] = 0 if i = 0 or w = 0
穿上你的思考全脸面罩,你会发现如果你已经装满了尽可能多的重量,除非新物品的重量小于或等于您的最大重量与袋子当前重量之间的差,否则无法考虑新物品。另一种情况是如果它比袋子里的某个物品重量小于或等于,但价值更高,你可能会考虑这个物品。 m[i, w] = 0 if i = 0 or w = 0
m[i - 1, w] if w[i] > w
max(m[i - 1, w], m[i - 1, w - w[i]] + v[i]) if w[i] <= w
这些是上面描述的递归关系式。一旦你有了这些关系式,编写算法就非常容易(而且短小精悍!)。
v = values from item1..itemn
w = weights from item1..itemn
n = number of items
W = maximum weight of knapsack
m[n, n] = array(int, int)
function knapsack
for w=0..W
m[0, w] = 0
for i=1 to n
m[i, 0] = 0
for w=1..W
if w[i] <= w
if v[i] + m[i-1, w - w[i]] > m[i-1, w]
m[i, w] = v[i] + m[i-1, w - w[i]]
else
m[i, w] = m[i-1, w]
else
m[i, w] = c[i-1, w]
return m[n, n]
幸运的是,在竞赛编程中,动态规划已经变得非常流行。请访问UVAJudge上的动态规划,尝试解决一些实践问题,这将测试您实现和找到动态规划问题递归关系的能力。
从维基百科的动态规划文章开始
如果你想测试自己,我的在线评测选择如下:
当然,你也可以查看算法家的动态规划类别
你还可以查看好的大学算法课程。
毕竟,如果您无法解决问题,请向 Stack Overflow 提问,因为这里有许多算法爱好者。
学习完动态规划后,您可以通过解决UVA问题来提高技能。在UVA的讨论板块中有一些UVA动态规划问题的列表。
此外,维基百科上有一些很好的简单示例。
编辑: 有关此书算法的信息,请参见以下内容:
此外,您还应该了解动态规划中的记忆化。
由于没有人提到,应用动态规划算法需要满足以下条件:
作为DP算法的典型例子,考虑使用Floyd-Warshall算法查找图中所有顶点对之间的最短路径长度。
假设有n
个编号为1到n
的顶点。虽然我们想计算一个函数d(a, b)
,即顶点a
和b
之间最短路径的长度,但很难从函数d()
的其他值中高效计算出这个值。
让我们引入第三个参数c
,并定义d(a, b, c)
为仅访问范围在1到c
之间的顶点的a
和b
之间最短路径的长度。(不需要访问所有这些顶点。)虽然这似乎是一个无意义的约束,但请注意我们现在有以下关系:
d(a, b, c) = min(d(a, b, c-1), d(a, c, c-1) + d(c, b, c-1))
上面min()
的2个参数显示了2种可能情况。只使用顶点1到c
的最短路径从a
到b
的最短方式要么是:
c
(在这种情况下,它与仅使用前c-1
个顶点的最短路径相同),或者c
。 在这种情况下,此路径必须是从a
到c
的最短路径,后跟从c
到b
的最短路径,两个路径都受限于只访问范围内的顶点1到c-1
之间。 我们知道,如果这种情况(通过c
)更短,则这两条路径不能访问任何相同的顶点,因为如果它们这样做,跳过它们之间的所有顶点(包括c
)仍然会更短,因此将选择第1种情况。这种表述满足最优子结构属性——只需要知道子问题的最优解即可找到更大问题的最优解。(并非所有问题都具有这个重要属性——例如,如果我们想要找到所有顶点对之间的最长路径,则此方法会失效,因为从a
到c
的最长路径可能会访问也被从c
到b
的最长路径访问的顶点。)
知道上述函数关系和边界条件d(a, b, 0)
等于a
和b
之间的边的长度(如果不存在这样的边,则为无限大),就可以计算每个值d(a, b, c)
,从c=1
开始,一直到c=n
。 d(a, b, n)
是可以访问中间任何顶点的a
和b
之间的最短距离——我们要寻找的答案。
如果你想学习算法,我发现MIT有一些非常优秀的讲座视频可供观看。
例如,6.046J / 18.410J 算法导论 (SMA 5503) 看起来是一个不错的选择。
该课程涵盖了动态规划等许多有用的算法技术。个人认为所使用的书籍也非常出色,对于任何认真学习算法的人来说都值得购买。
此外,该课程还附带了一系列作业等,因此您将有机会将理论付诸实践。