【算法设计】递归与分治

递归法

直接或间接地调用自身的算法叫递归算法。

序列元素的全排列

设某序列R中有n个元素,分别为R[0],R[1],…,R[n-1],请输出该序列的所有可能的排列。
分析:所谓全排列就是将序列中的元素按照不同的顺序进行排列,将所有这些可能的排列输出就称之为全排列。
分析一下其全排列,我们可以发现其产生规律:
依次取序列中的一个元素放在序列的最前面,然后对剩余元素求全排列,在对剩余元素进行全排列时所采用的方法也是依次取其中一个元素,然后对剩余元素进行全排列,依此类推,当序列中只有一个元素时,也就无须再对其排列了。其具体程序见Perm(密码:cfx6)

整数的划分问题

求将某正整数划分成若干个正整数之和的所有排列的个数。
分析:将某正整数划分成若干个正整数之和的方法无外乎将其依次分解成比其小的数的和。比如将4可分解成:

3+1
2+2,2+1+1
1+1+1+1

观察上面的分解过程可以看出, 可依次取比前一个小的数加上剩余的数,比如第一个取3,剩余的就是1,1无法再分。如果第一个取2,则剩余的取2,2还可以分解成1+1。如果令q(n,m)表示将n分解成最大可取m的数的个数,n表示需要分解的数,m表示分解后最大可取的数。则有如下的表达式:

  1. q(n,1)=1; 如果待分解的数为5,而分解后最大可取的数最大为1,则只能有一种取法。

  2. q(n,n)=1+q(n,n-1);也就是如果待分解的数为n,分解后最大可取的数为n,则可理解为分解后最大可取的数为n-1的个数再加上1。

  3. q(n,m)=q(n,m-1)+q(n-m,m)。其中1<m<n。也就是如果将n分解成最大数为m的数,其实可以理解为首先将其中一个加数确定取m,显然剩余的数为n-m。在最大数为n-m的情况下,其最大可取的个数即为在一个数确定之后,其余部分可分解的个数。显然在一个加数确定之后,其排列只是其中一部分,而另一部分则是比该加数小1的情况下的全排列。也就是将n分解成最大数为m-1的所有排列。

综合上述情况,可得到如下的递推式:
q(n,m)=1; m=1或者n=1;
q(n,m)=q(n,n); m>n
q(n,m)=1+q(n,n-1); n=m;
q(n,m)=q(n,m-1)+q(n-m,m) n>m>1
根据上面的递推式可以很容易的写出程序,计算出将n分解成最大为m的元素的全排列。
设计q(n,m)的递归函数如下:

int q(int n,int m){
if((n<1)||(m<1))
   return 0;
if((n==1)||(m==1))
   return 1;
if(n<m)
   return q(n,n);
 if(n==m)
   return q(n,m-1)+1;
 return q(n,m-1) + q(n-m,m);
 } 
  • 使用递归方法时须注意的问题:
    (1) 递归调用函数必须在满足某个条件时能够退出该程序
    (2) 递归调用由于使用堆栈,因此占用的存储空间会很大,且所花费的时间也很多,因而效率低下。

为避免效率低下的问题,一般采用非递归算法来取代递归算法,可采用堆栈的形式来解决该问题。

分治法

将一个规模比较大的问题分解成若干个规模较小的问题,然后将这些子问题的解合并解出整个问题的解。
比如排序算法中的归并排序算法就是比较典型的分治法。

二分搜索技术

二分搜索算法是运用分治策略的典型例子。给定已排好序的n个元素a[0:n-1],现要在这n个元素中找出一个特定元素x。
采用二分搜索算法的基本思想是,将n个元素分成个数大致相同的两半,取a[n/2]与x作比较。如果x=a[n/2],则找到x,算法终止;如果x<a[n/2],则在数组a的左半部继续搜索x;如果x>a[n/2],则在数组a的右半部继续搜索x。具体算法可描述如下:

int BinarySearch(int a[], const int& x, int n)//在a[0]...a[n-1]中搜索x
{
 //找到x时返回其在数组中的位置,否则返回-1
 int left = 0;
 int right = n - 1;
 while (left <= right)
 {
  int middle = (left + right) / 2;
  if (x == a[middle])
   return middle;
  if (x > a[middle])
   left = middle + 1;
  else
   right = middle - 1;
 }
 return -1;
 }
}

归并排序

A集合中含有n/k 个元素,B中包含其余的元素。递归地使用分而治之方法对A和B进行排序。然后采用一个被称之为归并(merge)的过程,将已排好序的A和B合并成一个集合。

void MergeSort(int a[], int left, int right) {
 if (left < right) { //至少有两个元素
  int i = (left + right) / 2; //取中点
  MergeSort(a, left, i);
  MergeSort(a, i + 1, right);
  Merge(a, b, left, i, right);//合并到数组b
  Copy(a, b, left, right);//复制到数组a
 }
}

事实上,上述算法的递归过程只是将待排序集合一分为而,直至待排序集合只剩下一个元素为止,然后不断合并两个排好序的数组段。
消去递归后的归并排序算法如下:

