【PKU算法课0x01】递归

什么是递归?

所谓递归,就是一个函数调用其自身。注意,递归也属于函数调用,递归和普通的函数调用并没有什么区别。
递归的重要性…怎么说呢?大概相当于好好吃饭好好睡觉对于健康的重要性吧。

如何识别递归?

当你觉得用正常的迭代一头雾水的时候,当你看不明白题意的时候,多画几项。

递归的特点

  • 要有递归出口
  • 有可以使当前状态向递归出口状态逼近的递推

递归是自己调用自己,根据算法的有穷性,总要达到一个条件终止递归。所以递归的两大特点就是以上

递归的调用细节

函数调用是通过栈来实现的。栈是专门用来调用函数用的。程序调用一个函数,栈就会向上生长一层。 那么,新生长出来的一层栈里放了些啥?包括 这次函数调用的形参、这次调用在执行期间的局部变量、这次函数调用的返回地址。
在函数调用结束的时候栈顶上还会放着这次函数调用的返回值。

递归解决这些问题

  • 代替多重循环,或者层数不定的循环。
  • 解决本来就是用递归形式定义的问题。
  • 把问题分解为规模更小的子问题来求解。

举个栗子。
代替多重循环:n皇后问题,你总不能未卜先知先写n重循环吧?所以只能用递归
解决递归形式定义的问题:很明显的可以看出来使递归定义的。比如求表达式的值。1+2*(3-1)是一个表达式,而表达式中的一部分,(3-1)也是一个表达式。所以说这个题目就是递归形式定义的问题。
把问题分解为规模更小的问题:走楼梯,放苹果。 该类题目的特点是先尝试着走出第一步,然后就会发现规律了。注意,我们要学会分类,在递归中,分类是很重要的思想。

代码们:
AC代码,所有题目的,最后贴出来。

代替多重循环

n皇后问题

问题描述:
输入整数n, 要求n个国际象棋的皇后,摆在n*n的棋盘上,互相不能攻击,输出全部方案。

说明:
输入一个正整数N,则程序输出N皇后问题的全部摆法。输出结果里的每一行都代表一种摆法。行里的第i个数字如果是n,就代表第i行的皇后应该放在第n列。
皇后的行、列编号都是从1开始算。

样例输入:

4

样例输出:

2 4 1 3
3 1 4 2

问题分析:
多重循环是不靠谱的,因为我们不知道到底要写多少重循环。
我们只能用n来表示最大递归层数来解决。递归层数到n了,代表不要再循环了。

递归形式定义的问题

波兰式

问题描述:
波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式2 + 3的波兰表示法为+ 2 3。波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,例如(2 + 3) * 4的逆波兰表示法为* + 2 3 4。本题求解逆波兰表达式的值,其中运算符包括+ - * /四个。

输入:
输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数

输出:
输出为一行,表达式的值。

样例输入:

* + 11.0 12.0 + 24.0 35.0

样例输出:

1357.000000

提示:

(11.0+12.0)*(24.0+35.0)

tips:
波兰式定义:

  • 一个数是一个波兰表达式,值为该数。
  • “运算符 波兰表达式 波兰表达式” 是波兰表达式 ,值为两个波兰表达式的值运算的结果。

问题分析:
咦,从这儿我们就可以看出来,这是递归定义的一个概念。有递归出口(第一个条件),有递归递推(第二个条件)
我们可以把波兰式转化为递推式:

  • 如果读到的串为数字, 则返回该数字。
  • 如果读到的串为运算符,那么返回两个波兰表达式运算后的结果。

四则运算表达式求值

问题描述:

输入为四则运算表达式,仅由整数、+、-、*、/ 、(、)组成,没有空格,要求求其值。假设运算符结果都是整数。"/"结果也是整数。

问题分析:

表达式的值等于各项做加减之后的值。
每一项的值是若干因子相乘或者相除之后的结果。
因子:要么是一个让括号括起来的表达式, 要么是一个数

所以我们看到,这题目依然是递归定义的(有没有发现基本所有的运算类题目都是递归定义的)。

把问题分解为更小的子问题

求阶乘

问题描述:
输出n的阶乘。

问题分析:

n! = 1*2*3*4*...*n = n*(n-1)*.....*1

特别注意,0的阶乘等于1.
我们可以发现,n! = n * (n-1)! . 我们把问题分解为了更小的子问题。
所以我们可以写出递推式:
return 1 n = 0
return n * (n-1)! n > 0

汉诺塔

