分治法 ( Divide And Conquer ) 详解

Posted Xurtle

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分治法 ( Divide And Conquer ) 详解相关的知识,希望对你有一定的参考价值。

文章目录

引言

在这篇 blog 中,我首先会介绍一下分治法的范式,接着给出它的递归式通式,最后我会介绍三种方法(代入法,递归树,和主方法)求解递归式。

分治法的范式

算法导论把分治法描述为以下3步:

1、Divide the problem into a number of subproblems that are smaller instances of the same problem.

2、Conquer the subproblems by solving them recursively. If the subproblem sizes are small enough, however, just solve the subproblems in a straightforward manner.

3、Combine the solutions to the subproblems into the solution for the original problem.

归并排序就是一个用分治法的经典例子,这里我用它来举例描述一下上面的步骤:

1、归并排序首先把原问题拆分成2个规模更小的子问题。
2、递归地求解子问题,当子问题规模足够小时,可以一下子解决它。在这个例子中就是,当数组中的元素只有1个时,自然就有序了。
3、最后,把子问题的解(已排好序的子数组)合并成原问题的解。

递归式

对于每个算法来说,我们不可避免的要分析它的时间复杂度,而递归式可以很自然地刻画分治算法的运行时间。下面我给出一下递归式的通式:

假设把原问题分解成 a 个子问题,每个子问题的规模都是原问题的 1 b \\frac1b b1,求解一个规模为 n b \\fracnb bn 的问题需要的时间为 T ( n b ) T(\\fracnb) T(bn),因此需要 a T ( n b ) aT(\\fracnb) aT(bn) 的时间来求解 a 个子问题。如果分解问题所需的时间为 D ( n ) D(n) D(n),合并子问题的解到原问题的解需要的时间为 C ( n ) C(n) C(n),那么递归式为:

T ( n ) = a T ( n b ) + D ( n ) + C ( n ) T(n)=aT(\\fracnb)+D(n)+C(n) T(n)=aT(bn)+D(n)+C(n)

对于归并排序来说,我们每次都是把原问题一分为二并且每个子问题的规模都是原来的1/2,所以 a = b = 2; 分解问题的时间为 Θ ( 1 ) \\Theta(1) Θ(1),这是因为你只是求解数组索引的中间值; 合并子问题所需要的时间为 Θ ( n ) \\Theta(n) Θ(n).

得到了递归式以后,我们如何得到算法的渐近时间呢?比如我们得到了归并排序的递归式,如何求解它的渐近时间呢?当然了,我们都知道它的时间是 Θ ( n l g n ) \\Theta(n lgn) Θ(nlgn). 下面我来介绍三种求解递归式的方法。

求解递归式的三种方法

这三种方法分别是:代入法,递归树法,和主方法。代入法是一种十分强大的方法,它几乎可以求解所有的递归式。然而,对于一些“小鸡”来说,不需要这么强大的“牛刀”。对于一些特定类型的递归式(具体类型在主方法的小节中会介绍),用主方法可以用更少的时间得到一个更确切的界。

代入法

The substitution method for solving recurrences comprises two steps:

1、Guess the form of the solution.

2、Use mathematical induction to find the constants and show that the solution works.

如上面的步骤所示,代入法虽然很强大,但是我们必须首先猜测出一个解,然后用数学归纳法来证明这个解是正确的。并于如何猜测并没有一个通用的步骤,我们可以依靠经验或者用下面介绍的递归树法来进行猜测。关于一些数学基础不好的同学,强烈推荐 MIT 的 Mathematics for Computer Science 课程,如果没有太多时间的同学,我把归纳法的通用步骤从课程的书中截图下来,有一点需要注意的就是,归纳法的演绎步骤一定要用到前面的假设,否则你的归纳法就有问题了!

关于代入法的例子请参考康奈尔大学的课程笔记:Substitution method example. 一个小小的建议:大家不要看到数学公式就不愿意去看,如果你静下心来去看,并没有你想像的那么难。

递归树法

在这个小节中,俺打算用递归树法分析一下归并排序和求斐波那契序列的时间复杂度。

下面是算法导论中关于归并排序的伪代码,其中 MERGE 的时间为 O(n). 假设输入规模为 N, 我们来看看各个部分所需要的时间:

  • 第2行代码用于分解问题:时间为 O(1)
  • 第 3, 4 行代码是2个相同规模的子问题:时间为 2T(N/2)
  • 第5行代码是合并2个已经有序的子问题:时间为 O(N)

因此,递归式为:T(N) = 2T(N/2) + O(N)

基于上面得到的递归式,可以得到下图所示的递归树。树为什么是这样的呢?可以从上面的伪码来分析。规模为 N 的输入进算法,经过了第2,3,4 行代码以后,就得到了2个有序的数组,然后合并这2个数组的时间是 O(N). 所以,树的根结点正是合并2个规模为 N/2 的有序数组的时间,它为 O(N). 也就是说,经过2,3,4行代码以后,得到了2个规模为 N/2 的子问题,它们是根结点的2个孩子,然后合并这2个孩子所需要的时间为 O(N). 因此,把每一层的工作量汇总起来是 O(N),总共有 logN 层,所以总的时间复杂度为 O(N logN).


接下来,我来谈谈关于求斐波那契序列的递归树。斐波那契序列的递归式如下:

T(N) = T(N - 1) + T(N - 2) + O(1)

上式中之所以有 O(1),是因为一旦你知道了 T(N - 1) 和 T(N - 2) 的值,合并它们的时间为常量,也就是一个加法运算。这个递归式的递归树如下图所示。


假设输入规模为 N 的情况,从上图不难看出,树的高度为 N. 由于合并2个孩子结点,得到父结点的解所需的时间为 O(1),因此我们只需要算出整个树有多少结点就知道时间复杂度了。用满二叉树来估算树中的结点数为 2 N − 1 2^N -1 2N1,因此用递归的方法求斐波那契序列的时间复杂度为 O(2^N). 当然,用动态规划的方法可以得到更好的时间。

下面是另外2个例子,这里我就不过多解释了。

1、 T ( n ) = 2 T ( n / 2 ) + n 2 T(n) = 2T(n/2) + n^2 T(n)=2T(n/2)+n2

2、 T ( n ) = T ( n / 3 ) + T ( 2 n / 3 ) + n T(n) = T(n/3) + T(2n/3) + n T(n)=T(n/3)+T(2n/3)+n

资料来源:Recursion Trees

主方法

上面我已经给出了通用的递归式,把上面的公式重写一下,得到了如下图所示的公式,每个变量所代表的含义也都已经清晰的给出了:

那么如何应用主方法呢?其实很简单,就是简单地应用主定理,它类似于数学中的分段函数,不同的 case 会得到不同的结果。对于主定理来说,它有3个 case:

Case 1:

Case 2:

Case 3:

不难看出,我们只需要找出a,b,和 c,然后比较大小,就能决定出是哪个 case 了。Master theorem 中给出了每个 case 的例子。关于主定理的证明,算法导论中用递归树给出了相应的证明。上面我已经说过了,主定理只针对特定类型的递归式子有效,如果出现下图中的情况,就不能应用主定理了:

以上是关于分治法 ( Divide And Conquer ) 详解的主要内容,如果未能解决你的问题,请参考以下文章

分治策略Divide and Conquer

The Divide and Conquer Approach - 归并排序

c_cpp 合并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。合并排序法是将两个(或两个以上)有序表合并成一个新的有序表,

分治(Divide and Conquer)算法之归并排序

归并排序

归并排序