五大常见算法策略之——递归与分治策略

五大常见算法策略之——递归与分治策略递归与分治策略递归与分治策略是五大常见算法策略之一,分治策略的思想就是分而治之,即先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的解。而分治与递归很多情况下都是一起结合使用的,能发挥出奇效(1+12),这篇文章我们将先从递归说起

大家好,欢迎来到IT知识分享网。

递归与分治策略

递归与分治策略是五大常见算法策略之一,分治策略的思想就是分而治之,即先将一个规模较大的大问题分解成若干个规模较小的小问题,再对这些小问题进行解决,得到的解,在将其组合起来得到最终的解。而分治与递归很多情况下都是一起结合使用的,能发挥出奇效(1+1>2),这篇文章我们将先从递归说起,再逐渐向分治过渡,主要讲解方式是通过9个例题来说明问题的,问题都是根据难度由简到难,由浅入深,对递归与分治能有个大概的了解雏形,当然最重要还是要做大量练习才能掌握。

0、递归
  0.0、Fibonacci数列(易)
  0.1、阶乘(易)
  0.2、小青蛙跳台阶(易)
  0.3、全排列问题(偏难)
  0.4、整数划分(偏难)
1、分治策略
  1.0、归并排序(一般)
  1.1、二分查找(易)
  1.2、棋盘覆盖(偏难)
  1.3、日程表问题(偏难)

递归

我们第一次接触递归一般都是在初学C语言时候的一道题目——Fibonacci数列中看到的,可能刚开始感觉有点不可思议,函数居然可以调用自己!Amazing!但事实如此,它确实存在,而递归也为我们某些算法的设计提供很大的便利,一般来说递归函数在理解起来并不是很难,甚至可以通过数学归纳法给予证明,但一直让人诟病的一点莫过于Debug的时候了,有时候调试一个较为复杂的递归函数能把人逼疯。

我们在这里将会由易到难,用一些例题先来讲解递归函数。采用Fibonacci数列来做这个引例来介绍递归函数。

Fibonacci

  第一个数是1,第二个数也是1,从第三个数开始,后面每个数都等于前两个数之和。要求:输入一个n,输出第n个斐波那契数。
我们先来整理一下思路,分下面三步来看:

  • 1、明确函数的输入和输出(即函数的作用)
  • 2、明确递归终止条件
  • 3、寻找函数的递归关系式

第一步,函数输入n,输出(也就是返回)第n个斐波那契数:

public static int fibonacci(int n){
        
}

第二步,明确递归终止条件:

public static int fibonacci(int n){
    if(n == 1) return 1;
    else if (n == 2) return 1;
}

第三步,寻找函数的递归关系:

public static int fibonacci(int n){
    if(n == 1) return 1;
    else if(n == 2) return 1;
    else return fibonacci(n - 1) + fibonacci(n - 2);
}

就这样,我们的一个斐波那契数列的递归函数就写完了。当然,这只是我们的一个开胃小菜,下面继续是入门级别的一个题,算阶乘。

阶乘

输入一个数,输出它的阶乘。我们同样用那三步往下走。
第一步,函数输入n,返回n的阶乘

public static int factorial(int n){     

}

第二步,明确递归终止条件:

public static int factorial(int n){                     //0的阶乘等于1
        if(n == 0)  return 1;
}

第三步,寻找函数的递归关系

public static int factorial(int n){                     //0的阶乘等于1
        if(n == 0)  return 1;
        else return factorial(n - 1) * n;
}

做完前两个你肯定会觉得这不是很简单吗,不要急,我们要由易到难,由浅入深,这样阶梯式的科学学习。下面这个例子是小青蛙跳台阶问题,这个问题被用于递归和动态规划类问题的例题,我们这里先用递归解答,后面还会用动态规划策略来解决这个问题。

小青蛙跳台阶

一只青蛙一次可以跳上1级台阶,也可以跳上2级,求该青蛙跳上一个n级的台阶共有多少种跳法。
还是三步走,第一步,明确函数的输入及返回

public static int Jump_Floor1(int n){
    
}

第二步,明确递归终止条件
如果n=1,那小青蛙只能一次跳上第一节台阶,所以一种跳法,如果n=2,那小青蛙可以一次跳一节跳两次,或者直接一次跳两节,所以两种跳法。

public static int Jump_Floor1(int n){
        if(n <= 2){
            return n;
        }
}

第三步,寻找函数的递归条件
这里可不能简单的return Jump_Floor1(n-1)就完事儿了,分了两种情况:1、第一次跳1级就还有n-1级要跳,2、第一次跳2级就还有n-2级要跳