问题描述:
古代有一个梵塔,塔内有三个座A、B、C,A座上有64个盘子,盘子大小不等,大的在下,小的在上(如图)。有一个和尚想把这64个盘子从A座移到C座,但每次只能允许移动一个盘子,并且在移动过程中,3个座上的盘子始终保持大盘在下,小盘在上。在移动过程中可以利用B座,要求输出移动的步骤。
在这里插入图片描述
问题分析:
好麻烦啊,这尼玛怎么移动?
我们可以先把第1到n-1个盘子看成一个整体。所以程序就成了把n-1个盘子从A借助C移动到B---->把第n个盘子直接移动到C------>把1到n-1个盘子从B借助A移动到C。
这样我们就成功的把移动n个盘子缩减到了挪动n-1个盘子。
然后递归退出的条件是什么?剩下1个盘子时,我们只需要直接把它移动到目的地即可。

想到这里就可以了,我们不要瞎想,递归程序最忌想的过多。

所以我们设计算法:
如果n==1, 则直接挪动。
否则的话,先把n-1个盘子从A借助C移动到B
再把第n个盘子直接移动到C
再把1到n-1个盘子从B借助A移动到C

爬楼梯

问题描述:

树老师爬楼梯,他可以每次走1级或者2级,输入楼梯的级数,求不同的走法数
例如:楼梯一共有3级,他可以每次都走一级,或者第一次走一级,第二次走两级,也可以第一次走两级,第二次走一级,一共3种方法。

输入:
输入包含若干行,每行包含一个正整数N,代表楼梯级数,1<= N <= 30输出不同的走法数,每一行输入对应一行

输出:
不同的走法数,每一行输入对应一行输出

样例输入:

5
8
10

样例输出:

8
34
89

问题分析:
看上去问题挺复杂的。我们先看如何登顶以缩小规模:第n级台阶的来源是从第n-1级台阶走了一步走过来的,也可能是从第n-2级台阶走了两步走过来的。所以我们成功的缩小了问题的规模:
f(n) = f(n-1) + f(n-2)
递归是要有退出条件的,这里的退出条件是:
如果走1级台阶,只有1种走法。 0+1
走2级台阶,我们有2种走法。0+1+1 0+2
所以写出递推式:
f(n) = f(n-1) + f(n-2) n > 2
f(1) = 1
f(2) = 2

当然啦,这题也可以这么想:我们先走第一步。第一步我们可以走一步,也可以走两步。 所以说剩下的就是n-1级的走法+n-2级的走法。其实都是一样的。

放苹果

问题描述:
把M个同样的苹果放在N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?5,1,1和1,5,1 是同一种分法。

输入:
第一行是测试数据的数目t(0 <= t <= 20)。以下每行均包含二个整数M和N,以空格分开。1<=M,N<=10。

输出:
对输入的每组数据M和N,用一行输出相应的K

样例输入:

1
7 3

样例输出:

8

问题分析:
注意盘子和苹果都是没有差别的,所以5,1,1和1,5,1是同一种放法。
我们又要来分情况了:有两种情况,一种是盘子比苹果多,一种是盘子不如苹果多。
我们设i个苹果放在k个盘子里放法总数是f(i,k), 则我们可以得知:
f(i,k) = f(i,i) if k > i
因为我们说过,盘子和盘子之间是没有差别的。所以如果k>i,则必然有k-i个空盘子,我们完全不用考虑这几个空盘子。
而对于i >= k 的情况来说,我们有两种放法,一种是有盘子为空的放法,一种是没有盘子为空的放法。

有一个盘子为空的放法中,它肯定至少有一个盘子为空。后续的工作就成了把i个苹果放到k-1个盘子中。

没盘子为空的算法中,由于没有盘子为空,所以我们可以先给每一个盘子都放一个苹果。
总结一下:
在这里插入图片描述
所以边界条件是:
没有盘子时,我们不能放苹果。(即return 0)
没有苹果时,我们相当于不放。(即return 1) 不放也是一种方法哈。

算24

问题描述:
给出4个小于10个正整数,你可以使用加减乘除4种运算以及括号把这4个数连接起来得到一个表达式。现在的问题是,是否存在一种方式使得得到的表达式的结果等于24。
这里加减乘除以及括号的运算结果和运算的优先级跟我们平常的定义一致(这里的除法定义是实数除法)。
比如,对于5,5,5,1,我们知道5 * (5 – 1 / 5) = 24,因此可以得到24。又比如,对于1,1,4,2,我们怎么都不能得到24。

