分治法

概述

分治法将一个难以直接解决的大问题划分成一些规模较小的子问题,分别求解各个子问题,再合并子问题的解得到原问题的解

求解过程

  1. 划分
    把规模为 n 的原问题划分为 k 个(通常 k = 2)规模较小的子问题
  2. 求解子问题
    分别求解各个子问题
  3. 合并
    把各个子问题的解合并起来

启发式原则

  1. 平衡子问题
    子问题的规模最好大致相同
  2. 各子问题之间相互独立

例题

汉诺塔问题

题目:

有一座宝塔(塔A)上有 64 个金碟,所有碟子按从大到小的次序从塔底堆放至塔顶。紧挨着这座宝塔有另外两个宝塔(塔 B 和塔 C),要求把塔 A 上的碟子移动到塔 C 上去,其间借助于塔 B 的帮助。每次只能移动一个碟子,任何时候都不能把一个碟子放在比它小的碟子上面

分析:

对于 n 个碟子的汉诺塔问题,其操作步骤如下:

  1. 将塔 A 上的 n-1 个碟子借助塔 C 先移到塔 B 上
  2. 把塔 A 上剩下的一个碟子移到塔 C 上;
  3. 将 n-1 个碟子从塔 B 借助塔 A 移到塔 C 上

实现:

void Hanio(int n, char a, char b, char c){
    if(n==1){
        cout<<a<<"-->"<<c<<endl;
    }
    else{
        Hanio(n-1,a,c,b);
        cout<<a<<"-->"<<c<<endl;
        Hanio(n-1,b,a,c);
    }
}

归并排序

分析:

  • 划分
    将待排序序列从中间位置划分为两个长度相等的子序列;
  • 求解子问题
    分别对这两个子序列进行排序,得到两个有序子序列;
  • 合并
    将这两个有序子序列合并成一个有序序列

实现

void Merge(int r[],int s,int m,int t){
    int temp[t+1];
    int i=s,j=m+1,k=s;
    while(i<=m&&j<=t){
        if (r[i]<=r[j])
            temp[k++]=r[i++];
        else
            temp[k++]=r[j++];
    }
    while (i<=m){
        temp[k++]=r[i++];
    }
     while (j<=t){
        temp[k++]=r[j++];
    }
    for ( i = s; i <= t; i++){
        r[i]=temp[i];
    }
}
void mergeSort(int r[],int s,int t){
    if(s==t) return;
    else{
        int m=(s+t)/2;
        mergeSort(r,s,m);
        mergeSort(r,m+1,t);
        Merge(r,s,m,t);
    }
}

快速排序

分析:

  1. 划分
    选定一个记录作为轴值,以轴值为基准将整个序列划分为两个子序列,轴值的位置在划分的过程中确定,并且左侧子序列的所有记录均小于或等于轴值,右侧子序列的所有记录均大于或等于轴值;
  2. 求解子问题
    分别对划分后的每一个子序列递归处理;
  3. 合并
    由于对子序列的排序是就地进行的,所以合并不需要执行任何操作

实现:

int Partition(int r[],int low, int high){
    int p=r[low];
    while (low<high){
        while(low<high&&r[high]>p)
            high--;
        r[low]=r[high];
        while(low<high&&r[low]<p)
            low++;
        r[high]=r[low];
    }
    r[low]=p;
    return low;

}
void quickSort(int r[],int low,int high){
    if (low>=high)return;
    int p=Partition(r,low,high);
    quickSort(r,low,p);
    quickSort(r,p+1,high);
}

最大字段和问题

问题:
给定由 n 个整数(可能有负整数)组成的序列(a1, a2, …, an),最大子段和问题要求字串和的最大值。例如,序列(-20, 11, -4, 13, -5, -2)的最大子段和为 20

分析:

  1. 划分
    划分为(a1, …, an/2 )和(an/2+1, …, an),则会出现以下三种情况:
    • (a1, a2, …, an)的最大子段和 = (a1, …, an/2 )的最大子段和;
    • (a1, a2, …, an)的最大子段和 = (an/2+1, …, an)的最大子段和;
    • (a1, a2, …, an)的最大子段和在中间
  2. 求解子问题
    分别求解划分阶段的三种情况
  3. 合并
    取三者之中的较大者为原问题的解。

img

实现

int maxSum(int t[],int left,int right){
    int sum=0,leftSum=0,rightSum=0,centerSum=0,center=0;
    if (left==right)
        sum = t[left];
    else{
        center=(left+right)/2;
        leftSum=maxSum(t,left,center);
        rightSum=maxSum(t,center+1,right);
        //求解第三种情况
        //左侧最大
        int lefts=0,s1=0;
        for (int i = center; i >= left; i--){
            lefts+=t[i];
            if(lefts>s1) s1=lefts;
        }
        //右侧最大
        int rights=0,s2=0;
        for (int i = center+1; i <=right; i++){
            rights+=t[i];
            if(rights>s2) s2=rights;
        }
        centerSum=s1+s2;
    }
    if(centerSum<leftSum)
        sum=leftSum;
    else
        sum=centerSum;
    if(sum<rightSum)
        sum=rightSum;
    return sum;
}

棋盘覆盖问题