public static int Jump_Floor1(int n){
    if(n <= 2){
        return n;
    }else{  //这里涉及到两种跳法,1、第一次跳1级就还有n-1级要跳,2、第一次跳2级就还有n-2级要跳
    return Jump_Floor1(n-1)+Jump_Floor1(n-2);
    }
}

下面这个例题是排列问题,就是求出一组数的全排列。

全排列问题

我们在全排列问题种需要用到一个交换函数swap用于交换两个数的位置,作如下定义:k数组种元素为待排列元素,k和m为待交换两元素的下标

private static void swap(int a[], int k, int m){       //交换k和m下标的元素的值
        int temp = a[k];
        a[k] = a[m];
        a[m] = temp;
}

接下来继续回到递归函数
第一步,明确函数的输入以及返回,这里我们需要输入待排列元素组成的数组,数组的第一个元素的下标,以及最后一个元素的下标

public static void perm(int a[], int k, int m){

}

第二步,明确递归终止条件,就是当只剩下一个元素时

public static void perm(int a[], int k, int m){
    if(k == m) {     //只有一个元素
        for (int i = 0; i <= m; i++) {
            System.out.print(a[i]+" ");
        }
        System.out.println();
    }
}

第三步,寻找递归条件

public static void perm(int a[], int k, int m){
    if(k == m) {     //只有一个元素
        for (int i = 0; i <= m; i++) {
            System.out.print(a[i]+" ");
        }
        System.out.println();
    }else{          //还有多个元素,递归产生排列
        for (int i = k; i <= m; i++) {
            swap(a,k,i);                //排列到这个元素就要将其放在第一个位置
            perm(a,k+1,m);
            swap(a,k,i);                //从此出口出去后还需要将刚刚调换的位置换回来
        }
    }
}

下面是递归这块的最后一个例题了,整数划分问题。

整数划分

说明一下问题,什么是整数划分?

  • n=m1+m2+…+mi; (其中mi为正整数,并且1 <= mi <= n),则{m1,m2,…,mi}为n的一个划分。
  • 如果{m1,m2,…,mi}中的最大值不超过m,即max(m1,m2,…,mi)<=m,则称它属于n的一个m划分。这里我们记n的m划分的个数为f(n,m);
  • 举个例子,当n=5时我们可以获得以下这几种划分(注意,例子中m>=5)
    5 = 5
    = 4 + 1
    = 3 + 2
    = 3 + 1 + 1
    = 2 + 2 + 1
    = 2 + 1 + 1 + 1
    = 1 + 1 + 1 + 1 + 1
    编写程序,输入整数n,m,返回n的所有m的划分个数。

算法思路:我们用q(n,m)表示将n用不大于m的数字划分的方法的个数

  • 1、n = 1时:只有一种划分法就是1
  • 2、m = 1时:也只有一种划分法就是n个1相加
  • 3、n < m时: 划分的方法也就只限于q(n,n)了,毕竟比n大的数也取不到嘛(不能取负数,要不然无限多了)
  • 4、n = m时:就是1+(m-1)这一种情况加q(n,m-1)即1+q(n,m-1)。比如q(6,6)就是1+q(6,5)
  • 5、n > m时:这种情况下又包含两种情况:
      5(1)、划分中包含m时:即{m, {x1,x2,…xi}}(它们之和为n), 其中{x1,x2,… xi} 的和为n-m,所以就是n-m的m划分,即q(n-m,m)
      5(2)、划分中不包含m时:划分中所有的值都比m小,即q(n,m-1)
  • 因此第5中情况的划分为q(n-m,m)+1(n,m-1)
  • 对第2中举例子详述:q(5,3):
      (1)包含3: 1+1+3; 2+3; 既然每种情况都包含了3,那去掉3对其余各数相加为(5-3=)2的划分的个数和其相等,那就是对2(m=3)的划分了
      (2)不包含3: 1+1+1+1+1; 1+1+1+2; 1+2+2;

第一步,明确函数输入和返回

public static int equationCount(int n, int m){

}

第二步,明确递归终止条件

public static int equationCount(int n, int m){
        if (n < 1 || m < 1)
            return 0;
        if(n == 1 || m == 1)
            return 1;
}

第三步,寻找递归关系

public static int equationCount(int n, int m){
        if (n < 1 || m < 1)
            return 0;
        if(n == 1 || m == 1)
            return 1;
        if(n < m)
            return equationCount(n,n);
        if(n == m)
            return equationCount(n,m-1)+1;
        return equationCount(n-m,m)+equationCount(n,m-1);   //n > m的情况
}

分治策略