输入:
输入数据包括多行,每行给出一组测试数据,包括4个小于10个正整数。最后一组测试数据中包括4个0,表示输入的结束,这组数据不用处理。

输出:
对于每一组测试数据,输出一行,如果可以得到24,输出“YES”;否则,输“NO”。

样例输入:

5 5 5 1
1 1 4 2
0 0 0 0

样例输出:

YES
NO

问题分析:
我们拿到这题之后,已经懵了,我们发现有好多好多可能的组合,还牵扯到(),这就很烦人了。我们尝试缩减问题的规模:我们现在尝试的是用4个数算24,我们可不可以把问题规模降低到用3个数算24呢? 我们只需要将2个数先算24,然后把他们的运算结果当做新的数参与到算24中。

n个数算24,必有两个数要先算。这两个数算的结果,和剩余n-2个数,就构成了n-1个数求24的问题。

我们要做的事情:
枚举先算的两个数,以及这两个数的运算方式。

那么边界条件是什么?
如果后来只剩下了一个数,它就是24的话,那么就返回true,否则返回false.

为什么我们不需要考虑括号问题呢?
因为括号的意义就是保证了运算的优先级,我们对所有的数进行了枚举,其实就是间接的保证了运算的优先级。
比如对于5 * (5 – 1 / 5) = 24来说,我们迭代时总能选出来1和5,总能得到1/5。然后5和1/5的结果总能做到减法运算,而括号内的结果总能和5得到乘法运算,所以我们的算法不考虑括号是合理的。

所有题目的AC代码

package program;

import java.math.*;
import java.io.*;
import java.util.*;
public class Main {