void MergeSort(int a[], int n) {
 int* b = new int[n];
 int s = 1;
 while (s < n) {
  MergePass(a, b, s, n); //合并到数组a
  s += s;
  MergePass(b, a, s, n); //合并到数组b
  s += s;
 }
}
void MergePass(int x[], int y[], int s, int n) { //合并大小为s的相邻子数组
 int i = 0;
 while (i <= n - 2 * s){
 Merge(x, y, i, i + s - 1,i + 2 * s - 1);    //合并大小为s的两段子数组
 i = i + 2 * s;                               
 }
  if (i + s < n)      //剩下的元素个数小于2s
   Merge(x,y, i, i + s - 1, n - 1);
  else
   for (int j = i; j <= n - 1; j++)
    y[j] = x[j];
}
void Merge(int c[], int d[], int l, int m, int r) { //合并c[l:m]和c[m+1:r]到d[l:r]
 int i = l,j = m + 1, k = l;
 while((i <= m) && (j = r)) {
  if (c[i] <= c[j])
   d[k++] = c[i++];
  else
   d[k++] = c[j++];
  if (i > m) {
   for (int q = j; q <= r; q++)
    d[k++] = c[q];
  }
  else {
   for (int q = i; q <= m; q++)
    d[k++] = c[q];
  }
 }

快速排序

快速排序算法是基于分治策略的另一个排序算法。其基本思想是,对于输入的子数组a[p:r],按照以下三个步骤进行排序。

  1. 分解:以a[p]为基准元素将a[p:r]划分成3段a[p:q-1],a[q]和a[q+1:r],使a[p:q-1]中任何一个元素小于等于a[q],而a[q+1:r]中任何一个元素大于等于a[q]。下标q在划分过程中确定。
  2. 递归求解:通过递归调用快速排序算法,分别对a[p:q-1]和a[q+1:r]进行排序。
  3. 合并:由于对a[p:q-1]和a[q+1:r]的排序是就地进行的,因此在a[p:q-1]和a[q+1:r]都已经排好序后,a[p:r]已经排好序。

快速排序算法如下:

void QuickSort(int a[], int p, int r){
  if (p < r) {
   int q = Partition(a, p, r);
   QuickSort(a, p, q - 1);
   QuickSort(a, q + 1, r);
  }
 }
 int Partition(int a[], int p, int r) {
  int i = p, j = r + 1;
  int x = a[p];
  //将小于x的元素交换到左边区域,将大于x的元素交换到右边区域
  while (true) {
   while (a[++i] < x && i < r);
   while (a[--j] > x);
   if (i >= j)
    break;
   swap(a[i], a[j]);
  }
  a[p] = a[j];
  a[j] = x;
  return j;
 }

棋盘覆盖问题

在一个2k*2k个方格组成的棋盘中,若恰有一个方格与其他方格不同,则称该方格为一特殊方格,且称该棋盘为一特殊棋盘。显然,特殊方格在棋盘上出现的位置有4k种情形。因而对任何k≥0,有4k种特殊棋盘。图中的特殊棋盘是k=2时16个特殊棋盘中的一个。
在这里插入图片描述
在棋盘覆盖问题中,要用图所示的4种不同形态的L型骨牌覆盖一个给定的特殊棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。易知,在任何一个2k*2k的棋盘覆盖中,用到的L型骨牌个数恰为(4^k-1)/3。
在这里插入图片描述

用分治策略,可以设计解棋盘覆盖问题的一个简捷的算法。当k>0时,将2k * 2k棋盘分割为4个2k-1 * 2k-1子棋盘。
特殊方格必位于4个较小子棋盘之一中,其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘,可以用一个L型骨牌覆盖这3个较小棋盘的汇合处,如图所示,这3个子棋盘上被L型骨牌覆盖的方格就成为该棋盘上的特殊方格,从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割,直至棋盘简化为1*1棋盘。
在这里插入图片描述

实现这种分治策略的算法如下:

void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
 if (size == 1)
  return;
 int t = tile++, s = size / 2; //L型骨牌号,分割棋盘
 //覆盖左上角子棋盘
 if (dr < tr + s && dc < tc + s) //特殊方格在此棋盘中
  ChessBoard(tr, tc, dr, dc, s); 
 else{ //在此棋盘中无特殊方格    
  Board[tr + s - 1][tc + s - 1] = t; //用t号L型骨牌覆盖右下角
  ChessBoard(tr, tc, tr + s - 1, tc + s - t, s); //覆盖其余方格
 } 
 //覆盖右上角子棋盘
 if (dr < tr + s && dc >= tc + s)  //特殊方格在此棋盘中
  ChessBoard(tr, tc + s, dr, dc, s);
 else{   //在此棋盘中无特殊方格    
  Board[tr + s - 1][tc + s ] = t; //用t号L型骨牌覆盖左下角
  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; //用t号L型骨牌覆盖右上角
  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; //用t号L型骨牌覆盖右上角
  ChessBoard(tr + s, tc + s, tr + s, tc + s, s); //覆盖其余方格
 }   
}

最近点对问题

求平面上若干个点中距离最近的一组点对。
为求解该问题,可将这个问题分解成若干个小的问题。也就是把这些点按照X坐标大小首先排序,然后将其分解成若干组,分别求解这些组内点的距离,这样可得到这些组内距离最小的一组点。但该组点并不是距离最小的点对,因为组与组之间的点的距离还可能比求得的点的距离小,因此还需要求解组与组之间的点的最小距离。
如何求解组与组之间的点的最小距离呢?若设前面求得的最小距离为d,则在分割线两端d*2d范围内的点的距离可能小于d,也就是说只要求得这些点对的最小距离,与d比较就可求得其最小点对了。
在这里插入图片描述
显然,在该例中涉及到如下几个方面的算法:

  1. 排序算法。需要将所有点按照从小到大的顺序排列。这里涉及到既要按x大小排序也要按y大小排序的问题。
  2. 把按x坐标大小排序的点集分解成2个点或者3个点构成的集合,并求解集合内点的距离,然后得到其最小距离。并求分割线两端点所构成的点集中距离最小的点,与前面求得的点距比较,若小,则取代前面的结果;若大,则继续进行后面的合并比较处理。

其详细的程序设计见ShortDist.cpp(密码:7amy)。该程序还涉及到用二分法实现排序的算法。

备注

以上内容综合自老师所给资料和课本,侵删。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值