在分治策略中,我们递归地求解一个问题,在每层递归中应用如下三个步骤:
分解:将问题划分为一些子问题,子问题的形式与原问题一样,只是规模更小。
解决递归地求解出子问题。。如果子问题规模足够小,则停止递归,直接求解。
合并 将子问题的解组合成原问题的接。
当子问题足够大,需要递归求解时,我们称之为递归情况。当子问题变得足够小,不再需要递归时,我们说递归已经“触底”,进入了基本情况。
在本章中,我们将看到更多基于分治策略的算法。第一个算法求解最大子数组问题,其输入是一个数值数组,算法需要确定具有最大和的连续子数组。然后我们将看到两个求解n*n的矩阵乘法问题的分治算法。其中一个的运行时间为Θ(
递归式
一个递归式就是一个等式或不等式,它通过更小的输入上的函数值来描述一个函数。例如,在2.3.2节中,我们用递归式描述了MERGE-SORT过程的最坏情况运行时间T(n):
求解可得T(n)=Θ(
递归可以有很多形式。一个递归算法可能将问题划分为规模不等的子问题,子问题的规模不必是原问题的一个固定比例。例如:线性查找的递归版本仅生成一个子问题,其规模仅比原问题的规模少一个元素。每次递归调用将花费常量时间在加上下一层递归调用的时间,因此递归式为
本章介绍三种求解递归式的方法,即得出算法的“Θ”或“O”渐近界的方法。
- 代入法:我们猜测一个接,然后用数学归纳法郑敏这个界是正确的。
- 递归数法:将递归式转换成一棵树,其节点表示不同层次的递归调用产生的代价,然后采用边界和技术来求解递归式。
- 主方法:可求解形如下面公式的递归式的界:
T(n)=aT(n/b) + f(n)
其中a≥1,b>1 ,f(n)是一个给定的函数。这种形式的递归式很常见,它刻画了这样一个分治算法:生成a个问题,每个子问题的规模是原问题规模的1/b,分解和合并步骤总共花费时间为f(n)。
在本章中,我们将使用主方法来确定最大子数组问题和矩阵相乘问题的分治算法的运行时间。我们偶尔会遇到不是等式而是不等式的递归式,例如T(n)≤2T(n/2)+Θ(n) 因为这样一种递归式仅描述了T(n)的一个上界,因此可以用O符号而不是Θ符号来描述其解。类似地,如果不等式为T(n)≥2T(n/2)+Θ(n) ,则由于递归式只给出了T(n)的一个下界,我们应使用Ω符号来描述其解。
递归式技术细节
在实际应用中,我们常常忽略递归式的一些技术细节。当声明、求解递归式时,我们常常忽略向下取整、向上取整及边界条件。但是,在本章中,我们会讨论某些细节,展示递归式求解方法的要点。
4.1 最大子数组问题
暴力求解
问题变换
问题转化为寻找数组A的和最大的非空连续子数组。称这样的连续子数组为最大子数组。
只有当数组中包含负数时,最大子数组问题才有意义。
使用分治策略的求解方法
假定我们要寻找A[low..high]的最大子数组。使用分治技术意味着我们要将子数组划分为两个规模尽量相等的子数组。比如mid,然后考虑求解两个子数组A[low..mid]和A[mid+1..high]。A[low..high]的任何连续子数组A[i..j]所处的位置必然是以三种情况之一:
- 完全位于子数组A[low..mid]中,因此
low≤i≤j≤mid 。 - 完全位于子数组A[mid+1..high]中,因此
mid<i≤j≤high 。 - 跨越了中点,因此
low≤i≤mid<j≤high .
我们可以递归地求解A[low..mid]和A[mid+1..high]的最大子数组,因为这两个子问题仍是最大子数组问题,只是规模更小。因此剩下的全部工作就是寻找跨越中点的最大子数组,然后在三种情况中选取和最大者。
我们可以很容易地在线性时间内求出跨越中点的最大子数组。此问题并非原问题规模更小的实例,因为它加入了限制——求出的子数组必须跨越中点。任何跨越中点的子数组都由两个子数组A[i..mid]和A[mid+1..j]组成,其中low≤i≤mid<j≤high 。因此我们只需找出形如A[i..mid]和A[mid+1..j]的最大子数组,然后将其合并即可。过程FIND-MAX-CROSSING-SUBARRAY接受数组A和下标low、mid、high为输入,返回一个下标元组划定跨越中点的最大子数组的边界,并返回最大子数组中值的和。
FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
left-sum = -∞
sum = 0
for i = mid downto low
sum = sum + A[i]
if sum > left-sum
left-sum = sum
max-left = i
right-sum = -∞
sum = 0
for j = mid + 1 to high
sum = sum + A[j]
if sum > right-sum
right-sum = sum
max-right = j
return (max-left,max-rght,left-sum+right-sum)
如果子数组A[low..high]包含n个元素,则调用FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)花费Θ(n)时间。然后设计求解最大子数组问题的分治算法的伪代码:
FIND-MAXIMUM-SUBARRAY(A,low,high)
if high == low
return(low,high,A[low])
else mid = ⌊(low+high)/2⌋
(left-low,left-high,left-sum) = FIND-MAXIMUM-SUBARRAY(A,low,mid)
(right-low,right-high,right-sum)=FIND-MAXIMUM-SUBARRAY(A,mid+1,high)
(cross-low,cross-high,cross-sum)=FIND-MAX-CROSSING-SUBARRAY(A,low,mid,high)
if left-sum >= right-sum and left-sum >= cross-sum
return (left-low, left-high,left-sum)
else if right-sum >= left-sum and right-sum >= cross-sum
return (right-low, right-high,right-sum)
else return (cross-low, cross-high,cross-sum)
分治算法的分析
我们用T(n)表示FIND-MAXIMUM-SUBARRAY求解n个元素的最大子数组的运行时间:
此递归式与归并排序的递归式一样。其解为T(n)=Θ(
4.2 矩阵乘法的Strassen算法
若A=
我们需要计算
SQUARE-MATRIX-MULTIPLY(A,B)
n = A.rows
let C be a new n*n matrix
for i = 1 to n
for j = 1 to n
Cij = 0
for k = 1 to n
Cij = Cij + Aik · Bkj
return C
由于三重for循环的没一重都恰好是执行n步。因此过程SQUARE-MATRIX-MULTIPLY花费
在本节中,我们将看到Strassen的著名n*n矩阵相乘的递归算法,其运行时间为
一个简单的分治过程
为简单起见,当使用分治算法计算矩阵积
假定将A、B和C均分解为4个n/2 * n/2的子矩阵:
因此可以将公式改写为:
等价于下面4个公式:
可以利用这些公式设计一个直接的递归分治算法:
SQUARE-MATRIX-MULTIPLY-RECURSIVE(A,B)
n = A.rows
let C be a new n*n matrix
if n == 1
c_11 = a_11 · b_11
else partition A,B and C as in equations(4.9)
C_11 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11,B_11)
+ SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12,B_21)
C_12 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_11,B_12)
+ SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_12,B_22)
C_21 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21,B_11)
+ SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22,B_21)
C_22 = SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_21,B_12)
+ SQUARE-MATRIX-MULTIPLY-RECURSIVE(A_22,B_22)
return C
这段伪代码掩盖了一个微妙但重要的实现细节,第 5 行应该如何分解矩阵?如果我们真的创建12个新的n/2 * n/2矩阵,将会花费Θ(
令T(n)表示用此过程计算两个n*n矩阵乘积的时间。
利用主方法求解递归式,得到的解为T(n) = Θ
Strassen方法
Strassen算法的核心思想是令递归数稍微不那么茂盛第二,即只递归进行7次而不是8次n/2 * n/2矩阵的乘法。Strassen算法不是那么直观,它包含4个步骤:
1.按公式将输入矩阵A、B和输入出矩阵C分解为n/2 * n/2的子矩阵。采用下标计算方法,此步骤花费Θ(1)时间。
2.创建10个n/2 * n/2的矩阵
3.用步骤1中创建的子矩阵和b步骤2中创建的10个矩阵,递归地计算7个矩阵积
4.通过
Strassen算法运行时间T(n)的递归式:
可以求出解为T(n) = Θ
4.3 用代入法求解递归式
代入法求解递归是分为两步:
1.猜测解的形式
2。用数学归纳法求出解中的常数,并证明解是正确的。
做出好的猜测
猜测解要靠经验,偶尔还需要创造力。如果要求解的递归式与曾经见过的递归式类似,那么猜测一个类似的解是合理的。另一种做出好的猜测的方法是先证明递归式较松的上界和下界,然后缩小不确定的范围。
微妙的细节
有时可能猜出了递归式的渐近界,但证明失败。,问题常常处在归纳假设不够强,无法证出准确的界。这时,将它减去一个低阶的项,可能会顺利进行。
避免陷阱
改变变量
有时一个小得代数运算可以将一个未知的递归式变成你所熟悉的形式。例如:
令m = lg n,得到
重命名
得到
4.4 用递归树方法求解递归式
在递归树中,每个节点表示一个单一子问题的代价,子问题对应某次递归函数的调用。我们将树中每层中的代价求和,得到每层代价,然后将所有层的代价求和,得到所有层次的递归调用的总代价。
递归树最适合用来生成好的猜测,然后即可用代入法来验证猜测是否正确。
4.5 用主方法求解递归式
主方法为如下形式的递归式提供了一种“菜谱”式的求解方法
T(n) = aT(n/b) + f(n)
其中,a>=1和b>1是常数,f(n)是渐近正函数。它描述的是这样一种算法的运行时间:它将规模为n的问题分解为a个子问题,每个子问题规模为n/b,其中a和b都是正常数。a个子问题递归地进行求解,每个花费时间T(n/b)。函数f(n)包含了问题分解和子问题合并的代价。
主定理
主方法依赖于下面的定理:
定理 4. 1(主定理) 令a>=1和b>1是常数,f(n)是一个函数,T(n)是定义在非负整数上的递归式:
T(n) = aT(n/b) + f(n)
其中,我们将n/b解释为
1. 若对某个常数ε>0有f(n)=O(
2. 若f(n)=Θ(
3. 若对某个常数ε>0有f(n)=Ω(
对于三种情况的每一种,我们将函数f(n)与函数
注意,这三种情况并未覆盖f(n)的所有可能性。情况1和情况2之间有一定的间隙,f(n)可能小于
使用主方法
使用主方法很简单,我们只需确定主定理的哪种情况成立,即可得到解。
例1:T(n)=9T(n/3)+n
a=9,b=3,f(n)=n.因此
例2:T(n)=T(2n/3)+1
a=1,b=3/2,f(n)=1.因此
例3:T(n)=3T(n\4)+nlg n
a=3,b=4,f(n)=nlg n,因此