當我們求解某些問題時,由於這些問題要處理的數據相當多,或求解過程相當復雜,使得直接求解法在時間上相當長,或者根本無法直接求出。對於這類問題,我們往往先把它分解成幾個子問題,找到求出這幾個子問題的解法后,再找到合適的方法,把它們組合成求整個問題的解法。如果這些子問題還較大,難以解決,可以再把它們分成幾個更小的子問題,以此類推,直至可以直接求出解為止。這就是分治策略的基本思想。
1、引例:
如果給你一個裝有16枚硬幣的袋子,其中有一枚是偽造的,並且那枚偽造硬幣的重量和真硬幣的重量不同。你能不能用最少的比較次數找出這個偽造的硬幣?為了幫助你完成這一任務,將提供一台可用來比較兩組硬幣重量的儀器,利用這台儀器,可以知道兩組硬幣的重量是否相同。
常規的解決方法是先將這些硬幣分成兩枚一組,每一次只稱一組硬幣,如果運氣好的話只要稱1次就可以找到,最壞最多稱8次才可以找出那枚硬幣,這種直接尋找的方法存在着相當大的投機性,適用於硬幣數量少的情況,在硬幣數量多的情況下就成為一件費時費力又需要運氣的事。
試着改變一下方法:如果我們將全部硬幣分成兩組,將原來設計的一次比較兩枚硬幣變為一次比較兩組硬幣,我們會發現通過一次比較后.完全可以舍棄全部是真幣的一組硬幣,選取與原有問題一致的另一半進行下一步的比較,這樣問題的規模就明顯縮小,而且每一次比較的規模都是成倍減少(如圖4-1所示)。
根據以上分析。我們可以得到以下的結論:
(I)參與比較的硬幣數量越多,使用該方法來實現就越快.而且投機性大大減少;
(2)解決方法關鍵在於能將大問題分割成若干小問題;
(3)小問題與原有問題是完全類似的。
通常我們將這種大化小的設計策略稱之為分治法.即“分而治之”的意思。
2、分治法的基本思想和解題的一般步驟:
分治算法的基本思想是將一個規模為N的問題分解為K個規模較小的子問題,這些子問題相互獨立且與原問題性質相同。求出子問題的解,就可得到原問題的解。側重點在於能各個擊破。分治法在設計檢索、分類算法或某些速算算法中很有效。最常用的分治法是二分法、歸並法、快速分類法等。
分治法解題的一般步驟:
(1)分解,將要解決的問題划分成若干規模較小的同類問題;
(2)求解,當子問題划分得足夠小時,用較簡單的方法解決;
(3)合並,按原問題的要求,將子問題的解逐層合並構成原問題的解。
1、解決算法實現的同時,需要估算算法實現所需時間。分治算法時間是這樣確定的: 解決子問題所需的工作總量(由 子問題的個數、解決每個子問題的工作量 決定);合並所有子問題所需的工作量
2、分治法是把任意大小問題盡可能地等分成兩個子問題的遞歸算法
3、分治的具體過程大致如下:
begin {分治過程開始}
if ①問題不可分 then ②返回問題解
else begin
③從原問題中划出含一半運算對象的子問題1;
④遞歸調用分治法過程,求出解1;
⑤從原問題中划出含另一半運算對象的子問題2;
⑥遞歸調用分治法過程,求出解2;
⑦將解1、解2組合成整修問題的解;
end;
end; {結束}
3、典型例題:
【例1】用遞歸算法和非遞歸算法實現二分查找即:有 n個已經從小到大排序好的數據,從鍵盤輸入一個數m,用二分查找方法,判斷它是否在這n 個數中。

1 var a:array[1..20]of integer; 2 n,i,m,x,y:integer; 3 procedure jc(x,y:integer); //遞歸過程 4 var k:integer; 5 begin 6 k:=(x+y)div 2; //取中間位置點 7 if a[k]=m then writeln('the num in ',k); //找到查找的數,輸出結果 8 if x>y then writeln('no find') //找不到該數 9 else begin 10 if a[k]<m then jc(k+1,y); //在后半中查找 11 if a[k]>m then jc(x,k-1); //在前半中查找 12 end; 13 end; 14 begin 15 readln(n); 16 x:=1;y:=n; 17 for i:=1 to n do readln(a[i]); //輸入排序好的數 18 readln(m); //輸入要查找的數 19 jc(x,y); //遞歸查找 20 end.

