一、分治策略簡介
在很多情況下,分治策略有可能是簡單而強有力的方法,可以用來提高算法的效率
不要問我爲什麼,他就是這樣的……
大概的步驟有三部分
1,將輸入的數據分成若干部分
2、遞歸的求解每個部分的問題
3、將子問題的解組合成爲一個全局的解
二、應用領域
分治算法可以被運用到許許多多的方面,可以充當其他算法的一個i組成部分,也可以稍做修改成爲另一個算法,例如
歸併排序
找平面上最鄰近的點對問題
降低整數相乘的複雜度
消除信號的噪音
……
三、算法核心部分分析
馬克思和恩格斯教導我們一定要抓住主要矛盾的主要方面
從我的角度看,我們學習分治算法的主要矛盾是將其應用的其他方面
而主要方面就是一定要理解算法的核心部分
這樣就可以根據實際情況對分治算法做出適當的修改,提高解決問題的效率
接下來我將會通過解決一下幾個問題,讓大家比較簡單的掌握分治算法
上面以及提到過分治算法的大概過程
我再重述一下
-
將輸入的數據分成若干部分
-
遞歸的求解每個部分的問題
-
將子問題的解組合成爲一個全局的解
爲了讓大家更好的理解這個過程就拿最爲熟悉的歸併排序算法舉例
嘛也不說直接上源代碼
python實現
def mergeSort(arr):
if(len(arr)<2):
return arr
middle = math.floor(len(arr)/2)
left, right = arr[0:middle], arr[middle:]
return merge(mergeSort(left), mergeSort(right))
def merge(left,right):
result = []
while left and right:
if left[0] <= right[0]:
result.append(left.pop(0))
else:
result.append(right.pop(0))
while left:
result.append(left.pop(0))
while right:
result.append(right.pop(0))
return result
if __name__ == '__main__':
list = [1, 5, 8, 45, 98, 90, 78, 99, 123, 2]
print("List is:", list)
result = mergeSort(list)
print("list sorted :", result)
相信很多人又這樣的問題,爲啥這玩意要在middle處切割的,又爲啥要分成兩個部分,不分成三個,四個……部分呢?這些問題就是我們下面要詳細討論的了
“小朋友,你是否有很多問好???”
說實話,一開始學的時候我也一臉懵逼━┳━ ━┳━
要想弄懂這個問題就我們就需要知道下面這個公式,並且需要會進行對應的推導
四、公式推導
首先定義T(n)爲該算法在規模爲n的輸入實例的最壞的運行時間
然後看
T(n)={aT(n/b)+g(n)(n>1)0 (n=1)
這個公式
這個公式就是歸併排序的遞推公式,下面的很多問題就是圍繞這個問題展開的
公式的含義
-
一個規模爲n的問題每一次被分成了a個子問題,
-
每個問題的規模是n/b
-
各個子問題的解的複雜度爲 g(n)
接下來的這個圖你們一定就可以看懂了
這個圖就是上面歸併排序的分治策略圖
n爲2的倍數
接下來我們就手動推導出歸併排序的複雜度爲T(n)=nlog2n
首先根據
T(n)=aT(bn)+g(n)
我們可以寫出
歸併排序中每一次被分爲2個子問題,每一個子問題的規模是其爸爸的一半,因爲歸併排序進行排序操作的時候只比較頭兩個元素的大小,當一半爲空時,直接把另一半的其他部分加到結果上就行,所以每一個子問題的複雜度爲n
首先從簡單的n爲2的倍數開始
T(n)=2T(2n)+n
nT(n)=n2T(2n)+1
=2nT(2n)+1
=4nT(4n)+1+1
=(8n)T(8n)+1+1+1
……
=(nn)T(nn)+log2n∗1
=log2n
T(n)=nlog2n
n爲自然數
其次對於一般情況我們要證明
T(n)≤n ceiling(log2n)
遞推公式爲
T(n)<= {0 (n=1)T(ceiling(n/2))+T(floor(n/2))+n (n>1)
我們採用歸納法
當n= 1時顯然成立
令 n1=floor(2n) n2=ceiling(2n)
假設n−1命題時命題成立
T(n)<=T(n1)+T(n2)+n <=n1(ceiling(log2n1))+n2(ceiling(log2 n2))+n <=n1(ceiling(log2 n2))+n2(ceiling(log2 n2))+n =n(ceiling(log2 n2))+n <=n(ceiling(log2 ceiling(n/2)))+n <=n(ceiling(log2 n)−1)+n <=n(ceiling(log2 n))
綜上,我們回答剛剛的問題爲啥這玩意要在middle處切割的,又爲啥要分成兩個部分,不分成三個,四個…部分呢?
其實可以在不同地方切割,我們一般選擇在middle出切割,也可選擇切成多個部分,但是這樣的複雜度時不同的
g(n)=c
對於g(n)=c
這個公式最後可以證明
T(n)=O(nlogba)
g(n) = c*n^d
對於g(n)=cnd
滿足如下定理
設函數滿足這個遞推關係T(n)=aT(bn)+cnd的增函數,其中n=bk,a>=1,b是大於1的正整數,c,d爲實數且c>=0,d>=0,那麼
T(n)=⎩⎨⎧O(nd) a<bdO(ndlog n) a=bdO(nloga b) a>bd
有興趣的可以自己證明一下,這裏就不給出證明了
敲公式實在太累人了ε(┬┬﹏┬┬)3
五、分治算法的應用
經過對核心算法的理解
我們就可以解決之前提出的問題了
分治算法第一步就是就是將問題分成對應的a部分,每個問題的規模是n/b之後遞歸求解每一個問題,最後進行組合
那怎麼對一個分治算法的複雜度進行評估呢?
我們可以用上面T(n)公式進行推導,將對應的a,b,g(n),帶入然後計算出T(n)
同時我們可以運用不同的計算方法改變a,b,g(n)的值,由此來達到改進算法效率的目的
接下來我們就通過改進整數的乘法來看一下分治算法的牛逼之處吧
最普通的乘法少說一點吧
x*y,在計算機中是通過二進制來計算的,所以是先轉爲二進制再相乘
複雜度爲O(n^2)
接下來我們通過分治算法來進行改進
我們令
x=2(n/2)∗x1+x0
y=2(n/2)∗y1+y0
x∗y=(2(n/2)∗x1+x0)(2(n/2)∗y1+y0)
=2n∗x1∗y1+2(n/2)∗(x1∗y0+x0∗y1)+x0∗y0
我們可以通過遞推公式來看一下,現在的算法優劣情況
T(n)=4T(n/2)+O(n)
=>T(n)=O(n2)
O(n) 爲每次移位和相加的複雜度
貌似沒有啥改變……
接下來看看我們可不可以通過改變a,b,O(n)的值來降低複雜度
可以看出O(n),沒有啥可以很好的改進的地方
但是我們可以發現
x∗y=2n∗x1∗y1+2(n/2)∗(x1∗y0+x0∗y1)+x0∗y0
=2n∗x1∗y1+2(n/2)∗((x1+x0)∗(y1+y0)−x1∗y1−x0∗y0)+x0∗y0
這樣就成將4次乘法降低爲3次了
讓我們再來看一次複雜度
T(n)=3T(n/2)+O(n)=>T(n)=O(n(1.585))
d=====( ̄▽ ̄*)b
讓我們最後再看一下改進後的整數相乘的僞代碼
def Recursive_Multiply(x,y):
x = x1 * 2**(n/2) + x0
y = y1 * 2**(n/2) + y0
p = Recursive_Multiply(x1+x0,y1+y0)
x1 * y1 = Recursive_Multiply(x1,y1)
x0 * y0 = Recursive_Multiply(x0,y0)
return x1 * y1 * 2**n + (p-x1 * y1 - x0 * y0) * 2**(n/2) + x0 * y0
小編是個菜雞,若有錯誤請大佬們不吝指正