分治法(divide-and-conquer)基础(
分治法,从字面上理解即为“分而治之”。分而治之,即将复杂的问题分解为简单的子问题,再把子问题分解为更小的问题,直到问题可以直接得到解答。原问题的解即为子问题解的合并。
分治和递归就像是孪生兄弟一样,想了解分治法,必须先知道递归的基本概念。递归是指子程序(或函数)直接调用自己或通过一系列调用语句间接调用自己,是一种描述问题和解决问题的常用方法。使用递归往往可以使函数的定义和算法的描述简洁且易于理解。
递归有两个基本要素:边界条件,即确定递归到何时终止,也称为递归出口;递归模式,即大问题是如何分解为小问题的,也称为递归体。
一般来说,分治法在每一层递归上都有三个步骤:
- 分解。将原问题分解成一系列子问题。
- 求解。递归地求解各子问题。若子问题足够小,则直接求解。
- 合并。将子问题的解合并成原问题的解。
分治法的典型实例
实例一 归并排序
所谓“归并”,是将两个或两个以上的有序文件合并成为一个新的有序文件。归并排序是把一个有n个记录的无序文件看成是由n个长度为1的有序子文件组成的文件,然后进行两两归并,得到[n/2]个长度为2或1的有序文件,再两两归并,如此重复,直到最后形成包含n个记录的有序文件。
两路归并排序的核心操作是将一维数组中前后相邻的两个有序序列归并为一个有序序列。
根据分治法的三个步骤,归并排序算法可以进行一下操作:
(1)分解。将n个元素分成含[n/2]个元素的子序列。
(2)求解。用归并排序法对两个子序列递归地排序。
(3)合并。合并两个已排序的子序列以得到排序结果。
归并排序的核心步骤是第三步中合并两个已排序好的子序列。为了完成合并,需要引入一个过程merge(A,p,q,r),其中A是一个数组,p,q,r都是下标,满足p<=q<r.该过程假设了子数组A[p..q]和A[q+1..r]都已经排好序,并将它们合并成一个已排好序的子数组代替当前子数组A[p..r]。
merge的具体实现代码如下:
void merge(int a[], int p, int q, int r) { int n1 = q - p + 1; int n2 = r - q; int L[n1+1]; //申明数组L和R的大小均加上1位 int R[n2+1]; int i = 0; int j = 0; for (; i < n1; i++) //将数组a前一半元素复制到数组L中 { L[i] = a[p+i]; } L[n1] = 1000; //在数组L最后放置一张“哨兵牌” for (; j < n2; j++) //将数组a后一半元素复制到数组R中 { R[j] = a[q+j+1]; } R[n2] = 1000; //在数组R最后放置一张“哨兵牌” i = 0; j = 0; for (int k = p; k <= r; k++) //依次将两个数组中较小的元素放入a中 { if (L[i] <= R[j]) { a[k] = L[i]; i++; } else { a[k] = R[j]; j++; } } }
merge代码中,我们可以发现在两个辅助的最后均加入了一个较大的数,即为判断的“哨兵牌”。这样当最后进行循环操作时,每当露出一张“哨兵牌”,程序可以知道该循环已经要结束了。因为“哨兵牌”不可能是两张中较小的,除非另一个数组也出现了“哨兵牌”。如果两个数组均出现了“哨兵牌”,那么就说明了所有的元素都已经放入了数组a中了,而由于数组a的大小限制,循环已经结束了。如果没有设置“哨兵牌”,可能导致一个数组已经没有了元素,而另外一个数组还有一个元素没有放入a中,那么循环中的if判断语句就会失败,直接跳转执行else里面的语句,会导致结果出错。
如果不使用“哨兵牌”,我们同样可以实现merge所做的任务。代码如下:
void merge(int a[], int a2[], int s, int m, int n) { int i = m + 1; int k = s; for (; s <= m && i <= n; k++) //将a中记录由小到大地并入a2 { if (a[s] < a[i]) a2[k] = a[s++]; else a2[k] = a[i++]; } for (; s <= m; k++) //将剩余a[s..m]复制到a2 a2[k] = a[s++]; for (; i <= n; k++) //将剩余a[i..n]复制到a2 a2[k] = a[i++]; }
为了对整个序列进行排序,我们还需要一个函数merge_sort(A,p,r),它是对子数组A[p..r]进行排序。如果P>=r,则该子数组中至多只有一个元素,当然就是排序好了的。否则,分解步骤计算出下一个下标q,将A[p..r]分成A[p..q]和A[q+1..r]两个数组,分别各含[n/2]个元素,对他们进行排序。
merge_sort的实现代码如下:
void merge_sort(int a[], int p, int r) { int q = 0; if (p < r) { q = (p + r)/2; merge_sort(a, p, q); merge_sort(a, q+1, r); merge(a, p, q, r); } }
这样在进行排序时,首先调用merge_sort,先将问题分解为一个一个的子问题,然后将子问题求解,最后合并得到大问题的解。