分治策略的基本思想就是将一个规模为n的问题分解成k个规模较小的子问题,这些子问题互相独立且与原问题相同。递归的解这些子问题,然后将子问题的解合并得到原问题的解,和这种说法最贴切的就是我们之前一篇文章介绍的归并排序法了,这篇文章里我们还会再引出一遍。

我们将分治策略解决问题的步骤归纳为:将大问题分解成子问题,分别解决子问题,再将子问题的解合并成大问题的解.
先看第一个典型的例子——归并排序

归并排序

这里我们对归并排序主要注重体现它分治策略的算法逻辑,而不过多深究这个排序算法是如何执行的,具体的图解归并排序请移步我的另一篇博文——数据结构之——八大排序算法。
归并排序的思想是,先将数组分割成为一个个小数组,直到每个小数组中只含有一个元素,那么在这一个小数组里面,这一个元素自然就是有序的,然后将其合并起来(由merge函数实现),按从小到大的顺序,逐层向上,就是将小问题的解合并为大问题的解。

下面是将大问题分解成小问题的过程

/**
 * 只要数组的大小不为1,就一直分割,直到不能分割为止(即数组长度为1),
 * 不能分割后按照出入栈顺序,会将分割的小数组分别排序后归并起来
 * @param data      待排序数组
 * @param start     起始位置
 * @param end       终止位置
 */
public static void merge_sort(int data[], int start, int end){
    int mid = (start+end)/2;
    if(start < end){
        merge_sort(data,start,mid);
        merge_sort(data,mid+1,end);
        merge(data,start,mid,end);
    }
}

下面是合并小问题的解,归并过程

/**
 * 这个函数是将数组合并在一起的,其实并没有将数组真的分开,只是用start和end指示不同的元素,来达到分割的目的
 * @param  p        指示子数组1的元素
 * @param  q        指示子数组2的元素
 * @param  r        指示合并后数组的元素
 * @param start     start到mid是需要合并的子数组1
 * @param mid
 * @param end       mid+1到end是需要合并的子数组2
 */
private static void merge(int data[], int start, int mid, int end){
    int p = start, q = mid+1, r = 0;
    int newdata[] = new int[end-start+1];
    while(p <= mid && q <= end){
        if(data[p] >= data[q]){                 //从大到小排序
            newdata[r++] = data[p++];
        }else{
            newdata[r++] = data[q++];
        }
    }

    //此时,两个子数组中会有一个中元素还未被全部归并到新数组中,作如下处理
    while(p <= mid){
        newdata[r++] = data[p++];
    }
    while(q <= end){
        newdata[r++] = data[q++];
    }
    //再将有序的数组中的值赋给原数组,其实也可以直接返回这个新数组
    for (int i = start; i <= end; i++) {
        data[i] = newdata[i-start];
    }
}

二分查找

然后是分治策略的另一个经典例子———二分查找,顾名思义,就是在一个有序(从小到大)的数组中查找一个元素的位置,先从最中间将数组变为两个小数组,然后与中间值进行对比,如果相等直接返回,不相等又分两种情况,如果中间元素比待查找值小,就从后半个数组中继续二分查找,反之,从前半个数组中二分查找。

public static int Binary_Search(int []data, int x, int n){   //data为待搜索数组(有序),x为待搜索元素,n为数组大小
    int left = 0, right = n - 1;            //指示左右的两个指示器
    while(left <= right){                   //left可以等于right,因为有可能刚好两个指示器同时指示到了待查找元素上
        int mid = (left+right)/2;
        if(data[mid] > x)
            right = mid-1;
        else if(data[mid] < x)
            left = mid+1;
        else    return mid;
    }
    return -1;           //表示查找失败
}

棋盘覆盖

下面我们逐渐加大难度,接下来这个问题叫做棋盘覆盖,我们先简单介绍一下这个问题。

在一个2^k × 2^k (k≥0)个方格组成的棋盘中,恰有一个方格与其他方格不同,称该方格为特殊方格。显然,特殊方格在棋盘中可能出现的位置有4k种,因而有4k种不同的棋盘,图4.10(a)所示是k=3时64种棋盘中的一个。棋盘覆盖问题(chess cover problem)要求用图4.10(b)所示的4种不同形状的L型骨牌覆盖给定棋盘上除特殊方格以外的所有方格,且任何2个L型骨牌不得重叠覆盖。

在这里插入图片描述

图4.10(a)

在这里插入图片描述

图4.10(b)
在这里为了方便讲解,我们采用k=2时候的情况来说明这个问题,设初始情况为

在这里插入图片描述