1 var a:array[1..20]of integer; 2 n,i,m,x,y,k:integer; 3 begin 4 readln(n); 5 x:=1;y:=n; 6 for i:=1 to n do readln(a[i]); 7 readln(m); 8 repeat 9 k:=(x+y)div 2; 10 if a[k]=m then begin writeln('the num in ',k);halt;end 11 else begin 12 if a[k]<m then x:=k+1; 13 if a[k]>m then y:=k-1; 14 end; 15 until x>y; 16 writeln('No find'); 17 end.
【例2】一元三次方程求解
有形如:ax3 +bx2 +cx+d=0這樣的一個一元三次方程。給出該方程中各項的系數(a,b,c,d 均為實數),並約定該方程存在三個不同實根(根的范圍在-100 至 100 之間),且根與根之差的絕對值≥1。 要求由小到大依次在同一行輸出這三個實根(根與根之間留有空格),並精確到小數點后 2 位。
提示:記方程 f(x)=0,若存在 2 個數 x1 和 x2,且 x1<x2,f(x1)*f(x2)<0,
則在(x1,x2)之間一定有一個根。
輸入:
a,b,c,d
輸出:
三個實根(根與根之間留有空格)
輸入輸出樣例
輸入: 1 -5 -4 20
輸出: -2.00 2.00 5.00
【例3】循環比賽
【問題描述】 設有N個選手的循環比賽。其中N=2M,要求每名選手要與其他N-1名選手都賽一次。每名選手每天比賽一次,循環賽共進行N-1天.要求每天沒有選手輪空。
輸入:M
輸出:表格形式的比賽安排表
【樣例輸入】
3
【樣例輸出】
1 2 3 4 5 6 7 8
2 1 4 3 6 5 8 7
3 4 1 2 7 8 5 6
4 3 2 1 8 7 6 5
5 6 7 8 1 2 3 4
6 5 8 7 1 1 4 3
7 8 5 6 3 1 1 2
8 7 6 5 d 3 2 1
【問題分析】:此題很難直接給出結果,我們先將問題進行分解,n=2^m,將規模減半,如果m=3(即n=8),8名選手的比賽,減半后變成4名選手的比賽(n=4),4個選手的比賽的安排方式還不是很明顯,再減半到兩名選手隊的比賽(n=2),兩名選手的比賽安排方式很簡單,只要讓兩名選手直接進行一場比賽即可:
1 |
2 |
2 |
1 |
分析兩個球隊的比賽的情況不難發現,這是一個對稱的方陣,我們把這個方陣分成4部分(即左上,右上,左下,右下),右上部分可由左上部分加1(即加n/2)得到,而右上與左下部分、左上與右下部分別相等。因此我們也可以把這個方陣看作是由n=1的方陣所成生的,同理可得n=4的方陣:
1 |
2 |
3 |
4 |
2 |
1 |
4 |
3 |
3 |
4 |
1 |
2 |
4 |
3 |
2 |
1 |
同理可由n=4方陣生成n=8的方陣:
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
2 |
1 |
4 |
3 |
6 |
5 |
8 |
7 |
3 |
4 |
1 |
2 |
7 |
8 |
5 |
6 |
4 |
3 |
2 |
1 |
8 |
7 |
6 |
5 |
5 |
6 |
7 |
8 |
1 |
2 |
3 |
4 |
6 |
5 |
8 |
7 |
2 |
1 |
4 |
3 |
7 |
8 |
5 |
6 |
3 |
4 |
1 |
2 |
8 |
7 |
6 |
5 |
4 |
3 |
2 |
1 |
這樣就構成了整個比賽的安排表。
在設計程序時,我們采用由小到大的方法進行擴展,而數組下標的處理是解決該問題的關鍵。用數組a記錄2^m名選手的循環比賽表,整個循環比賽表從最初的1*1方陣按上述規則生成2*2的方陣,再生成4*4的方陣,……直到生成出整個循環比賽表為止。變量h表示當前方陣的大小,也就是要生成的下一個方陣的一半。
【參考程序】
1 program p4_1; 2 var
3 i,j,h,m,n:integer; 4 a:array[1..100,1..100]of integer; 5 begin
6 assign(input,'word.in'); reset(input); 7 assign(output,'word.out'); rewrite(output); 8 readln(m); 9 n:=1;a[1,1]:=1;h:=1; 10 for i:=1 to m do n:=n*2; 11 repeat
12 for i:=1 to h do
13 for j:=1 to h do begin
14 a[i,j+h]:=a[i,j]+h;{構造右上角方陣}
15 a[i+h,j]:=a[i,j+h];{構造左下角方陣}
16 a[i+h,j+h]:=a[i,j];{構造右下角方陣}
17 end; 18 h:=h*2; 19 until h=n; 20 for i:=1 to n do
21 begin
22 for j:=1 to n do write(a[i,j]:4); writeln; 23 end; 24 close(input); close(output); 25 end.
[例4] 求方程的根
【問題描述】
輸入m,n,p,a,b,求方程f(x)=mx+nx-px=0在[a,b]內的根。m,n,p,a,b均為整數,且a<b;m,n,p都大於等於1。如果有根,則輸出,精確到1E-11;如果無方程根,則輸出“NO”。
【樣例】
equation.in
2 3 4 1 2
equation.out
1.5071265916E+00
2.9103830457E-11
【算法分析】
首先這是一個單調遞增函數,對於一個單調遞增(或遞減)函數,如圖4-7所示,判斷在[a, b]范圍內是否有解,解是多少。方法有多種,常用的一種方法叫“迭代法”,也就是“二分法”。先判斷f(a)·f(b)≤0,如果滿足則說明在[a, b]范圍內有解,否則無解。如果有解再判斷x=(a+b)/2是不是解,如果是則輸出解結束程序,否則我們采用二分法,將范圍縮小到[a, x)或(x, b],究竟在哪一半區間里有解,則要看是f(a)·f(x)<0還是f(x)·f(b)<0。
當然對於yx,我們需要用換底公式把它換成exp(x*ln(y))。
4、小結:
分治法所能解決的問題一般具有以下幾個特征:
- 該問題的規模縮小到一定的程度就可以容易地解決;
- 該問題可以分解為若干個規模較小的相同問題,即該問題具有子結構性質
- 利用該問題分解出的子問題的解可以合並為該問題的解;
- 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。
第4條特征涉及到分治法的效率,如果各子問題是不獨立的,則分治法要做許多不必要的工作,重復地解公共的子問題,此時雖然也可用分治法,但一般用動態規划較好。