分治算法

分治算法
基本思想
◇ 概念

在计算机科学中,分治法是一种很重要的算法。字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……
任何一个可以用计算机求解的问题所需的计算时间都与其规模有关。问题的规模越小,越容易直接求解,解题所需的计算时间也越少。例如,对于n个元素的排序问题,当n=1时,不需任何计算。n=2时,只要作一次比较即可排好序。n=3时只要作3次比较即可,…。而当n较大时,问题就不那么容易处理了。要想直接解决一个规模较大的问题,有时是相当困难的。

◇ 基本思想及策略

分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
如果原问题可分割成k个子问题,1<k≤n,且这些子问题都可解并可利用这些子问题的解求出原问题的解,那么这种分治法就是可行的。由分治法产生的子问题往往是原问题的较小模式,这就为使用递归技术提供了方便。在这种情况下,反复应用分治手段,可以使子问题与原问题类型一致而其规模却不断缩小,最终使子问题缩小到很容易直接求出其解。这自然导致递归过程的产生。分治与递归像一对孪生兄弟,经常同时应用在算法设计之中,并由此产生许多高效算法。


算法分析
◆ 分治法适用的情况

分治法所能解决的问题一般具有以下几个特征:
⑴ 该问题的规模缩小到一定的程度就可以容易地解决
⑵ 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
⑶ 利用该问题分解出的子问题的解可以合并为该问题的解;
⑷ 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;
第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。

◆ 分治法的基本步骤

分治法在每一层递归上都有三个步骤:
step1 分解:将原问题分解为若干个规模较小,相互独立,与原问题形式相同的子问题;
step2 解决:若子问题规模较小而容易被解决则直接解,否则递归地解各个子问题
step3 合并:将各个子问题的解合并为原问题的解。它的一般的算法设计模式如下:
Divide-and-Conquer(P)
1. if |P|≤n0
2. then return(ADHOC(P))
3. 将P分解为较小的子问题 P1 ,P2 ,...,Pk
4. for i←1 to k
5. do yi ← Divide-and-Conquer(Pi) △ 递归解决Pi
6. T ← MERGE(y1,y2,...,yk) △ 合并子问题
7. return(T)
其中|P|表示问题P的规模;n0为一阈值,表示当问题P的规模不超过n0时,问题已容易直接解出,不必再继续分解。ADHOC(P)是该分治法中的基本子算法,用于直接解小规模的问题P。因此,当P的规模不超过n0时直接用算法ADHOC(P)求解。算法MERGE(y1,y2,...,yk)是该分治法中的合并子算法,用于将P的子问题P1 ,P2 ,...,Pk的相应的解y1,y2,...,yk合并为P的解。

◆ 分治法的复杂性分析

一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n)= k T(n/m)+f(n)
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。
通常假定T(n)是单调上升的,从而当 mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。

◆ 使用分治法求解的一些经典问题

① 二分搜索
② 大整数乘法
③ Strassen矩阵乘法
④ 棋盘覆盖
⑤ 合并排序
⑥ 快速排序
⑦ 线性时间选择
⑧ 最接近点对问题
⑨ 循环赛日程表
⑩ 汉诺塔

◆ 依据分治法设计程序时的思维过程

实际上就是类似于数学归纳法,找到解决本问题的求解方程公式,然后根据方程公式设计递归程序。
⒈ 一定是先找到最小问题规模时的求解方法
⒉ 然后考虑随着问题规模增大时的求解方法
⒊ 找到求解的递归函数式后(各种规模或因子),设计递归程序即可。


例题
找出伪币