第一次将其分割成四个小块,分成了四个子棋盘,以黄线为分割线

在这里插入图片描述

然后分别对其进行填充

在这里插入图片描述

填充完后,又可以将其分割

在这里插入图片描述

重复上述填充操作,即可对所有方格填充

在这里插入图片描述

当k更大的时候的过程可以参考这位大佬的博客棋盘覆盖问题,接下来我们用代码实现。

static int board[][] = new int[4][4];   //棋盘
static int tag = 1;                     //骨牌编号
/**
 * 分治算法典例2———棋盘覆盖问题
 * @date    2019/11/3   afternoon
 * @param tr    棋盘左上角方格的行号
 * @param tc    棋盘左上角方格的列号
 * @param dr    特殊方格所在的行号
 * @param dc    特殊方格所在的列号
 * @param size  棋盘宽度
 * @param s     当前棋盘宽度的一半
 * @param tr+s  当前棋盘中间行的行号
 * @param tc+s  当前棋盘中间列的列号
 */
public static void chess(int tr, int tc, int dr, int dc, int size){
    if(size == 1)
        return;
    int newtag = tag++;
    int s = size / 2;     //分割棋盘

    //覆盖左上角子棋盘
    if(dr < tr+s && dc < tc+s){ //特殊方格在此棋盘中
        chess(tr,tc,dr,dc,s);
    }else{      //此棋盘中无特殊方格
        board[tr+s-1][tc+s-1] = newtag;
        chess(tr,tc,tr+s-1,tc+s-1,s);
    }

    //覆盖右上角子棋盘
    if(dr < tr+s && dc >= tc+s){
        chess(tr,tc+s,dr,dc,s);
    }else{
        board[tr+s-1][tc+s] = newtag;
        chess(tr,tc+s,tr+s-1,tc+s,s);
    }

    //覆盖左下角子棋盘
    if(dr >= tr+s && dc < tc+s){
        chess(tr+s,tc,dr,dc,s);
    }else{
        board[tr+s][tc+s-1] = newtag;
        chess(tr+s,tc,tr+s,tc+s-1,s);
    }

    //覆盖右下角子棋盘
    if(dr >= tr+s && dc >= tc+s){
        chess(tr+s,tc+s,dr,dc,s);
    }else{
        board[tr+s][tc+s] = newtag;
        chess(tr+s,tc+s,tr+s,tc+s,s);
    }
}

接下来的问题依然有一些难度,叫做打印日程表问题。

日程表问题

问题:设有n=2^k个选手参加循环赛,要求设计一个满足以下要求比赛日程表:

1)每个选手必须与其它n-1个选手各赛一次;

2)每个选手一天只能赛一次。

分析,按照上面的要求,可以将比赛表设计成一个n行n-1列的二维表,其中第i行第j列的元素表示和第i个选手在第j天比赛的选手号。

采用分治策略,可将所有参加比赛的选手分成两部分,n=2^k 个选手的比赛日程表就可以通过n=2^(k-1) 个选手的的比赛日程表来决定。递归的执行这样的分割,直到只剩下两个选手,比赛日程表的就可以通过这样的分治策略逐步构建。
说个大白话就是:先默认构造日程表第一行,即0,1,2,3,…然后先分割日程表,将左上角复制到右下角,右上角复制到左下角

初始化第一行不做赘述,让chess[0][i] = i+1即可

下面在Excel中用图示做一演示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

代码实现

/**
 * 将比赛日程表设计成n行n列,表中除了第一列,其他n-1列才是我们要的,数组下标行列都从0开始,第i行j列代表第(i+1)位选手在第j天的对手:
 * 表格初始化会将第一行按1到n一次填充,然后递归填充下面的,用左上角和右上角分别去填充右下角和左下角,因为要是对称矩阵(具体原因好好想想)
 * @param p     表示行序号
 * @param q     表示列序号
 * @param t     表示当前传进函数方格的规模也就是大小
 * @param arr   表格
 */
public static void arrange(int p, int q, int t, int arr[][]){
    if(t>=4){           //如果规模大于4,就继续递归
        arrange(p,q,t/2,arr);
        arrange(p,q+t/2,t/2,arr);
    }

    //填左下角
    for(int i=p+t/2;i<p+t;i++){
        for(int j=q;j<q+t/2;j++){
            arr[i][j]=arr[i-t/2][j+t/2];
        }
    }
    //填右下角
    for(int i=p+t/2;i<p+t;i++){
        for(int j=q+t/2;j<q+t;j++){
            arr[i][j]=arr[i-t/2][j-t/2];
        }
    }
}

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/27753.html

(0)

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信