许多有用的算法在结构上是递归的:为了解决一个给定的问题,算法一次或多次递归地调用其自身以解决紧密相关的若干子问题。这些算法典型地遵循分治法的思想:将原问题分解为几个规模较小但类似于原问题的子问题,递归地求解这些子问题,然后再合并这些子问题的解来建立原问题的解。
分治模式在每层递归时都有三个步骤:
分解(Divide)步骤将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
解决(Conquer)步骤递归地求解出子问题。如果子问题的规模足够小,则停止递归,直接求解。
合并(Combine)步骤将子问题的解组合成原问题的解。
当子问题足够大,需要递归求解时,我们称之为递归情况(recursive case)。当子问题变得足够小,不再需要递归时,我们说递归已经“触底”,进行了基本情况(base case)。有时,除了与原问题形式完全一样的规模更小的子问题外,还需要求解与原问题不完全一样的子问题。我们将这些子问题的求解看做合并步骤的一部分。
1. 归并排序
归并排序算法完全遵循分治模式。直观上其操作如下:
分解:分解待排序的n个元素的序列成各具n/2个元素的两个子序列。
解决:使用归并排序递归地排序两个子序列。
合并:合并两个已排序的子序列以产生已排序的答案。
归并排序算法的关键操作是“合并”步骤中两个已排序序列的合并。我们通过调用一个辅助过程MERGE(A, p, q, r)来完成合并,其中A是一个数组,p、q和r是数组下标,满足p <= q < r。该过程假设子数组A[p..q]和A[q+1..r]都已排好序。它合并这两个子数组形成单一的已排好序的子数组并代替当前的子数组A[p..r]。
过程MERGE需要O(n)的时间,其中n = r-p+1是待合并元素的总数。代码如下所示:(使用INF作为哨兵值,用于简化代码)
MERGE(A, p, q, r)
1. n1 = q-p+1
2. n2 = r-q
3. let L[1..n1+1] and R[1..n2+1] be new arrays
4. for i = 1 to n1
5. L[i] = A[p+i-1]
6. for j = 1 to n2
7. R[i] = A[q+j]
8. L[n1+1] = INF
9. R[n2+1] = INF
10. i = 1
11. j = 1
12. for k = p to r
13. if L[i] <= R[j]
14. A[k] = L[i]
15. i = i + 1
16. else A[k] = R[j]
17. j = j+1
现在我们可以把MERGE作为归并排序算法的一个子程序来用。下面的过程MERGE-SORT(A, p, r)排序子数组A[p..r]中的元素。若p >= r,则该子数组最多有一个元素,所以已经排好序。否则,分解步骤简单地计算一个下标q,将A[p..r]分成两个子数组。伪代码如下:
MERGE-SORT(A, p, r)
1. if p < r
2. q = (p+r)/2
3. MERGE-SORT(A, p, q)
4. MERGE-SORT(A, q+1, r)
5. MERGE(A, p, q, r)
2. 最大子数组问题
问题:寻找数组A的和最大的非空连续子数组。我们称这样的连续子数组为最大子数组(maximum subarray)。
我们来思考如何使用分治技术来求解最大子数组问题。假定我们要寻找子数组A[low..high]的最大子数组。使用分治技术意味着我们要将子数组划分为两个规模尽量相等的子数组。也就是说,找到子数组的中央位置,比如mid,然后考虑求解两个子数组A[low..mid]和A[mid+1..high],A[low..high]的任何连续子数组A[i..j]所处的位置必然是以下三种情况之一:
完全位于子数组A[low..mid]中,因此low <= i <= j <= high。
完全位于子数组A[mid+1..high]中,因此mid < i <= j <= high。
跨越了中点,因此low <= i <= mid < j <= high。
因此,A[low..high]的一个最大子数组所处的位置必然是这三种情况之一。我们可以递归地求解A[low..mid]和A[mid+1..high]的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此,剩下的全部工作就是寻找跨越中点的最大子数组,然后这三种情况中选取和最大者。
我们可以很容易地在线性时间内求出跨越中点的最大子数组。此问题并非原问题规模更小的实例,因为它加入了限制——求出的子数组必须跨越中点。因此,我们只需找出开如A[i..mid]和A[mid+1..j]的最大子数组,然后将其合并即可。过程FIND-MAX-CROSSING-SUBARRAY接收数组A和下标low、mid和high为输入,返回一个下标元组划定跨越中点的最大子数组的边界,并返回最大子数组中值的和。
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
1. left-sum = -INF
2. sum = 0
3. for i = mid downto low
4. sum = sum + A[i]
5. if sum > left-sum
6. left-sum = sum
7. max-left = i
8. right-sum = -INF
9. sum = 0
10. for j = mid+1 to high
11. sum = sum + A[i]
12. if sum > right-sum
13. right-sum = sum
14. max-right = j
15. return (max-left, max-right, left-sum + right-sum)
有了一个线性时间的FIND-MAX-CROSSING-SUBARRAY在手,我们就可以设计求解最大子数组问题的分治算法的伪代码了:
FIND-MAXIMUM-SUBARRAY(A, low, right)
1. if high == low
2. return (low, high, A[low])
3. else mid = (low + high)/2
4. (left-low, left-high, left-sum) =
FIND-MAXIMUM-SUBARRAY(A, low, mid)
5. (cross-low, cross-high, cross-sum) =
FIND-MAX-CROSSING-SUBARRAY(A, low, mid, high)
6. (right-low, right-high, right-sum) =
FIND-MAXIMUM-SUBARRAY(A, mid, high)
7. if left-sum >= right-sum and left-sum >= cross-sum
8. return (left-low, left-high, left-sum)
9. else if right-sum >= left-sum and right-sum >= cross-sum
10. return (right-low, right-high, right-sum)
11. else return (right-low, right-high, right-sum)
3. 求解递归式