给你一个装有16个硬币的袋子。16个硬币中有一个是伪造的,并且那个伪造的硬币比真的硬币要轻一些。你的任务是找出这个伪造的硬币。为了帮助你完成这一任务,将提供一台可用来比较两组硬币重量的仪器,利用这台仪器,可以知道两组硬币的重量是否相同。比较硬币1与硬币2的重量。假如硬币1比硬币2轻,则硬币1是伪造的;假如硬币2比硬币1轻,则硬币2是伪造的。这样就完成了任务。假如两硬币重量相等,则比较硬币3和硬币4。同样,假如有一个硬币轻一些,则寻找伪币的任务完成。假如两硬币重量相等,则继续比较硬币5和硬币6。按照这种方式,可以最多通过8次比较来判断伪币的存在并找出这一伪币。
另外一种方法就是利用分而治之方法。假如把16硬币的例子看成一个大的问题。第一步,把这一问题分成两个小问题。随机选择8个硬币作为第一组称为A组,剩下的8个硬币作为第二组称为B组。这样,就把16个硬币的问题分成两个8硬币的问题来解决。第二步,判断A和B组中是否有伪币。可以利用仪器来比较A组硬币和B组硬币的重量。假如两组硬币重量相等,则可以判断伪币不存在。假如两组硬币重量不相等,则存在伪币,并且可以判断它位于较轻的那一组硬币中。最后,在第三步中,用第二步的结果得出原先16个硬币问题的答案。若仅仅判断硬币是否存在,则第三步非常简单。无论A组还是B组中有伪币,都可以推断这16个硬币中存在伪币。因此,仅仅通过一次重量的比较,就可以判断伪币是否存在。
假设需要识别出这一伪币。把两个或三个硬币的情况作为不可再分的小问题。注意如果只有一个硬币,那么不能判断出它是否就是伪币。在一个小问题中,通过将一个硬币分别与其他两个硬币比较,最多比较两次就可以找到伪币。这样,16硬币的问题就被分为两个8硬币(A组和B组)的问题。通过比较这两组硬币的重量,可以判断伪币是否存在。如果没有伪币,则算法终止。否则,继续划分这两组硬币来寻找伪币。假设B是轻的那一组,因此再把它分成两组,每组有4个硬币。称其中一组为B1,另一组为B2。比较这两组,肯定有一组轻一些。如果B1轻,则伪币在B1中,再将B1又分成两组,每组有两个硬币,称其中一组为B1a,另一组为B1b。比较这两组,可以得到一个较轻的组。由于这个组只有两个硬币,因此不必再细分。比较组中两个硬币的重量,可以立即知道哪一个硬币轻一些。较轻的硬币就是所要找的伪币。
主要实现代码如下:

import java.util.Scanner;

public class Main {
    static final int MAXNUM = 30;
    private static int FalseCoin(int[] coin, int low, int high) {
        int sum1 = 0, sum2 = 0, sum3 = 0;
        int re = 0;
        if ( low+1 == high ) {
            if ( coin[low] < coin[high] ) {
                re = low+1;
                return re;
            }
            else {
                re = high+1;
                return re;
            }
        }

        if ( (high-low+1)%2 == 0 ) {//如果n是偶数
            //前半段
            for ( int i = low; i <= low+(high-low)/2; i++ ) {
                sum1 = sum1 + coin[i];
            }
            //后半段
            for ( int i = low+(high-low)/2+1; i <= high; i++ ) {
                sum2 = sum2 + coin[i];
            }
            if ( sum1 > sum2 ) {
                re = FalseCoin(coin, low+(high-low)/2+1, high);
                return re;
            }
            else if ( sum2 > sum1 ){//sum2 > sum1
                re = FalseCoin(coin, low, low+(high-low)/2);
                return re;
            }
            else {

            }
        }
        else {//如果是奇数
            //前半段,除去中间的一个硬币
            for ( int i = low; i <= low+(high-low)/2-1; i++ ) {
                sum1 = sum1 + coin[i];
            }
            //后半段,除去中间的一个硬币
            for ( int i = low+(high-low)/2+1; i <= high;i++ ) {
                sum2 = sum2 + coin[i];
            }
            sum3 = coin[low+(high-low)/2];
            if ( sum1 > sum2 ) {
                re = FalseCoin(coin, low+(high-low)/2+1, high);
                return re;
            }
            else if ( sum2 > sum1 ){
                re = FalseCoin(coin, low, low+(high-low)/2-1);
                return re;
            }
            else {

            }
            if ( sum1+sum3 == sum2+sum3 ) {
                re = low+(high-low)/2+1;
                return re;
            }
        }
        return re;
    }
    public static void main(String[] args) {
        int[] coin = new int[MAXNUM];
        System.out.println("分治算法求假币问题: ");
        System.out.println("请输入硬币总的个数: ");
        Scanner in = new Scanner(System.in);
        int n = in.nextInt();
        System.out.println("请输入所有硬币权值: ");
        for ( int i = 0; i < n; i++ ) {
            coin[i] = in.nextInt();
        }
        int position = FalseCoin(coin, 0, n-1);
        System.out.println("假币在 " + position + "个位置 ");
    }   
}
求最值

在n个元素中找出最大元素和最小元素。我们可以把这n个元素放在一个数组中,用直接比较法求出。算法如下:

void maxmin1(int A[],int n,int *max,int *min)
{
    int i;
   *min=*max=A[0];
    for(i=0;i <= n;i++)
    {
        if(A[i]> *max) *max= A[i];
        if(A[i] < *min) *min= A[i];
    }
}

