在分治策略中,我們遞歸地求解一個問題,在每層遞歸中應用如下三個步驟:
分解:將問題劃分爲一些子問題,子問題的形式與原問題一樣,只是規模更小。
解決遞歸地求解出子問題。。如果子問題規模足夠小,則停止遞歸,直接求解。
合併 將子問題的解組合成原問題的接。
當子問題足夠大,需要遞歸求解時,我們稱之爲遞歸情況。當子問題變得足夠小,不再需要遞歸時,我們說遞歸已經“觸底”,進入了基本情況。
在本章中,我們將看到更多基於分治策略的算法。第一個算法求解最大子數組問題,其輸入是一個數值數組,算法需要確定具有最大和的連續子數組。然後我們將看到兩個求解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,因此