分治法

1. 分治法

分治法的基本思想是將一個規模爲n的問題分解爲k個規模較小的子問題,這些子問題相互獨立且與原問題相同。遞歸的解這些子問題,然後將各子問題的解合併得到原問題的解。

  • 分治模式在每一層遞歸上都有三個步驟:分解(Divide);求解(Conquer);合併(Combine)

    1. Divide:將一個難以直接解決的大問題,分割成一些規模較小的子問題,這些子問題互相獨立,且與原問題相同。
    2. Conquer:遞歸求解子問題,若問題足夠小則直接求解。
    3. Combine:將各子問題的解合併得到原問題的解
  • 分治法所能解決的問題一般具有以下四個特徵:

    1. 該問題的規模縮小到一定的程度就可以容易地解決
    2. 該問題可以分解爲若干個規模較小的相同問題,即該問題具有最優子結構性質
    3. 利用該問題分解出的子問題的解可以合併爲該問題的解
    4. 該問題所分解出的各個子問題是相互獨立的,即子問題之間不包含公共的子問題。
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. 示例: 棋盤覆蓋問題

問題描述:在一個2k×2k 個方格組成的棋盤中,有一個方格與其它不同,稱該方格爲特殊方格,且稱該棋盤爲一特殊棋盤。棋盤覆蓋問題如下:

  • 要求用圖示的4種L形態骨牌覆蓋給定的特殊棋盤
  • 限制條件:覆蓋給定特殊棋盤上除特殊方格以外的所有方格
  • 限制條件:任何2個L型骨牌不得重疊覆蓋
    圖片名稱

思路:

  1. k>0 時,將 2k×2k 棋盤分割爲4個 2k1×2k1 子棋盤

    • 特殊方格必位於4個較小的子棋盤其中之一
    • 其餘3個子棋盤中無特殊方格
  2. 爲了將這3個無特殊方格的子棋盤轉化爲特殊棋盤,可以用一個L型骨牌覆蓋這3個較小棋盤的會合處,從而將原問題轉化爲4個較小規模的棋盤覆蓋問題

  3. 遞歸地使用這種分割,直至棋盤簡化爲棋盤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. 示例: 快速排序

算法基本思想:

  1. 在數組中確定一個記錄(的關鍵字)作爲“劃分元”
  2. 將數組中關鍵字小於劃分元的記錄均移動至該記錄之前
  3. 由此:一趟排序之後,序列R[s...t] 將分割成兩部分
    • R[s...i1]R[i+1...t]
    • 且滿足:R[s...i1]R[i]R[i+1...t]
    • 其中:R[i] 爲選定的“劃分元”
  4. 對各部分重複上述過程,直到每一部分僅剩一個記錄爲止

    • 首先對無序的記錄序列進行一次劃分
    • 之後分別對分割所得兩個子序列“遞歸”進行快速排序

圖片名稱

快速排序算法特點:

  1. 時間複雜度

    • 最好情況:T(n)=O(nlog2n) (每次總是選到中間值作劃分元)
    • 最壞情況:T(n)=O(n²) (每次總是選到最小或最大元素作劃分元)
    • 快速排序算法的平均時間複雜度爲:O(nlog2n)
  2. 快速排序算法是不穩定的

    • 例如待排序序列: 49 49 38 65
    • 快速排序結果爲: 38 49 49 65
  3. 算法性能與序列中關鍵字的排列順序和劃分元的選取有關

    • 當初始序列按關鍵字有序(正序或逆序)時,快速排序蛻化爲冒泡排序,此時算法性能最差:時間複雜度爲O(n²)
    • 可以用“三者取中”法來選取劃分元
    • 也可採用隨機選取劃分元的方式

5. 示例: 最接近點對問題

問題描述:給定平面上的n個點,找出其中的一對點,使得在n個點組成的所有點對中,該點對的距離最小

求解最接近點對方法:

  1. 直觀解法
    • 將每一個點與其他n1 個點的距離算出,找出最小距離
    • 時間複雜度:T(n)=n(n1)/2+n=O(n2)
  2. 分治法
    • 分解:將n個點的集合分成大小近似相等的兩個子集
    • 求解:遞歸地求解兩個子集內部的最接近點對
    • 合併(關鍵問題):從子空間內部最接近點對,和兩個子空間之間的最接近點對中,選擇最接近點對

一維空間的情況:

圖片名稱

  1. 假設我們用x軸上某個點mS 劃分爲2個子集S1S2 ,基於平衡子問題的思想,用S中各點座標的中位數來作分割點
  2. 遞歸地在S1S2 上找出其最接近點對 {p1,p2 } 和 {q1,q2 }
  3. d=min {|p1p2|,|q1q2| } ,則S中的最接近點對或者是{{p1,p2 },或者是{q1,q2 },或者是某個{p3,q3 },其中p3S1q3S2
  4. 如果S的最接近點對是{p3,q3 },即|p3q3|<d
    • p3q3 兩者與m 的距離不超過d
    • 即:p3(md,m]q3(m,m+d]
  5. 問題分析
    • S1 中每個長度爲d 的半閉區間至多包含一個點
    • 由於mS1S2 的分割點,因此(md,m] 中至多包含S 中的一個點
    • 如果(md,m] 中有S 中的點,則此點就是S1 中最大點(S2 同理)
    • 因此用線性時間可找到(md,m](m,m+d] 中所有點(即p_{3}p3q3
    • 所以,用線性時間就可以將S1 的解和S2 的解合併成爲S的解
  6. 算法複雜度:T(n)=O(nlogn)
  7. 求一維點集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;
}

二維空間的情況:

圖片名稱

  1. 考慮二維的情況
    • 選取二維平面的一條垂直線 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
  2. 問題分析
    • 考慮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個候選者
      圖片名稱
  3. 如何確定需要檢查的6個點?
    • 可以將p和P2中所有S2的點投影到垂直線l上
    • 由於能與p點一起構成最接近點對候選者的S2中的點一定在矩形R中,所以它們在直線 L 上的投影點距 p 在 L 上投影點的距離小於d
    • 根據上述分析,這種投影點最多隻有6個
    • 因此,若將區域P1和P2中所有S中的點按其y座標排好序 則對P1中的所有點,只需一次掃描就可以找出所有候選者:①對排好序的點作一次掃描,可以找出所有最接近點對的候選者;②對P1中每個點,最多隻需檢查P2中排好序的相繼6個點
      圖片名稱

6. 示例: 循環賽日程表問題

  1. 設計一個滿足以下要求的比賽日程表:
    • 每個選手必須與其他n-1個選手各賽一次
    • 每個選手一天只能賽一次
      循環賽一共進行n-1天
      圖片名稱
  2. 分治算法策略:
    • 將所有的選手分爲兩半,n個選手的比賽日程表可以通過爲n/2個選手設計的比賽日程表來決定
    • 遞歸地用對選手進行分割,直到只剩下2個選手時,只要讓這2個選手進行比賽就可以了
      圖片名稱
發佈了50 篇原創文章 · 獲贊 703 · 訪問量 55萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章