首页 > 代码库 > 分治算法
分治算法
分治算法
一、大话分治
分治算法,Divide-and-Conquer Method,我给他它的字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。也正对应着它的单词Devide—分,Conquer—治。
分支思想是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
二、基本思想及策略实现
设计思想
将一个难以直接解决的大问题,分割成若干个不可再分割且相互独立的原子级问题,这些原子级问题与原问题形式相同并且可以直接求出解,然后把这些原子级问题各个击破,最后把所有原子级问题的解收集起来即原问题的解。
算法准备
分治法实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
1、一定是先找到最小问题规模时的求解方法
2、然后考虑随着问题规模增大时的求解方法
3、找到求解的递归函数式后(各种规模或因子),设计递归程序即可。
分治的策略分为三步
1、Divide—分解:将原问题分解为若干个与原问题形式相同的子问题;
2、Conquer—解决:如果这些子问题可以直接解决,则解决并返回解,否则继续 分解;
3、Combine—合并:将各个子问题的解合并为原问题的解。
递归实现
分治算法的不断分解和收集解一般需要用递归实现,其实分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。
三、分治法的应用场景
分治法所能解决的问题一般具有以下几个特征:
① 该问题可以分解为若干个规模较小的且相互对立的相同问题,即该问题具有最优子结构性质;(这是分治应用的前提)
② 该问题的规模缩小到一定的程度就可以容易地解决;
③ 利用该问题分解出的子问题的解可以合并为该问题的解;(这是分治算法的关键,如果不满足,则分治毫无意义。若果不满足这一条时,可考虑动态规划)
以下经典问题常用分治方法解决
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)归并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔
四、分治法的具体实现
1、棋盘覆盖问题
问题描述:
在一个2^k×2^k 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。
解题思路:
当k>0时,将2k×2k棋盘分割为4个2^k-1×2^k-1 子棋盘(a)所示。特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的会合处,如 (b)所示,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为棋盘1×1。
每次都对分割后的四个小方块进行判断,判断特殊方格是否在里面。这里的判断的方法是每次先记录下整个大方块的左上角(top left coner)方格的行列坐标,然后再与特殊方格坐标进行比较,就可以知道特殊方格是否在该块中。如果特殊方块在里面,这直接递归下去求即可,如果不在,这根据分割的四个方块的不同位置,把右下角、左下角、右上角或者左上角的方格标记为特殊方块,然后继续递归。在递归函数里,还要有一个变量s来记录边的方格数,每次对方块进行划分时,边的方格数都会减半,这个变量是为了方便判断特殊方格的位置。其次还要有一个变nCount来记录L型骨牌的数量。
Java代码实现
public class CoverChessBoard {
static int n=0;
static int k=4;
static int [][] board;
static int len=1;
{
while(k>0) {
len*=2;
k--;
}
board = new int[len][len];
}
//xy为当前数组起始坐标,len是当前块的长度,xs ys是标记点坐标
void cover(int x,int y,int len,int xs,int ys){
if(len == 1) return;
n++;
//定义中间坐标,方便运算
int xc=x+len/2-1,yc=y+len/2-1;
len/=2;
//标记点位于左上方区域
if(xs<=xc&&ys<=yc){
//先把其他三个区域的相邻角定点设为标记点并填充
//board[xc][yc]=n;
board[xc][yc+1] = n;
board[xc+1][yc] = n;
board[xc+1][yc+1] = n;
//把这个大区域分成四个相同大小的小区域,分别填充
cover(x,y,len,xs,ys);
cover(x,yc+1,len,xc,yc+1);
cover(xc+1,y,len,xc+1,yc);
cover(xc+1,yc+1,len,xc+1,yc+1);
}
//标记点位于右上区域
if(xs<=xc&&ys>yc){
//先把其他三个区域的相邻角定点设为标记点并填充
board[xc][yc]=n;
//board[xc][yc+1] = n;
board[xc+1][yc] = n;
board[xc+1][yc+1] = n;
//把这个大区域分成四个相同大小的小区域,分别填充
cover(x,y,len,xc,yc);
cover(x,yc+1,len,xs,ys);
cover(xc+1,y,len,xc+1,yc);
cover(xc+1,yc+1,len,xc+1,yc+1);
}
//标记点位于左下区域
if(xs>xc&&ys<=yc){
//先把其他三个区域的相邻角定点设为标记点并填充
board[xc][yc]=n;
board[xc][yc+1] = n;
//board[xc+1][yc] = n;
board[xc+1][yc+1] = n;
//把这个大区域分成四个相同大小的小区域,分别填充
cover(x,y,len,xc,yc);
cover(x,yc+1,len,xc,yc+1);
cover(xc+1,y,len,xs,ys);
cover(xc+1,yc+1,len,xc+1,yc+1);
}
//标记点位于右下区域
if(xs>xc&&ys>yc){
//先把其他三个区域的相邻角定点设为标记点并填充
board[xc][yc]=n;
board[xc][yc+1] = n;
board[xc+1][yc] = n;
//board[xc+1][yc+1] = n;
//把这个大区域分成四个相同大小的小区域,分别填充
cover(x,y,len,xc,yc);
cover(x,yc+1,len,xc,yc+1);
cover(xc+1,y,len,xc+1,yc);
cover(xc+1,yc+1,len,xs,ys);
}
}
public static void main(String[] args) {
CoverChessBoard cc = new CoverChessBoard();
System.out.println(len);
//数据测试,假设(10,10)是标记点
cc.cover(0, 0, len, 10, 10);
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board.length; j++) {
if(cc.board[i][j]<10) System.out.print(0);
System.out.print(cc.board[i][j]+" ");
}System.out.println();
}
}
}
2、Strassen矩阵乘法
题目描述:矩阵m*n的A矩阵和n*p的B矩阵相乘,他们的乘积AB是个m*p的矩阵,求这个AB矩阵。
普通的暴力破解方法:直接通过三个for循环来模拟我们矩阵就算的过程,这样当然可以做出来,但是这样做的时候时间复杂度是O(n3),效率比较低。
Strassen矩阵乘法:
1969年,德国的一位数学家Strassen证明O(N^3)的解法并不是矩阵乘法的最优算法,他做了一系列工作使得最终的时间复杂度降低到了O(n^2.80)。他把矩阵分成多个块并定义了多个变量来便于计算,比如矩阵A={{a,b}{c,d}},矩阵B={{e,f}{g,h}},
那么两矩阵相乘的结果AB={{ae+bg,ag+bh}{ce+df,cg+dh}};
Strassen先生通过定义了7个变量来方便运算,变量如下
P1 = a ( f - h ) ;
P2 =(a + b ) h;
P3 = (c + d) e;
P4 = d (g - e );
P5 = ( a + d ) (e + h);
P6 = (b - d ) ( e + h);
P7 = (a - c) (e + f );
那么两矩阵乘积AB={{P5+P4-P2+P6 ,P1+P2} {P3+P4 ,P1+P5-P3-P7 }};
如此便完成了计算,如果我们我们遇到更大的矩阵时,我们先把矩阵等分成四部分,就比如是矩阵A中的a,b,c,d,按照Strassen方法模拟整个过程就可以实现了。因为只是个模拟的过程我就不在这里实现了。
分治算法