    static int N; // 代表了N皇后
    static int count;
    static Scanner scanner = new Scanner(System.in);
    // queenPos用下标代表皇后所在行,用实际的元素值代表了皇后所在列
    int[] queenPos = new int[100];
    String expString = "1+2*3/(2-1)";
    public static void main(String[] args){
        Main main = new Main();
//        int n = scanner.nextInt();
//        main.Hanoi(n,'A','B','C');
//        N = scanner.nextInt();

//        main.nQueen(0);
//        System.out.println(count);


//        System.out.println(main.exp());
//        System.out.println(main.expressionValue());
        if(main.count24(new double[]{1,1,4,2},4)){
            System.out.println("xxxxx");
        }
    }
    private int jieCheng(int n){
        if(n == 0){
            // 退出条件
            return 1;
        }
        // 正确缩小递归规模
        return n*this.jieCheng(n-1);
    }
    private void Hanoi(int n, char src, char mid, char dest){
        /*
        * 算法:只有一个盘子时,直接挪动到目的地
        * 当有很多个盘子时,先将n-1个盘子通过dest挪动到mid
        * 然后把第n个盘子直接挪动到目的地
        * 然后把剩下n-1个盘子从mid通过src挪到dest上。
        *
        * */
        if(n == 1){
            // 递归退出条件
            System.out.printf("Move disk %d from %c to %c\n",n,src,dest);
            return;
        }
        // 正确缩小递归规模
        // 从src通过dest挪动到mid上
        this.Hanoi(n-1,src,dest,mid);
        // 第n个盘子直接挪动到目的地
        System.out.printf("Move disk %d from %c to %c\n",n,src,dest);
        this.Hanoi(n-1,mid,src,dest);
    }
    private void nQueen(int k){
        if(k == N){
            // 递归退出条件
            ++count; // 对解法进行计数

            // 打印
            for(int i = 0; i < N; ++i){
                if(i == 0){
                    System.out.print(queenPos[i]+1);
                }else{
                    System.out.print(" ");
                    System.out.print(queenPos[i]+1);
                }
            }
            System.out.println();
            return;
        }
        for(int i = 0; i < N; ++i){
            // 尝试将第k个皇后放在第i列
            int j;
            // 与之前摆好的k-1个皇后进行冲突检测
            for(j = 0; j < k; ++j){
                // 如果在j行i列正好有皇后,或者是j行的皇后与当前待选的皇后的位置在对角线上
                // 对角线的定义是,横着等于竖着,sin45.
                if(queenPos[j] == i || Math.abs((k-j)) == Math.abs(queenPos[j]-i)){
                    // 如果发生冲突,则退出内层循环,进行下一次循环
                    break;
                }
            }
            if(j == k){
                // 证明不是break出来的,跟之前的k-1个皇后没有冲突。
                // 所以可以把第k个皇后放在第i列
                queenPos[k] = i;
                // 正确缩减递归规模
                this.nQueen(k+1);
            }
        }
    }
    private double exp(){
        String s = scanner.next(); //next读取被空格截断的字符串
        switch (s.charAt(0)){
            case '+': return exp() + exp();
            case '-': return exp() - exp();
            case '*': return exp() * exp();
            case '/': return exp() / exp();
            default: return Double.parseDouble(s);
        }
    }
    private int expressionValue(){
        // 函数功能:读入一个表达式,返回它的值

        int result = this.termValue();
        boolean more = true;
        while(more){
            if(expString.length() == 0){
                break; // 确保字符串长度是有效的
            }
            if(expString.charAt(0) == '+' || expString.charAt(0) == '-'){
                // 读取操作符
                char op = expString.charAt(0);
                // 将操作符弹出字符串
                expString = expString.substring(1);
                // 读取下一个项
                int value = this.termValue();

                // 进行操作
                if(op == '+'){
                    result += value;
                }else{
                    result -= value;
                }
            }else{
                // 后面没有项了,直接退出循环
                more = false;
            }
        }
        return result;
    }
    private int termValue(){
        // 读入一项,返回它的值
        int result = this.factorValue();
        boolean more = true;
        while(more){
            if(expString.length() == 0){
                break;
            }
            if(expString.charAt(0) == '*' || expString.charAt(0) == '/'){
                char op = expString.charAt(0);
                expString = expString.substring(1);
                int value = this.factorValue();
                if(op == '*'){
                    result *= value;
                }else{
                    result /= value;
                }
            }else{
                more = false;
            }
        }
        return result;
    }
    private int factorValue(){
        // 读入因子,返回其值
        int result = 0;
        if(expString.length() == 0){
            return result;
        }
        char c = expString.charAt(0);
        if(c == '('){
            // 对因子是小括号括起来的情况进行处理
            expString = expString.substring(1);
            // 括号中间是一个表达式
            result = this.expressionValue();
            // 将右括号弹出字符串
            expString = expString.substring(1);
        }else{
            // 因子是数字的情况
            while('0'<=c&&c<='9'){
                result = result*10 + c - '0';
                expString = expString.substring(1);
                if(expString.length() == 0){
                    return result;
                }
                c = expString.charAt(0);
            }
        }
        return result;
    }
    private int stairs(int n){
        if(n == 1){
            return 1;
        }else if(n == 2){
            return 2;
        }
        return this.stairs(n-1)+this.stairs(n-2);
    }
    private int putApple(int i,int k){
        if(k > i){
            this.putApple(i,i);
        }
        if(i == 0){
            // 没有苹果
            return 1;
        }
        if(k == 0){
            //没有盘子
            return 0;
        }
        // 有一个盘子为空的情况 + 没有盘子为空的情况
        return this.putApple(i,k-1) + this.putApple(i-k,k);
    }
    private boolean count24(double[] arr,int n){
        if(n == 1){
            if(isEquals(arr[0],24)){
                return true;
            }
            return false;
        }
        // 取arr[i],arr[j] 运算
        // 取剩下的n-2个数存储在b中
        // arr[i] 与 arr[j]的运算结果存储在b[n-2]中。
        double[] b = new double[5];
        for(int i = 0; i < n-1; ++i){ // 枚举两个待选数的所有组合
            for(int j = i+1; j < n; ++j){
                int m = 0; // 计数器
                // 找到未被选中的n-2个数并存储
                for(int k = 0; k < n; ++k){
                    if(k!=i && k!=j){
                        b[m++] = arr[k];
                    }
                }
                // 枚举所有的运算
                b[m] = arr[i]+arr[j]; // 加法谁在前谁在后都是一样的
                if(this.count24(b,n-1)){
                    return true;
                }
                b[m] = arr[i]-arr[j]; // 减法就不一样了,所以有两个减法
                if(this.count24(b,n-1)){
                    return true;
                }
                b[m] = arr[j]-arr[i];
                if(this.count24(b,n-1)){
                    return true;
                }
                b[m] = arr[i]*arr[j]; // 同加法一样
                if(this.count24(b,n-1)){
                    return true;
                }
                // 除法同减法。
                if(!this.isEquals(arr[j],0)){
                    // 保证除法能运算
                    b[m] = arr[i]/arr[j];
                    if(this.count24(b,n-1)){
                        return true;
                    }
                }
                if(!this.isEquals(arr[i],0)){
                    b[m] = arr[j]/arr[i];
                    if(this.count24(b,n-1)){
                        return true;
                    }
                }
            }
        }
        return false;
    }
    private boolean isEquals(double a,double b){
        if(Math.abs(a-b) < 1e-6){
            return true;
        }
        return false;
    }
}

