算法--分治思想的運用
前言:上次算法課主要對分治思想進行了介紹,在這裏進行以下總結和幾個例子的應用。
一、分治算法
設計過程:(1)分解:將問題分解為子問題,子問題的形式與原問題是一樣的,只是規模減小了。
(2)求解:遞歸地求解出子問題。
(3)合並:將子問題的解組合成原問題的解。
分治算法中最重要的就是遞歸求解子問題,求解遞歸式估計出算法的復雜度一般可以有三種方式:
(1)代入法:猜測一個界,然後用數學歸納法證明這個界是正確的。
(2)遞歸樹法:將遞歸式轉為一棵樹,其結點表示不同層次的遞歸調用產生的代價。
(3)主方法:即Master定理,主要求解以下形式的遞歸式:T(n)=aT(n/b)+f(n);該方法有三種情形,只要記住這三種情形,可以很快速的求解出遞歸式:
【1】若對某個常數€>0有f(n)=O(nlogba-€)則T(n)=Θ(nlogba);
【2】若f(n)=Θ(nlogba),則T(n)=Θ(nlogbalgn);
【3】若f(n)=Θ(nlogba+€),則T(n)=Θ(f(n));
二、應用例子:
(1)設X[0:n-1]和Y[0:n-1]為兩個數組,每個數組中含有n個已排好序的數,設計一個算法復雜度為O(logn)的分治算法,找出X和Y中2n個數中的中位數。(中位數:個數為奇數:中間位置上的數;個數為偶數,中間兩個數的平均數)
思路:對於兩個已排好序的數組,可以尋找兩個數組中的中位數,只需要進行n次的比較,時間復雜度可以為O(n),代碼如下
int main(void)
{
int x[] = { 2, 4, 5, 11, 12, 13, 14 };
int y[] = { 1, 3, 6, 7, 8, 9, 10 };
int a1, a2, n1, n2, n;
n = n1 = n2 = 0;
while (n < 7)
{
a1 = x[n1] <= y[n2] ? x[n1++] : y[n2++];
n++;
}
a2 = x[n1] <= y[n2] ? x[n1++] : y[n2++];
printf("%lg\n", (double)(a1 + a2) / 2);
return 0;
}
使用分治法設計算法的思路如下:
1.分別取兩個數組的中位數:若n為奇數,則x=X[n/2],y=Y[n/2];若n為偶數,則x,y分別為兩個數組中間兩個數的平均數。並設兩個數組的左右邊界分別為:XLeft,XRight;YLeft,YRight.
2.比較兩個中位數的大小:
(1)若x<y:則舍去x之前的數和y之後的數,形成新的子數組:X[n/2:XRight],Y[YLeft:n/2]
(2)若x>y:則舍去x之後的數和y之前的數,形成新的子數組:X[XLeft:n/2],Y[n/2:YRight]
(3)若x=y:則該數即是中位數,直接輸出即可。
3.若是步驟2中的前兩種情形,則進行遞歸,每次都篩去一半的數,直至兩個子數組的長度為1,遞歸結束,輸出數組中兩個數的平均數。
復雜度分析:
T(n)=O(1) n=1;T(n)=T(n/2)+1 n>1;
使用Master定理,f(n)=nlog21 =1,所以T(n)=O(logn)。
(2)有一實數序列a1,a2,....an,若i<j且ai>aj,則(ai,aj)形成了一個逆序對,請使用分治算法求整個序列中逆序對個數,並分析算法時間復雜度。
思路:看到這個問題的第一個想法是建立兩層循環:
int i,j;
int count=0;
for(i=1;i<=n;i++)
{
for(j=i+1;j<=n;j++)
{
if (ai>aj)
count++;
}
}
這種算法的時間復雜度應該為O(n2)
分治算法設計思路:將該實數序列分解為兩個等大的子序列,則逆序對存在於左序列、右序列及左右序列之間,遞歸求解左序列、右序列中的逆序對,而左右序列之間的逆序對需要合並,若直接進行合並,則遍歷需要的時間復雜度為(n/2)*(n/2),復雜度並未得到改善,所以需要對合並進行優化,若對兩個排好序的左右序列進行合並,則不影響逆序對的結果,並且只需要遍歷n次,即合並的時間復雜度為O(n).
過程:
[1].分解:將原序列分為兩個等大的實數序列,即左序列,右序列
[2].求解:遞歸求解左序列、右序列中的逆序對
[3].合並:在合並的過程中排序並計算逆序對的個數。比如兩個排好序的數組{1,3,4,6}和{2,7,8,9}:1<2,左序列中往後移;3>2,則說明左序列中後面的數都大於2,所以有3個逆序對(3,2)(4,2)(6,2),右序列中指針向後移;3<7,左序列中往後移,4<7,往後移,6<7,往後移,至此已經沒有逆序對了。
仔細想這個過程和歸並排序非常像,只是在合並時計算逆序對的個數。
時間復雜度分析:
T(n)=2T(n/2)+O(1)+O(n);依次為求解子問題的代價,分解時的代價,及合並時的代價。利用Master定理:a=2,b=2,nlog22 =n,f(n)=n,所以T(n)=O(nlogn)
(3)求解天際線問題
給定n座建築物B[1,2,...,n],每個建築物B[i]表示為一個矩形,用三元組B[i]=(ai,bi,hi)表示,其中ai表示建築左下頂點,bi表示建築的右下頂點,hi表示建築的高,請設計一個O(nlogn)的算法求出這n座建築物的天際輪廓。例如,左下圖所示中8座建築的表示分別為(1,5,11),(2,7,6),(3,9,13),(12,16,7),(14,25,3),(19,22,18),(23,29,13)和(24,28,4),其中天際輪廓如右下圖所示可用9個高度的變化(1,11),(3,13),(9,0),(12,7),(16,3),(19,18),(22,3),(23,13)和(29,0)表示。另舉一個例子,假定只有一個建築物(1,5,11),其天際輪廓輸出為2個高度的變化(1,11),(5,0)。
【思路】:若只有一個建築,那麽輸出的點一定是建築的左上和右下頂點,根據這個思路,使用分治法解決。
算法描述:
- 取mid=n/2(n為建築的總數目)為分界線,將原有所有的建築劃分為左建築集合(0,mid)右建築集合(mid+1,n-1),將原問題一分為二進行遞歸
- 當建築數量為1時:返回兩個點:p1=左上頂點;p2=右下頂點
- 合並:
(1)定義三個變量hl,hr,h,全部初始化為0,分別記錄左集合、右集合及總體高度的變化值;
(2)遍歷左建築點L和右建築點R:選取橫坐標較小的點,若該點在左集合中,則hl為該點的縱坐標,比較hl與hr,選取二者的較大值替換該點的縱坐標,再與h進行比較,若縱坐標與h相等,則舍棄該點;若縱坐標與h不等,則保留該點,且當縱坐標大於h時,將h替換為縱坐標的值,h始終為高度的較大值。
(3)當左集合或右集合遍歷完畢時,另一方剩余的點直接取到。
可以用一個例子模擬該過程進棧出棧的過程:
建築群集合為(1,5,11)(2,7,6):
- 經過分解,取到每個建築的左上頂點和右下頂點:(1,11)(5,0);(2,6)(7,0);
- 進行合並:
hl=0,hr=0,h=0;
①left[0].x=1<right[0].x=2;(1,11)進棧。此時hl=11,hr=0,h=max(hl,hr)=11;保留該點;左集合中右移left[1]。
②left[1].x=5>right[0].x=2;(2,6)暫時進棧;此時hr=6,hl=11,h=11;則將right[1].y=max(hl,hr)=11;此時right[1].y=h:舍棄該點,(2,11)出棧;右集合右移,right[1]
③left[1].x=5<right[1].x=7;(5,0)暫時進棧;此時hr=0,hl=11,h=11;則left[1].y=max(hl,hr)=11;此時left[1].y=h:舍棄該點,(5,11)出棧。左集合右移
④左集合遍歷完畢,直接將右集合中剩余的點入棧
(4)POJ3714:在與聯邦的戰鬥接連失敗後,帝國撤退到最後的據點。帝國依靠其強大的防禦系統,擊退了六次聯合進攻。在經歷了幾個不眠之夜後,阿瑟將軍註意到防禦系統的唯一弱點是它的能源供應。該系統由N個核電站負責,並拆除其中任何一個將使該系統失效。這位將軍很快就開始了對這些電臺的突襲,這些特工被帶進了要塞。不幸的是,由於帝國空軍的襲擊,他們未能到達預期的位置。作為一名經驗豐富的將軍,亞瑟很快意識到他需要重新安排這個計劃。他現在想知道的第一件事是,哪個代理是離任何發電站最近的。你能不能幫助將軍計算一個代理和一個站之間的最小距離?
【思路】:此題是對最近點對問題的一個應用,最近點對問題中最後的解可能出現在三種情形中:左問題集合;右問題集合;一個點在左集合,一個點在右集合中。而POJ3714的這道題目相當於對最後的解進行了限制:必須是兩個集合中的點,所以我們只需在求最近點對的問題上加上一個限制:判斷最後求得的兩個點是否處於兩個集合中,判斷時可使用“標誌位”。具體的算法可以使用兩種方法:
【1】只進行了x軸預排序
f(Q,i,j)//Q已經按x軸進行了排序
{
if(j-i<=c)
{
直接計算距離,比較得到最小距離;
return;
}
di=f(Q,i,(i+j)/2-1);
dj=f(Q,(i+j)/2,j);
d=min{di,dj};
A:臨界區,存入中位線範圍d的所有點;
對A中的所有點按y軸排序;//該過程每次調用都對y進行排序,時間復雜度較大
計算A中點間的距離;
}
【2】空間換時間,對集合中的點進行X,Y預排序
算法描述如下:
(1)輸入點集合sa[2n]:為station[n]集合中的點加上標誌flag=ture,為agent[n]中的點加上標誌flag=false。
(2)預排序:對sa集合中的點按X軸從小到大排序;按y軸從小到大排序並記錄下標值。(通過預排序用空間換時間,降低算法的時間復雜度)
(3)遞歸調用求解最近點對的方法
- 集合中只有一個點,不進行計算
- 集合中有兩個點,判斷兩個點是否屬於同一集合:是--計算距離;否--不進行計算。
- 集合中多於兩個點,按x軸排序計算中位數然後將原集合分解為兩個規模基本一致的子集合,遞歸調用。
對於臨界區中的點,因為已經進行了y軸預排序,所以直接進行掃描,對每一個點計算按y軸排序後面的6個點(鴿巢原理)與它的距離,並與兩個子集合中的最小距離進行比較。
算法--分治思想的運用