Dynamic programming 是一种有用的算法类型,可用于通过将困难问题分解为更小的子问题来优化它们。通过存储和重用部分解决方案,它设法避免了使用贪心算法的陷阱。动态规划有两种,自下而上和自上而下。
为了使用动态规划来解决问题,该问题必须具有称为optimal substructure 的属性。这意味着,如果将问题分解为一系列子问题,并找到每个子问题的最优解,那么最终的解决方案将通过这些子问题的解来实现。没有这种结构的问题不能用动态规划来解决。
自上而下
自上而下更好地称为memoization。这是存储过去的计算以避免每次重新计算它们的想法。
给定一个递归函数,比如说:
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 件商品的商店。问题是您的tiny knapsack 最多只能容纳 W 公斤。知道每件物品的重量 (w[i]) 和价值 (v[i]),您希望最大化您的赃物的价值,这些物品的总重量最多为 W。对于每件物品,您必须做出二元选择 -要么接受,要么离开。
现在,您需要找出子问题是什么。作为一个非常聪明的小偷,您意识到给定项目的最大值 i 和最大重量 w 可以表示为 m[i, w]。此外,m[0, w](最多 0 个权重 w)和 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]
其他资源
- Introduction to Algorithms
- Programming Challenges
- Algorithm Design Manual
示例问题
幸运的是,在竞争性编程方面,动态编程已经真正流行。查看Dynamic Programming on UVAJudge 中的一些练习题,这些练习题将测试您实施动态编程问题并找到重现问题的能力。