SDUT 递归练习

青蛙过河

问题描述:
1)一条小溪尺寸不大,青蛙可以从左岸跳到右岸,在左岸有一石柱L,石柱L面积只容得下一只青蛙落脚,同样右岸也有一石柱R,石柱R面积也只容得下一只青蛙落脚。 2)有一队青蛙从小到大编号:1,2,…,n。 3)初始时:青蛙只能趴在左岸的石头 L 上,按编号一个落一个,小的落在大的上面-----不允许大的在小的上面。 4)在小溪中有S个石柱、有y片荷叶。 5)规定:溪中的每个石柱上如果有多只青蛙也是大在下、小在上,每个荷叶只允许一只青蛙落脚。 6)对于右岸的石柱R,与左岸的石柱L一样允许多个青蛙落脚,但须一个落一个,小的在上,大的在下。 7)当青蛙从左岸的L上跳走后就不允许再跳回来;同样,从左岸L上跳至右岸R,或从溪中荷叶、溪中石柱跳至右岸R上的青蛙也不允许再离开。 问题:在已知小溪中有 s 根石柱和 y 片荷叶的情况下,最多能跳过多少只青蛙?

输入:
输入数据有多组,每组占一行,每行包含2个数s(s是小溪中的石柱数目)、y(y是小溪中的荷叶数目)。(0 <= s <= 10,0 <= y <= 10),输入文件直到EOF为止!

输出:
对每组输入,输出有一行,输出最多能跳过的青蛙数目。

示例输入:

0 2
1 2

示例输出:

3
6

问题分析:
这题恕我愚钝,是靠画图+推测做出来的。
首先明确一点,这题目分成了3块区域,分别是L,M,R三块区域。从L只能到M和R, 不能回头。而M到R,也不能回头。但是M之间是可以随便跳的,题目并没有做限制。
所以这个题目我们可以理解为:
第一个跳到R的青蛙的值就代表了能跳过去的青蛙的总数了。这个题目看起来和汉诺塔很像,但是其实并不是汉诺塔。因为我们相当于是直接把最大的盘子弄到了R,它就不能再动了。所以这题求得是中间河道最多能容纳多少只青蛙。

当河道没有柱子时,河道的青蛙容量为荷叶数。
当河道有一个柱子时,河道的青蛙容量为(荷叶数+1)+荷叶数。 荷叶数+1是什么?想一下,我们先把所有荷叶都弄满青蛙,然后把下一只青蛙弄到河道的柱子上,然后各个荷叶的青蛙都跳到柱子上,这就是柱子的容量。再加上荷叶的容量,就是总的容量了。
当河道有2个柱子时,河道的青蛙容量为[(荷叶数+1)+荷叶数+1] + (荷叶数+1)+荷叶数,。
为什么?因为我们可以利用上次的结果,我们把上次满载青蛙之后,从L直接跳到R的青蛙可以让它跳到新增的柱子上,然后让荷叶上的青蛙跳到它上面,然后完成把第一根柱子上的青蛙全部挪动到它上面,实现了对上一个柱子和荷叶的清空,而且还多比上次的结果多容纳了一个青蛙,因为L->R的青蛙直接跳在了这根柱子上,然后这些荷叶和柱子所能容纳的青蛙在上一步已经算出来了,我们可以轻松得到满载。

最后我们看出来规律了,多一根柱子的河道的容量为:2*少一个柱子的河道的容量+1。而我们能跳过去的总数相当于河道容量+1. 所以说之前的公式,+1就可以免了,算出来直接就是青蛙数。 所以我们可以轻松写出递归条件:

if(s == 0)
	return y+1; // 荷叶+1
else
	return this.func(s-1,y)*2

最终的AC代码:

import java.util.*;
public class Main {
    public static void main(String[] args){
        Main main = new Main();
        int m,n;
        Scanner scanner = new Scanner(System.in);
        while(true){
            m = scanner.nextInt();
            n = scanner.nextInt();
            System.out.println(main.jump(m,n));
        }
    }
    private int jump(int s,int y){

        if(s==0)
            return y+1;
        else
            return 2*this.jump(s-1,y);
    }
}

发布了333 篇原创文章 · 获赞 22 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/weixin_41687289/article/details/104127088