关于算法:分而治之算法的差异与动态规划

Difference between Divide and Conquer Algo and Dynamic Programming

分而治之算法和动态规划算法有什么区别? 这两个术语有何不同? 我不了解他们之间的区别。

请举一个简单的例子来说明两者之间的任何区别,以及它们看起来相似的依据。


分而治之

分而治之的工作原理是将问题分为多个子问题,然后递归地解决每个子问题,然后将这些解决方案结合起来。

动态编程

动态编程是解决重叠子问题的技术。每个子问题仅解决一次,并且每个子问题的结果存储在表中(通常实现为数组或哈希表),以供将来参考。这些子解决方案可用于获取原始解决方案,并且存储子问题解决方案的技术称为备忘录。

您可能会想到DP = recursion + re-use

理解差异的一个经典示例是查看这两种获取第n个斐波那契数的方法。从麻省理工学院检查此材料。

分而治之的方法
Divide and Conquer approach

动态规划方法
enter image description here


分而治之与动态编程之间的另一个区别可能是:

分而治之:

  • 在子问题上做更多的工作,因此有更多的时间消耗。
  • 在分而治之中,子问题彼此独立。
  • 动态编程:

  • 仅解决一次子问题,然后将其存储在表中。
  • 在动态编程中,子问题不是独立的。

  • 有时在递归编程时,您多次调用具有相同参数的函数是不必要的。

    著名的斐波那契数示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
               index: 1,2,3,4,5,6...
    Fibonacci number: 1,1,2,3,5,8...

    function F(n) {
        if (n < 3)
            return 1
        else
            return F(n-1) + F(n-2)
    }

    让我们运行F(5):

    1
    2
    3
    4
    F(5) = F(4) + F(3)
         = {F(3)+F(2)} + {F(2)+F(1)}
         = {[F(2)+F(1)]+1} + {1+1}
         = 1+1+1+1+1

    所以我们打电话给:
    1倍F(4)
    2倍F(3)
    3倍F(2)
    2倍F(1)

    动态编程方法:如果多次调用具有相同参数的函数,请将结果保存到变量中,以在下次直接访问它。迭代方式:

    1
    2
    3
    4
    5
    6
    7
    8
    if (n==1 || n==2)
        return 1
    else
        f1=1, f2=1
        for i=3 to n
             f = f1 + f2
             f1 = f2
             f2 = f

    让我们再次调用F(5):

    1
    2
    3
    4
    5
    fibo1 = 1
    fibo2 = 1
    fibo3 = (fibo1 + fibo2) = 1 + 1 = 2
    fibo4 = (fibo2 + fibo3) = 1 + 2 = 3
    fibo5 = (fibo3 + fibo4) = 2 + 3 = 5

    如您所见,只要需要多次调用,您都只需访问相应的变量即可获取值,而无需重新计算。

    顺便说一句,动态编程并不意味着将递归代码转换为迭代代码。如果需要递归代码,也可以将子结果保存到变量中。在这种情况下,该技术称为记忆。对于我们的示例,它看起来像这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // declare and initialize a dictionary
    var dict = new Dictionary<int,int>();
    for i=1 to n
        dict[i] = -1

    function F(n) {
        if (n < 3)
            return 1
        else
        {
            if (dict[n] == -1)
                dict[n] = F(n-1) + F(n-2)

            return dict[n]                
        }
    }

    因此,与分而治之的关系是D&D算法依赖于递归。而且它们的某些版本具有"具有相同参数问题的多功能调用"。在需要DP来改善D&D算法的T(n)的此类示例中,搜索"矩阵链乘法"和"最长公共子序列"。


    动态编程和分而治之相似

    就目前而言,我可以说动态编程是分而治之范式的扩展。

    我不会将它们视为完全不同的东西。因为它们都通过递归将一个问题分解为两个或多个相同或相关类型的子问题来工作,直到它们变得足够简单以至于可以直接解决。然后将子问题的解决方案合并以给出原始问题的解决方案。

    那么为什么我们仍然有不同的范例名称,为什么我将动态编程称为扩展。这是因为仅当问题具有某些限制或先决条件时,才可以将动态编程方法应用于该问题。然后,动态编程扩展了采用记忆或制表技术的分而治之方法。

    让我们一步一步走...

    动态编程先决条件/限制

    正如我们刚刚发现的那样,为了应用动态编程,必须具有两个必须解决的"分而治之"关键属性:

    • 最优子结构?-最优解可以从子问题的最优解中构造出来

    • 子问题可以重叠吗?—问题可以分解为多个子问题,这些子问题可以重复使用几次,或者递归算法可以反复解决相同的子问题,而不是总生成新的子问题

    一旦满足这两个条件,就可以说可以使用动态规划方法解决分而治之的问题。

    分而治之的动态编程扩展

    动态编程方法使用两种技术(内存化和制表法)扩展了分而治之的方法,这两种技术的目的都是存储和重用可能大大提高性能的子问题解决方案。例如,斐波那契函数的朴素递归实现具有O(2^n)的时间复杂度,其中DP解决方案仅用O(n)的时间即可做到。

    备注(自上而下的缓存填充)是指缓存和重新使用以前计算的结果的技术。记住的fib函数将如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    memFib(n) {
        if (mem[n] is undefined)
            if (n < 2) result = n
            else result = memFib(n-2) + memFib(n-1)

            mem[n] = result
        return mem[n]
    }

    制表(自下而上的高速缓存填充)相似,但侧重于填充高速缓存的条目。迭代计算缓存中的值最容易。 fib的列表版本如下所示:

    1
    2
    3
    4
    5
    6
    7
    tabFib(n) {
        mem[0] = 0
        mem[1] = 1
        for i = 2...n
            mem[i] = mem[i-2] + mem[i-1]
        return mem[n]
    }

    您可以在此处阅读更多有关记忆和制表比较的信息。

    您应该在此处掌握的主要思想是,由于我们的分而治之问题具有重叠的子问题,因此可以缓存子问题解决方案,从而使备忘/制表逐步成为现实。

    那么DP和DC到底有什么区别

    由于我们现在已经熟悉了DP的先决条件及其方法,因此,我们准备将上面提到的所有内容合而为一。

    Dynamic Programming vs Divide-and-Conquer

    如果要查看代码示例,可以在这里查看更详细的说明,在其中可以找到两个算法示例:二进制搜索和最小编辑距离(Levenshtein距离),它们说明了DP和DC之间的差异。


    我假设您已经阅读过Wikipedia和其他学术资源,所以我不会再利用任何这些信息。我还必须告诫我无论如何都不是计算机科学专家,但是我将以我的两分钱分享我对这些主题的理解...

    动态编程

    将问题分解为离散的子问题。 Fibonacci序列的递归算法是动态规划的一个示例,因为它首先通过求解fib(n-1)来求解fib(n)。为了解决原始问题,它解决了另一个问题。

    分而治之

    这些算法通常可以解决问题的相似部分,然后将它们放到最后。 Mergesort是分而治之的经典示例。此示例与斐波那契示例之间的主要区别在于,在归并排序中,除法(理论上)可以是任意的,并且无论如何分割,您仍在合并和排序。无论如何拆分,都必须完成相同数量的工作才能对数组进行归并排序。解决fib(52)比解决fib(2)需要更多的步骤。


    我认为Divide & Conquer是一种递归方法,而Dynamic Programming是表填充。

    例如,Merge SortDivide & Conquer算法,因为在每一步中,您都将数组分成两半,对这两个半部分递归调用Merge Sort,然后将它们合并。

    KnapsackDynamic Programming算法,因为您要填写一个表,该表表示整体背包子问题的最佳解决方案。表格中的每个条目均对应于您可以在给定的物品1-j的重量w袋中携带的最大值。


    分而治之在每个递归级别都涉及三个步骤:

  • 将问题分为子问题。
  • 通过递归解决子问题来解决它们。
  • 将子问题的解决方案合并为原始问题的解决方案。

    • 这是一种自上而下的方法。
    • 它在子问题上做更多的工作,因此有更多的时间
      消费。
    • 例如。斐波那契数列的第n个项可以用O(2 ^ n)时间复杂度来计算。

  • 动态编程涉及以下四个步骤:
    1.表征最佳解决方案的结构。
    2.递归定义最优解的值。
    3.计算最佳解决方案的价值。
    4.根据计算出的信息构建最佳解决方案。

    • 这是一种自下而上的方法。
    • 与分而治之相比,时间消耗更少,因为我们使用的是较早计算的值,而不是再次计算。
    • 例如。斐波那契数列的第n个项可以用O(n)时间复杂度来计算。

    为了更容易理解,让我们将分而治之视为强力解决方案,并将其优化视为动态编程。
    N.B.子问题重叠的分治法只能使用dp进行优化。