1. 分治法
分治法的基本思想是將一個規模爲n的問題分解爲k個規模較小的子問題,這些子問題相互獨立且與原問題相同。遞歸的解這些子問題,然後將各子問題的解合併得到原問題的解。
分治模式在每一層遞歸上都有三個步驟:分解(Divide);求解(Conquer);合併(Combine)
- Divide:將一個難以直接解決的大問題,分割成一些規模較小的子問題,這些子問題互相獨立,且與原問題相同。
- Conquer:遞歸求解子問題,若問題足夠小則直接求解。
- Combine:將各子問題的解合併得到原問題的解
分治法所能解決的問題一般具有以下四個特徵:
- 該問題的規模縮小到一定的程度就可以容易地解決
- 該問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質
- 利用該問題分解出的子問題的解可以合併爲該問題的解
- 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。
divide-and-conquer(P) {
if ( | P | <= n0) solve(P); // 解決小規模的問題
divide P into smaller subinstances P1,P2,...,Pk; // 分解
for ( i=1, i<=k, i ++)
yi=divide-and-conquer(Pi); // 遞歸地解各子問題
return merge(y1,...,yk); // 合併子問題的解爲原問題的解
}
經驗:實踐表明,在用分治法設計算法時,最好使子問題的規模大致相同,即:將一個問題分成大小相等的k個子問題。 這種使子問題規模大致相等的做法是出自一種平衡(balancing)子問題的思想,它幾乎總是比子問題規模不等的做法要好。
2. 示例: 二分搜索
問題描述:給定已按升序排好序的n個元素a[0:n-1],問題:在這n個元素中找出一特定元素x。
分析1:該問題的規模縮小到一定的程度就可以容易地解決:
- 如果n=1,則通過一次比較就可以解決問題
分析2:該問題可以分解爲若干個規模較小的相同問題
- 取中間元素a[mid]對序列進行劃分
- 在a[mid]前面或後面查找x,其方法都和在a中查找x一樣
分析3:分解出的子問題的解可以合併爲原問題的解
分析4:分解出的各個子問題是相互獨立的
- 在a[i]的前面或後面查找x是獨立的子問題
int BinarySearch(int a[], int x, int left, int right) {
while (right >= left){
int mid = (left+right)/2;
if (x == a[m]) return mid;
if (x < a[m]) right = mid-1;
else left = mid+1;
}
return -1;
}
算法複雜度分析:每執行一次算法的while循環, 待搜索數組的大小減少一半。因此,在最壞情況下,while循環被執行了O(logn) 次。循環體內運算需要O(1) 時間,因此整個算法在最壞情況下的計算時間複雜性爲O(logn) 。
3. 示例: 棋盤覆蓋問題
問題描述:在一個
- 要求用圖示的4種L形態骨牌覆蓋給定的特殊棋盤
- 限制條件:覆蓋給定特殊棋盤上除特殊方格以外的所有方格
- 限制條件:任何2個L型骨牌不得重疊覆蓋
思路:
當
k>0 時,將2k×2k 棋盤分割爲4個2k−1×2k−1 子棋盤- 特殊方格必位於4個較小的子棋盤其中之一
- 其餘3個子棋盤中無特殊方格
爲了將這3個無特殊方格的子棋盤轉化爲特殊棋盤,可以用一個L型骨牌覆蓋這3個較小棋盤的會合處,從而將原問題轉化爲4個較小規模的棋盤覆蓋問題
- 遞歸地使用這種分割,直至棋盤簡化爲棋盤1×1
//2d6 棋盤覆蓋問題
#include "stdafx.h"
#include <iostream>
using namespace std;
int tile = 1;//全局變量 骨牌編號
int Board[4][4];//棋盤
void ChessBoard(int tr,int tc,int dr,int dc,int size);
int main()
{
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
Board[i][j] = 0;
}
}
ChessBoard(0,0,2,3,4);
for(int i=0; i<4; i++)
{
for(int j=0; j<4; j++)
{
cout<<Board[i][j]<<" ";
}
cout<<endl;
}
}
/**
* tr : 棋盤左上角的行號,tc棋盤左上角的列號
* dr : 特殊方格左上角的行號,dc特殊方格左上角的列號
* size :size = 2^k 棋盤規格爲2^k*2^k
*/
void ChessBoard(int tr,int tc,int dr,int dc,int size)
{
if(size == 1)
{
return;
}
int t = tile++;//L型骨牌編號
int s = size/2;//分割棋盤
//覆蓋左上角子棋盤
if(dr<tr+s && dc<tc+s)//特殊方格在此棋盤中
{
ChessBoard(tr,tc,dr,dc,s);
}
else//特殊方格不在此棋盤中
{
//用編號爲t的骨牌覆蓋右下角
Board[tr+s-1][tc+s-1] = t;
//覆蓋其餘方格
ChessBoard(tr,tc,tr+s-1,tc+s-1,s);
}
//覆蓋右上角子棋盤
if(dr<tr+s && dc>=tc+s)//特殊方格在此棋盤中
{
ChessBoard(tr,tc+s,dr,dc,s);
}
else//特殊方格不在此棋盤中
{
//用編號爲t的骨牌覆蓋左下角
Board[tr+s-1][tc+s] = t;
//覆蓋其餘方格
ChessBoard(tr,tc+s,tr+s-1,tc+s,s);
}
//覆蓋左下角子棋盤
if(dr>=tr+s && dc<tc+s)//特殊方格在此棋盤中
{
ChessBoard(tr+s,tc,dr,dc,s);
}
else//特殊方格不在此棋盤中
{
//用編號爲t的骨牌覆蓋右上角
Board[tr+s][tc+s-1] = t;
//覆蓋其餘方格
ChessBoard(tr+s,tc,tr+s,tc+s-1,s);
}
//覆蓋右下角子棋盤
if(dr>=tr+s && dc>=tc+s)//特殊方格在此棋盤中
{
ChessBoard(tr+s,tc+s,dr,dc,s);
}
else//特殊方格不在此棋盤中
{
//用編號爲t的骨牌覆蓋左上角
Board[tr+s][tc+s] = t;
//覆蓋其餘方格
ChessBoard(tr+s,tc+s,tr+s,tc+s,s);
}
}
4. 示例: 快速排序
算法基本思想:
- 在數組中確定一個記錄(的關鍵字)作爲“劃分元”
- 將數組中關鍵字小於劃分元的記錄均移動至該記錄之前
- 由此:一趟排序之後,序列
R[s...t] 將分割成兩部分
R[s...i−1] 和R[i+1...t] - 且滿足:
R[s...i−1]≤R[i]≤R[i+1...t] - 其中:
R[i] 爲選定的“劃分元”
對各部分重複上述過程,直到每一部分僅剩一個記錄爲止
- 首先對無序的記錄序列進行一次劃分
- 之後分別對分割所得兩個子序列“遞歸”進行快速排序
快速排序算法特點:
時間複雜度
- 最好情況:
T(n)=O(nlog2n) (每次總是選到中間值作劃分元) - 最壞情況:
T(n)=O(n²) (每次總是選到最小或最大元素作劃分元) - 快速排序算法的平均時間複雜度爲:
O(nlog2n)
- 最好情況:
快速排序算法是不穩定的
- 例如待排序序列: 49 49 38 65
- 快速排序結果爲: 38 49 49 65
算法性能與序列中關鍵字的排列順序和劃分元的選取有關
- 當初始序列按關鍵字有序(正序或逆序)時,快速排序蛻化爲冒泡排序,此時算法性能最差:時間複雜度爲O(n²)
- 可以用“三者取中”法來選取劃分元
- 也可採用隨機選取劃分元的方式
5. 示例: 最接近點對問題
問題描述:給定平面上的n個點,找出其中的一對點,使得在n個點組成的所有點對中,該點對的距離最小
求解最接近點對方法:
- 直觀解法
- 將每一個點與其他
n−1 個點的距離算出,找出最小距離 - 時間複雜度:
T(n)=n(n−1)/2+n=O(n2)
- 將每一個點與其他
- 分治法
- 分解:將n個點的集合分成大小近似相等的兩個子集
- 求解:遞歸地求解兩個子集內部的最接近點對
- 合併(關鍵問題):從子空間內部最接近點對,和兩個子空間之間的最接近點對中,選擇最接近點對
一維空間的情況:
- 假設我們用x軸上某個點
m 將S 劃分爲2個子集S1 和S2 ,基於平衡子問題的思想,用S中各點座標的中位數來作分割點 - 遞歸地在
S1 和S2 上找出其最接近點對 {p1,p2 } 和 {q1,q2 } - 設
d=min {|p1−p2|,|q1−q2| } ,則S中的最接近點對或者是{{p1,p2 },或者是{q1,q2 },或者是某個{p3,q3 },其中p3∈S1 且q3∈S2 - 如果S的最接近點對是{
p3,q3 },即|p3−q3|<d
- 則
p3 和q3 兩者與m 的距離不超過d - 即:
p3∈(m−d,m] ,q3∈(m,m+d]
- 則
- 問題分析
- 在
S1 中每個長度爲d 的半閉區間至多包含一個點 - 由於
m 是S1 和S2 的分割點,因此(m−d,m] 中至多包含S 中的一個點 - 如果
(m−d,m] 中有S 中的點,則此點就是S1 中最大點(S2 同理) - 因此用線性時間可找到
(m−d,m] 和(m,m+d] 中所有點(即p_{3}p3 和q3 ) - 所以,用線性時間就可以將
S1 的解和S2 的解合併成爲S的解
- 在
- 算法複雜度:
T(n)=O(nlogn) - 求一維點集S的最接近點對的算法
int cpair(int S[], int n){
int d, d1, d2;
if(n<2) {return INTMAX;}
int m = {S中各點座標的中位數};
構造S1和S2; //S1={x S|x ≤m}, S2={x S|x > m}
d1 = cpair(S1); d2 = cpair(S2);
p = max(S1); q = min(S2);
d = min(d1, d2, q-p);
return d;
}
二維空間的情況:
- 考慮二維的情況
- 選取二維平面的一條垂直線 L:x=m作爲分割線,其中m爲S中各點x座標的中位數,由此將S分割爲S1和S2
- 遞歸地在S1和S2上找出其最小距離d1和d2
- 設:d=min{d1,d2},S中的最接近點對間的距離或者是d,或者是某個點對{p,q}之間的距離,其中p∈S1且q∈S2
- 如果用符號P1和P2分別表示直線 L 的左右兩邊寬爲d的區域,則必有p∈P1且q∈P2
- 問題分析
- 考慮P1中任意一點p:它若與P2中的點q構成最接近點對的候選者,則必有:distance(p,q)<d,P2中滿足條件的點一定落在矩形R中,矩形R的大小爲:d×2d
- 由d的定義可知:P2中任何2個點(qi∈S)的距離都不小於d,由此可以推出矩形R中最多隻有6個S中的點
- 因此,在分治法的合併步驟中最多只需要檢查6×n/2=3n個候選者
- 如何確定需要檢查的6個點?
- 可以將p和P2中所有S2的點投影到垂直線l上
- 由於能與p點一起構成最接近點對候選者的S2中的點一定在矩形R中,所以它們在直線 L 上的投影點距 p 在 L 上投影點的距離小於d
- 根據上述分析,這種投影點最多隻有6個
- 因此,若將區域P1和P2中所有S中的點按其y座標排好序 則對P1中的所有點,只需一次掃描就可以找出所有候選者:①對排好序的點作一次掃描,可以找出所有最接近點對的候選者;②對P1中每個點,最多隻需檢查P2中排好序的相繼6個點
6. 示例: 循環賽日程表問題
- 設計一個滿足以下要求的比賽日程表:
- 每個選手必須與其他n-1個選手各賽一次
- 每個選手一天只能賽一次
循環賽一共進行n-1天
- 分治算法策略:
- 將所有的選手分爲兩半,n個選手的比賽日程表可以通過爲n/2個選手設計的比賽日程表來決定
- 遞歸地用對選手進行分割,直到只剩下2個選手時,只要讓這2個選手進行比賽就可以了