上面这个算法需比较2(n-1)次。能否找到更好的算法呢?我们用分治策略来讨论。 把n个元素分成两组:
A1={A[1],...,A[int(n/2)]}和A2={A[INT(N/2)+1],...,A[N]}
分别求这两组的最大值和最小值,然后分别将这两组的最大值和最小值相比较,求出全部元素的最大值和最小值。如果A1和A2中的元素多于两个,则再用上述方法各分为两个子集。直至子集中元素至多两个元素为止。
例如有下面一组元素:-13,13,9,-5,7,23,0,15。用分治策略比较的算法如下:

void maxmin2(int A[],int i,int j,int *max,int *min)
/*A存放输入的数据,i,j存放数据的范围,初值为0,n-1,*max,*min 存放最大和最小值*/
{ int mid,max1,max2,min1,min2;
if (j==i) {最大和最小值为同一个数;return;}
if (j-1==i) {将两个数直接比较,求得最大和最小值;return;}
mid=(i+j)/2;
求i~mid之间的最大最小值分别为max1,min1;
求mid+1~j之间的最大最小值分别为max2,min2;
比较max1和max2,大的就是最大值;
比较min1和min2,小的就是最小值;
}
棋盘覆盖

题目:在一个(2^k)*(2^k)个方格组成的棋盘上,有一个特殊方格与其他方格不同,称为特殊方格,称这样的棋盘为一个特殊棋盘。我们要求对棋盘的其余部分用L型方块填满(注:L型方块由3个单元格组成。即围棋中比较忌讳的愚形三角,方向随意),且任何两个L型方块不能重叠覆盖。L型方块的形态如下:
题目的解法使用分治法,即子问题和整体问题具有相同的形式。我们对棋盘做一个分割,切割一次后的棋盘如图1所示,我们可以看到棋盘被切成4个一样大小的子棋盘,特殊方块必定位于四个子棋盘中的一个。假设如图1所示,特殊方格位于右上角,我们把一个L型方块(灰色填充)放到图中位置。这样对于每个子棋盘又各有一个“特殊方块”,我们对每个子棋盘继续这样分割,直到子棋盘的大小为1为止。
用到的L型方块需要(4^k-1)/3 个,算法的时间是O(4^k),是渐进最优解法。
本题目的C语言的主要代码如下(TC2.0下调试),运行时,先输入k的大小,(1<=k<=6),然后分别输入特殊方格所在的位置(x,y), 0<=x,y<=(2^k-1)。

#include<stdio.h>
//#include<conio.h>
//#include<math.h>
inttitle=1;
intboard[64][64];
voidchessBoard(inttr,inttc,intdr,intdc,intsize){
ints,t;
if(size==1)return;
t=title++;s=size/2;
if(dr<tr+s&&dc<tc+s)
chessBoard(tr,tc,dr,dc,s);
else
{
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
{
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
{
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
{
board[tr+s][tc+s]=t;
chessBoard(tr+s,tc+s,tr+s,tc+s,s);
}
}
voidmain(){
intdr=0,dc=0,s=1,i=0,j=0;
printf("printinthesizeofchess:\n");
scanf("%d",&s);
printf("printinspecalpointx,y:\n");
scanf("%d%d",&dr,&dc);
if(dr<s&&dc<s)
{
chessBoard(0,0,dr,dc,s);
for(i=0;i<s;i++)
{
for(j=0;j<s;j++){
printf("%4d",board[i][j]);
}
printf("\n");
}
}
else
printf("thewrongspecalpoint!!\n");
getch();
}
二分查找

二分查找也是典型的分治算法的有应用。二分查找需要一个默认的前提,那就是查找的数列是有序的。
二分查找的思路比较简单:
⑴ 选择一个标志i将集合分为二个子集合
⑵ 判断标志L(i)是否能与要查找的值des相等,相等则直接返回
⑶ 否则判断L(i)与des的大小
⑷ 基于判断的结果决定下步是向左查找还是向右查找
⑸ 递归记性上面的步骤
主要实现代码如下:

def binarySearch(L,e,low,high):    
   if high == low:       
      return L[low] == e    
   mid = (low+high)//2    
   if L[mid]==e:       
      return True    
   elif L[mid]>e:        
      if low == mid:            
          return False       
      else:            
          return binarySearch(L,e,low, mid-1)    
   else:       
      return binarySearch(L,e,mid+1,high) 
def search(L,e):    
   result = binarySearch(L,e,0,len(L)-1)    
   print result    
L = range(10);
e = 7 
search(L,e)    

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值