筆記
本節給出了分治法的一個例子。給定一個數組A[1..n],找出一個元素和爲最大的連續子數組A[i..j],其中1≤i≤j≤n,稱這樣的子數組爲最大子數組。例如,下圖所示數組中,第8個元素到第11個元素之間的子數組爲最大子數組。
求解最大子數組問題,最簡單的方法是暴力檢查所有的子數組,從中找出和爲最大的子數組。對於一個有n個元素的數組,一共有Cn2+Cn1=n(n−1)/2+n=Θ(n2)個子數組。參考練習4.1-2可知,計算每個子數組的和只需要O(1)時間。因此暴力求解法的所花費的時間爲Θ(n2)。
除了暴力求解法,最大子數組問題還可以使用分治法求解,並且分治法具有更優的時間複雜度。假定要尋找子數組A[low..high]的最大子數組,我們從中央位置mid=⌊(low+high)/2⌋將A[low..high]劃分爲兩個子數組,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。
• 跨越了中央位置mid,即low≤i≤mid<j≤high。
根據以上分析,可以遞歸求解最大子數組問題。對於一個子數組A[low..high],首先尋找跨越中央位置的最大子數組,然後分別遞歸求解A[low..mid]和A[mid+1..high]的最大子數組,比較這三種情況的最大子數組,從中選出元素和最大者作爲A[low..high]的最大子數組。
分治法的關鍵在於尋找跨越中央位置的最大子數組。對於一個子數組A[low..high],任何跨越中央位置mid的子數組必然都由兩個子數組A[i..mid]和A[mid+1..j]組成,其中low≤i≤mid並且mid<j≤high。因此,我們只需要找出形如A[i..mid]和A[mid+1..j]的最大子數組,然後將二者合併即可。下面給出尋找跨越中央位置的最大子數組的僞代碼。
接下來給出分治法求解最大數組問題的僞代碼。
要尋找數組A[1..n]的最大子數組,只需要調用FIND-MAXIMUM -SUBARRAY(A,1,n)即可。
下面分析分治法求解最大子數組問題的時間複雜度。對於長度爲n的數組,求解最大子數組的時間用T(n)表示。T(n)由三部分組成:
• 遞歸求解子數組A[1..mid]的最大子數組的時間T(n/2);
• 遞歸求解子數組A[mid+1..n]的最大子數組的時間T(n/2);
• 求解跨越中央位置的最大子數組的時間,這一時間爲Θ(n)。
所以有遞歸式T(n)=2T(n/2)+Θ(n)。求解這個遞歸式,得到T(n)=Θ(nlgn)。
練習
4.1-1 當A的所有元素均爲負數時,FIND-MAXIMUM-SUBARRAY返回什麼?
解
返回數值最大的那個負數,即絕對值最小的負數。
4.1-2 對最大子數組問題,編寫暴力求解方法的僞代碼,其運行時間應該爲Θ(n2)。
解
4.1-3 當你的計算機上實現最大子數組問題的暴力算法和遞歸算法。請指出多大的問題規模n0是性能交叉點——從此之後遞歸算法將擊敗暴力算法?然後,修改遞歸算法的基本情況——當問題規模小於n0時採用暴力算法。修改後,性能交叉點會改變嗎?
略
4.1-4 假定修改最大子數組問題的定義,允許結果爲空子數組,其和爲0。你應該如何修改現有算法,使它們能允許空子數組爲最終結果?
解
先對整個數組遍歷一遍,檢查是否所有元素都爲負數。如果所有元素都爲負數,則算法輸出空子數組。如果數組中存在正數,則調用FIND-MAXIMUM –SUBARRAY求解。
4.1-5 使用如下思想爲最大子數組問題設計一個非遞歸的、線性時間的算法。從數組的左邊界開始,由左至右處理,記錄到目前爲止已經處理過的最大子數組。若已知A[1..j]的最大子數組,基於如下性質將解擴展爲A[1..j+1]的最大子數組:A[1..j+1]的最大子數組要麼是A[1..j]的最大子數組,要麼是某個形如A[i..j+1]的最大子數組(1≤i≤j+1)。在已知形如A[i..j]的最大子數組的情況下,可以在常數時間內找出形如A[i..j+1]的最大子數組。
解
與分治法不同,這是典型的增量法。本題的關鍵在於:在已知以A[j]結尾的最大子數組的情況下,找出以A[j+1]結尾的最大子數組。假設以A[j]結尾的最大數組爲A[i..j](1≤i≤j)。分兩種情況:
(1) 如果A[i..j]各元素之和sum{A[i..j]}>0,那麼以A[j+1]結尾的最大數組爲A[i..j+1]。這一點可以用反證法來說明。假設以A[j+1]結尾的最大數組爲A[k..j+1],其中1≤k≤j+1並且k=i。又分兩種情況討論。
1) 1≤k≤j:由於以A[j]結尾的最大子數組爲A[i..j],所以sum{A[k..j]}≤sum{A[i..j]},從而有sum{A[k..j+1]}≤sum{A[i..j+1]}。如果sum{A[k..j+1]}<sum{A[i..j+1]},那麼A[k..j+1]肯定不是以A[j+1]結尾的最大數組,這與假設矛盾。如果sum{A[k..j+1]}=sum{A[i..j+1]},那麼如果假設成立,即A[k..j+1]是以A[j+1]結尾的最大數組,那麼A[i..j+1]也同樣是以A[j+1]結尾的最大數組。
2) k=j:此時假設的以A[j+1]結尾的最大數組爲A[j+1]本身。由於sum{A[i..j]}>0,所以sum{A[i..j+1]}>A[j+1]。這說明A[j+1]本身肯定也不是以A[j+1]結尾的最大數組,這與假設矛盾。
(2) 如果A[i..j]各元素之和sum{A[i..j]}≤0,那麼A[j+1]結尾的最大數組爲A[j+1]本身。這一點同樣可以用反證法來說明,這裏就不贅述。
下面給出該算法的僞代碼。
對於一個包含n個元素的數組,該算法一共包含n次迭代,每次迭代花費Θ(1)時間。因此,該算法的運行時間爲Θ(n)。
本節代碼鏈接:
https://github.com/yangtzhou2012/Introduction_to_Algorithms_3rd/tree/master/Chapter04/Section_4.1