什么是动态规划?

它与递归、记忆等有什么不同?

我读过维基百科上关于它的文章,但我还是不太明白。


当前回答

记忆是指存储函数调用之前的结果(给定相同的输入,真正的函数总是返回相同的结果)。在存储结果之前,算法的复杂性并没有什么不同。

递归是函数调用自身的方法,通常使用较小的数据集。由于大多数递归函数可以转换为类似的迭代函数,这对算法复杂性也没有影响。

动态规划是解决较容易解决的子问题,并由此建立答案的过程。大多数DP算法将处于贪婪算法(如果存在的话)和指数算法(枚举所有可能性并找到最佳的一个)之间的运行时间。

DP算法可以用递归来实现,但它们不必这样做。 DP算法不能通过记忆来加速,因为每个子问题只被解决(或“solve”函数被调用)一次。

其他回答

下面是一个简单的python代码示例,用于递归、自顶向下、自底向上的斐波那契数列:

递归:O (2 n)

def fib_recursive(n):
    if n == 1 or n == 2:
        return 1
    else:
        return fib_recursive(n-1) + fib_recursive(n-2)


print(fib_recursive(40))

自上而下:O(n)高效的大输入

def fib_memoize_or_top_down(n, mem):
    if mem[n] is not 0:
        return mem[n]
    else:
        mem[n] = fib_memoize_or_top_down(n-1, mem) + fib_memoize_or_top_down(n-2, mem)
        return mem[n]


n = 40
mem = [0] * (n+1)
mem[1] = 1
mem[2] = 1
print(fib_memoize_or_top_down(n, mem))

自底向上:O(n)为简单和小的输入大小

def fib_bottom_up(n):
    mem = [0] * (n+1)
    mem[1] = 1
    mem[2] = 1
    if n == 1 or n == 2:
        return 1

    for i in range(3, n+1):
        mem[i] = mem[i-1] + mem[i-2]

    return mem[n]


print(fib_bottom_up(40))

动态规划

定义

动态规划(DP)是求解问题的一种通用算法设计技术 带有重叠子问题的问题。这项技术是美国人发明的 数学家理查德·贝尔曼在20世纪50年代。

关键理念

其关键思想是保存重叠的小问题的答案,以避免重新计算。

动态规划属性

使用较小实例的解决方案求解实例。 较小实例的解决方案可能需要多次使用, 所以把结果存储在一个表中。 因此,每个较小的实例只解决一次。 额外的空间被用来节省时间。

我对动态规划(一种针对特定类型问题的强大算法)也非常陌生。

简单地说,只需将动态规划视为使用前面知识的递归方法

以前的知识在这里是最重要的,跟踪你已经有的子问题的解决方案。

下面是来自维基百科的dp最基本的例子

求斐波那契数列

function fib(n)   // naive implementation
    if n <=1 return n
    return fib(n − 1) + fib(n − 2)

让我们用n = 5来分解函数调用

fib(5)
fib(4) + fib(3)
(fib(3) + fib(2)) + (fib(2) + fib(1))
((fib(2) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))
(((fib(1) + fib(0)) + fib(1)) + (fib(1) + fib(0))) + ((fib(1) + fib(0)) + fib(1))

特别地,fib(2)从零开始计算了三次。在较大的示例中,需要重新计算fib(或子问题)的更多值,从而形成指数时间算法。

现在,让我们尝试将我们已经找到的值存储在一个数据结构(比如Map)中

var m := map(0 → 0, 1 → 1)
function fib(n)
    if key n is not in map m 
        m[n] := fib(n − 1) + fib(n − 2)
    return m[n]

这里我们将子问题的解决方案保存在地图中,如果我们还没有的话。这种我们已经计算过的保存值的技术被称为记忆化。

最后,对于一个问题,首先尝试找到状态(可能的子问题,并尝试考虑更好的递归方法,以便将以前的子问题的解决方案用于进一步的子问题)。

动态规划是一种在递归算法中避免多次计算同一子问题的技术。

让我们以斐波那契数为例:找到定义的第n个斐波那契数

Fn = Fn-1 + Fn-2, F0 = 0, F1 = 1

递归

最明显的方法是递归:

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1

    return fibonacci(n - 1) + fibonacci(n - 2)

动态规划

自顶向下——记忆

递归做了很多不必要的计算,因为给定的斐波那契数将被计算多次。一个简单的改进方法是缓存结果:

cache = {}

def fibonacci(n):
    if n == 0:
        return 0
    if n == 1:
        return 1
    if n in cache:
        return cache[n]

    cache[n] = fibonacci(n - 1) + fibonacci(n - 2)

    return cache[n]

自底向上

更好的方法是通过按正确的顺序计算结果来摆脱递归:

cache = {}

def fibonacci(n):
    cache[0] = 0
    cache[1] = 1

    for i in range(2, n + 1):
        cache[i] = cache[i - 1] +  cache[i - 2]

    return cache[n]

我们甚至可以使用常数空间,在整个过程中只存储必要的部分结果:

def fibonacci(n):
  fi_minus_2 = 0
  fi_minus_1 = 1

  for i in range(2, n + 1):
      fi = fi_minus_1 + fi_minus_2
      fi_minus_1, fi_minus_2 = fi, fi_minus_1

  return fi

如何应用动态规划? 找出问题中的递归。 自顶向下:将每个子问题的答案存储在一个表中,以避免重新计算。 自底向上:找到评估结果的正确顺序,以便在需要时获得部分结果。

动态规划通常适用于具有固有的从左到右顺序的问题,如字符串、树或整数序列。如果单纯递归算法不能多次计算同一子问题,动态规划就没有帮助。

我做了一个问题集合来帮助理解逻辑:https://github.com/tristanguigue/dynamic-programing

简而言之,就是递归记忆和动态规划的区别

顾名思义,动态规划是使用前面的计算值动态地构造下一个新的解决方案

在哪里应用动态规划:如果你的解决方案是基于最优子结构和重叠子问题,那么在这种情况下,使用之前的计算值将是有用的,这样你就不必重新计算它。这是一种自下而上的方法。假设你需要计算fib(n)在这种情况下,你所需要做的就是将之前计算的fib(n-1)和fib(n-2)的值相加

递归:基本上将你的问题细分为更小的部分,以轻松解决它,但请记住,如果我们在其他递归调用中有相同的值,它不会避免重新计算。

记忆:基本上将旧的计算递归值存储在表中被称为记忆,这将避免重新计算,如果它已经被以前的一些调用计算过,因此任何值都将计算一次。因此,在计算之前,我们检查这个值是否已经计算过,如果已经计算过,那么我们从表中返回相同的值,而不是重新计算。这也是一种自上而下的方法