分治法——自顶向下,逐步求精

以下全为口胡,看客轻喷。

介绍

分治法是计算机科学中很重要的一种思想。英文为Divide and Conquer,直译即为分治,或者分而治之。直观的理解就是将一个大而难的问题分解为一些小而易的问题,先解决这些易于解决的小问题,再合并这些小问题的解(合并可以是分别求出小问题的解再合并,或者是直接将相同的小问题合并只求解一次),从而得到大问题的解。需要注意的是小问题必须和大问题是同一个类型的问题,或者说解法相同,这样才可以递归求解。我们发现,这种实际上是自顶向下地分解问题。

我们可以说分治思想是被极广泛运用的思想。
很多算法和数据结构采用了这种思想。另外计算机图像渲染将像素点分配给GPU内的多个流处理器分别处理,最后合成出一帧的图像;也会将一个矩阵乘法分解成 n 个部分分配给n个处理器并行处理。

下面介绍一些采用了分治思想的算法和数据结构。

算法

  • 快速排序
    数列每次按小于某个数(可能是数列的首尾元素或者是随机选择或者是三值取中法等)的和大于某个数的标准划分为两个子序列,但子序列内的顺序和在原数列内的顺序一样,时间复杂度期望nlogn

    • 归并排序
      将数列从中间切成两半,分别递归解决两半的排序问题,再合并解,即合并两个有序数列,由于每次合并需要花费 O(n) 的时间,因此时间复杂度为 O(nlogn)
    • 为了能更好地理解分治的思想,我们详细介绍一下归并排序。
      比如我们定义mergeSort(array: list, from: int, to: int)为归并排序函数,那么我们可以这么写:

      join(a: list, b) =
          if (either a or b is empty) b or a
          else if (a[1] < b[1]) [a[1]] + join(a[2:], b)
          else [b[1]] + join(a, b[2:])
      mergeSort(array) =
          join(mergeSort(array[1:mid]), mergeSort(array[mid+1:]))
          where mid = length(array) / 2

      join函数表示合并两个有序数列,复杂度为 O(n) ,意思就是有两个指针ij一开始分别指向a和b的首元素。这是ij之前的元素都已经被检查过了,如果a[i]<b[j],那么a[i]就会小于a[i+1..len(a)]b[j+1..len(b)],也就是说,a[i]是目前剩下的所有元素中的最小值,那么就应该加到结果序列的最后面。直到数列空为止。
      mergeSort函数就是归并排序了,我们取中点均分了数列arrayarray[1..mid]array[mid+1..n],两个子序列经过mergeSort后就是有序的了,这时我们花费O(n)的时间就可以合并两个有序数列,而不需要再像选择排序那样每次都找一遍最值。
      如果我们在纸上画出递归的过程:

      |---------------|
      | 0   0   0   0 |
      |---------------|
      | 1   1 | 2   2 |
      |-------|-------|
      | 3 | 4 | 5 | 6 |
      |---|---|---|---|

      数字表示递归方法的节点编号。
      递归的层数最多是 log2n ,对于每个元素都被访问了 O(log2n) 次,因此时间复杂度为 O(nlogn)

      到这里读者可能会想到 log 的底(或者是每次划分的子问题个数)是不是越大越好呢?显然不是,应该与实际问题相适应。如果划分的子问题数过多,那么合并子问题的时候就会导致麻烦。比如归并排序的有序数列合并,我们之前认为的 O(n) 的合并是基于划分2次的。如果我们令划分的子序列数为 b ,那么最终的时间复杂度是O(bnlogbn)。我们将其可以写成

      O(nlnnblnb)

      这里写图片描述

      我们发现 blnb 的最小值点为 x=e ,也就在2~3之间,而且x=2和x=3的函数值相差不大,也就是说我们划分成2个子问题或者划分成3个子问题的时候,效率是最高的。另一方面,划分成2个子问题的程序比3个的要更好实现,因此我们一般在分治的时候将问题划分为2个子问题。当然在必要时可能会有其他划分方法。

      • 快速傅里叶变换
        处理多项式的乘法,分解多项式并应用复数的对称和周期性减少重复计算
      • 快速幂
        将求n次幂分为2个n/2次幂的较小问题,而两个小问题是相同的,求出一个就知道另外一个的解
      • (在树上的分治)
      • ……

      数据结构

      实际上很多基于树的数据结构都可以认为采用了分治思想,但分治不代表一定是简单的二分。

      • 线段树
        数列每次对半切割,区间查找被划分为几个已有的线段树节点的并。

      线段树是最基础的数据结构之一,这里较详细地讲一下。譬如给定一个数列1, 2, 3, 4,构造这个数列的线段树。
      结果如下:

      |---------------|
      | 1   2   3   4 |
      |---------------|
      | 1   2 | 3   4 |
      |---------------|
      | 1 | 2 | 3 | 4 |
      |---------------|

      看起来和归并排序的递归过程很像。实际上因为都有分治思想,像是必然的。
      那么我们查询区间 [2,3] 的和,我们会这么走:
      [2,3] in [1,4]->[2,2] in [1,2] & [3,3] in [3,4] -> [2,2] in [2,2] & [3,3] in [3,4]
      也就是我们将这个区间按照适当的方式(按照已有的划分方法)划分成2个子区间(或者正好不需要划分,继续往下走),得到2个子区间(子问题)的和,再加起来,得到当前区间(大问题)的和。我们很容易知道,这么做的时间复杂度是均摊 logn 的。

      对半划分子问题的优势我们在将归并排序的时候已经讲过了,这里的理由类似。但是像B树等就不是对半划分的。
      - 伸展树
      数列每次从某个点切割,某个点可以代表一个区间
      - 划分树
      数列每次按较小的一半和较大的一般切割
      - 树状数组
      和线段树类似
      - ……

      一些题目

      以下题目分类仅供参考。。

      分治

      POJ

      2083

      BZOJ

      1095, 2001, 2229, 2287, 2458, 2229, 3614, 3658, 3879, 4025, 4059

      CodeForces

      321E, 576E

      树分治

      POJ

      1741, 1987, 2114,

      BZOJ

      1095, 1468, 1758, 2152, 2599, 3365, 3435, 3648, 3672, 3697, 3784, 3924, 4012, 4016, 4372

      HDU

      4812

      CDQ分治

      BZOJ

      1176, 1492, 1537, 2244, 2683, 2716, 2961, 3262, 3295

      总结

      我们先介绍了分治法是怎么样的一种思想,并介绍了一些典型(常见)的运用了分治思想的算法和数据结构。我们了解了分治法是如何自顶向下的。实际上现实生活中我们也能见到分治法的运用。我们写一个策划,会将策划的不同环节交给不同的人做,最后总策划再将各个人做好的各个部分的策划修改并整合成最终的总策划。而每个写子策划的人又可能将任务继续下派分发给部门内部多个人员共同完成。可以说,计算机科学中的分治法源于生活。如此重要的思想,我们一定要理解。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值