问题:
在一个\(2^k×2^k\) 个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为一特殊方格,称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用 4 种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何2 个L型骨牌不得重叠覆盖
imgimg

想法:

将整个棋盘平均划分4个区域,在中间放置一个骨牌,因此每个区域都包括一个特殊方格,将问题转化为4个子问题。
img

实现:

void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
	int s, t1;
	if (size == 1) return;
	t1 = ++t;
	s = size/2;
	if (dr < tr + s && dc < tc + s)
		ChessBoard(tr, tc, dr, dc, s); 
	else{
		board[tr + s - 1][tc + s - 1] = t1;
		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] = t1;
		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] = t1;
		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] = t1;
		ChessBoard(tr+s, tc+s, tr+s, tc+s, s);
	}
}

最近对问题

问题:

要求在包含n个点的集合中找出距离最近的两个点。严格地讲,距离最近的点对可能多于一对,简单起见,只找出其中的一对即可

分析:

  1. 划分
    利用\(x=m\)将点集划分为两个子集,其中m为中位数,每个子集包含n/2个点,最近的点有三种情况:
    • 最近的点在左子集中
    • 最近的点在右子集中
    • 最近的点横跨两个子集
  2. 求解子问题
    • 同在一个子集中:递归解决
    • 不在一个子集中:取\(d=min\{d_1,d_2\}\),可能出现最近距离的点必然在\([m-d,m+d]\)的范围内,因此只需在这个范围内搜索即可
  3. 合并

实现

//默认x坐标都是从小到大排列的
double closedPoint(int x[],int y[],int low,int high){
    if(high-low==1){
        return sqrt((x[low]-x[high])*(x[low]-x[high])+(y[low]-y[high])*(y[low]-y[high]));
    }
    int mid=(low+high)/2;
    double d1=closedPoint(x,y,low,mid);
    double d2=closedPoint(x,y,mid,high);
    //第三种情况
    double d=min(d1,d2);
    int i,j;
    for(i=mid;i>=low;i--){
        if(x[mid]-x[i]>d){
            break;
        }
        for(j=mid+1;j<=high;j++){
            if(x[j]-x[mid]>d){
                break;
            }
            double d3=sqrt((x[i]-x[j])*(x[i]-x[j])+(y[i]-y[j])*(y[i]-y[j]));
            if(d3<d){
                d=d3;
            }
        }
    }
    return d;
}

练习

    • 题目:分治法求最大元素
    • 实现:
     int max(int a[],int low,int high){
          if(low==high){
               return a[low];
          }
          int mid=(low+high)/2;
          int max1=max(a,low,mid);
          int max2=max(a,mid+1,high);
          return max1>max2?max1:max2;
     }
    
    • 题目:分治法,数组循环左移k位
    • 分析:前后分别翻转,再整体翻转
    • 实现:
        void reverse(int a[],int low,int high){
            while(low<high){
                int temp=a[low];
                a[low]=a[high];
                a[high]=temp;
                low++;
                high--;
            }
        }
        void leftMove(int a[],int n,int k){
            reverse(a,0,k-1);
            reverse(a,k,n-1);
            reverse(a,0,n-1);
        }
    
    • 题目:递归n个元素全排列
    • 实现:
        void perm(int a[], int k, int m)
        {
            int i;
            if (k > m)
            {
                for (i = 0; i <= m; i++)
                    cout<<a[i];
                cout<<endl;
            }
            else
            {
                for (i = k; i <= m; i++)
                {
                    swap(a[k], a[i]);
                    perm(a, k + 1, m);
                    swap(a[k], a[i]);
                }
            }
        }
    
    • 题目:有序序列\((r_1,r_2,r_3,...r_n)\),有\(i\)使得\(r_i=i\),用分治法求i;
    • 分析:二分查找
    • 实现:
        int find(int a[],int low,int high){
            if(low>high){
                return -1;
            }
            int mid=(low+high)/2;
            if(a[mid]==mid){
                return mid;
            }else if(a[mid]>mid){
                return find(a,low,mid-1);
            }else{
                return find(a,mid+1,high);
            }
        }
    
    • 题目:\(n\times n\)的二维数组,从左到右,从上到下升序排列,分治查找
    • 分析:二分查找
    • 实现:
    int find(int *a[],int s, int n, int x){
        int mid = (s+n)/2;
        if(s>n) return -1;
        if(a[mid][mid]==x) return mid;
        if(a[mid][mid]>x) return find(a,s,mid-1,x);
        else return find(a,mid+1,n,x);
    }
    
    • 题目:分治法逆序个数
    • 分析:归并排序
    • 实现:
    int merge(int a[],int low,int mid,int high){
        int i=low,j=mid+1,k=0;
        int temp[high-low+1];
        int count=0;
        while(i<=mid&&j<=high){
            if(a[i]<=a[j]){
                temp[k++]=a[i++];
            }else{
                temp[k++]=a[j++];
                count+=mid-i+1;
            }
        }
        while(i<=mid){
            temp[k++]=a[i++];
        }
        while(j<=high){
            temp[k++]=a[j++];
        }
        for(i=0;i<k;i++){
            a[low+i]=temp[i];
        }
        return count;
    }
    
posted @ 2023-03-23 09:58  asdio  阅读(223)  评论(0)    收藏  举报