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. 示例: 棋盘覆盖问题
问题描述:在一个 2k×2k 个方格组成的棋盘中,有一个方格与其它不同,称该方格为特殊方格,且称该棋盘为一特殊棋盘。棋盘覆盖问题如下:
- 要求用图示的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]
- 则
p3
和
q3
两者与
m
的距离不超过
- 问题分析
- 在 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个选手进行比赛就可以了