分治法

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轴上某个点 m S划分为2个子集 S1 S2 ,基于平衡子问题的思想,用S中各点坐标的中位数来作分割点
  2. 递归地在 S1 S2 上找出其最接近点对 { p1,p2 } 和 { q1,q2 }
  3. d=min { |p1p2|,|q1q2| } ,则S中的最接近点对或者是{{ p1,p2 },或者是{ q1,q2 },或者是某个{ p3,q3 },其中 p3S1 q3S2
  4. 如果S的最接近点对是{ p3,q3 },即 |p3q3|<d
    • p3 q3 两者与 m 的距离不超过d
    • 即: p3(md,m] q3(m,m+d]
  5. 问题分析
    • S1 中每个长度为 d 的半闭区间至多包含一个点
    • 由于m S1 S2 的分割点,因此 (md,m] 中至多包含 S 中的一个点
    • 如果(md,m]中有 S 中的点,则此点就是S1中最大点( S2 同理)
    • 因此用线性时间可找到 (md,m] (m,m+d] 中所有点(即p_{3} p3 q3
    • 所以,用线性时间就可以将 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个选手进行比赛就可以了
      图片名称
  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值