算法思想(递归)——汉诺塔问题+棋盘分割+小游戏【POJ2802】+算24【POJ2787】

        生活中不知道大家有没有遇见这样得例子:你站在两面相对的镜子之间,那么就可以在其中一面的镜子中看到自己,同一时间镜子中的自己后面又有一面镜子,镜子的里面又是自己,就这样反反复复叠下去…………(无穷无尽)

        又例如家庭第一年拍了一次团圆照,然后第二年拿着第一年的照片拍,第三年拿着第二年的照片拍,这样就会出现照片里的人拿的照片里的人也拿着照片…………(有终点) 

递归从程序设计的角度来看,实际上就是让程序自己调用自己的一种编程技巧 

目录

阶乘和斐波那契数列(代码和思路)

 阶乘代码

斐波那契数列代码

 递归的好处和弊端

递归与枚举的区别 

汉诺塔问题

棋盘分割

小游戏【POJ2802】

算24(POJ 2787)


        程序中递归的表示一个过程或函数在其定义或说明中直接或间接的调用自身的一种方法,通常将一个大型复杂的问题转换为一个与原问题相似的规模较小的问题来解决。由于与原问题相似,所以规模较小的问题依旧可以用原来的算法来解决,并且更加容易地被求解,最后再由这些子问题的解来构成最终问题的解。

阶乘和斐波那契数列(代码和思路)

        构成递归的条件:(一):子问题和原始问题要执行的操作是一致的,并且通常来说子问题的规模更小,也更为简单。(二):不能无限的调用自身,需要一个出口,不然就是一个死循环。

        关于递归有两个十分典型的例子:(一):求阶乘 。(二):斐波那契数列

(一):4!=4*3!        3!=3*2!        2!=2*1!        1!=1*0!

满递归构成的条件,求4!可以先求3!在*4;求3!可以先求2!,所以4!=4*3!=4*3*2!=4*3*2*1!=4*3*2*1*1

(二):1        1        2        3        5        8        13        …………

可知第三个等于第二个加第一个;第四个等于第三个加第二个;第五个等于第四个加第三个…………,到第二个的时候结束。

 阶乘代码

//阶乘
public static void main(String[] args)
    {
        System.out.println(jichen(5));
    }
    public static int jichen(int num)//用递归
    {
        int sum=1;
        if(num==1) {
            return 1;//跳出循环
        }else {
            sum=num*jichen(num-1);//递归
            return sum;
        }
    }

    public static void f1(int n)//用循坏
    {
    int result = 1;
    for(int i = 2; i <= n; i++)
    {
    result *= i;
    }
    System.out.println(n + "的阶乘是" + result);
    }

斐波那契数列代码

斐波那契数列最关键的代码就是——   f(n)=f(n-1)+f(n-2)

//斐波那契数列
public class FeiBoNaQieShu
{
    public static void main(String[] args)
    {
        Scanner scanner = new Scanner(System.in);
        int N = scanner.nextInt();
        int[] a = new int[N+1];
        a[1]=1;
        a[2]=1;
        System.out.println(a[1]);
        for (int i = 2; i <= N; i++)
        {
            System.out.println(f(i));
        }
    }

    private static int f(int i)
    {
        if (i==1 || i==2)
            return 1;
        return f(i-1)+f(i-2);
    }
}

 递归的好处和弊端

        我们可以看得出来,递归写的代码十分简洁,消除很多的冗余,但是使得代码会变得不是很直观,不是很容易理解,有点云里雾里的(当然对于大佬来说只会更加易于理解)。

        代码简洁和思路简洁所带来的就是运行效率的降低。递归算法解题相对于常用的算法来说是比较低的。因此,应该尽量避免使用递归,除非没有更好的算法,有或者在某种特定的情况下更为合适。

递归与枚举的区别 

递归与枚举的区别

枚举: 也可以看成是把一个问题划分成一组子问题,依次对这些子问题求解 子问题之间是横向的,同类的关系

递归: 把一个问题逐级分解成子问题 子问题与原问题之间是纵向的,同类的关系

汉诺塔问题

        相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置64个金盘(如图1)。游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

        分析:对于这样一个问题,任何人都不可能直接写出移动盘子的每一步,但我们可以利用下面的方法来解决。设移动盘子数为n,为了将这n个盘子从A杆移动到C杆,可以做以下三步:

(1)以C盘为中介,从A杆将1至n-1号盘移至B杆;

(2)将A杆中剩下的第n号盘移至C杆;

(3)以A杆为中介;从B杆将1至n-1号盘移至C杆。

这样问题解决了,但实际操作中,只有第二步可直接完成,而第一、三步又成为移动的新问题。以上操作的实质是把移动n个盘子的问题转化为移动n-1个盘,那一、三步如何解决?

        事实上,上述方法设盘子数为n, n可为任意数,该法同样适用于移动n-1个盘。因此,依据上法,可解决n -1个盘子从A杆移到B杆(第一步)或从B杆移到C杆(第三步)问题。

        现在,问题由移动n个盘子的操作转化为移动n-2个盘子的操作。依据该原理,层层递推,即可将原问题转化为解决移动n -2、n -3… … 3、2,直到移动1个盘的操作,而移动一个盘的操作是可以直接完成的。至此,我们的任务算作是真正完成了。

        而这种由繁化简,用简单的问题和已知的操作运算来解决复杂问题的方法,就是递归法。

 你会发现入宫有四个盘中,那就是将上面三个移动到第三根柱子(C柱),然后将第四个盘中移动到(B柱),然后再用上述同样的方法将(C柱)的三个盘中移动到(B柱),就完美完成任务。不过将思路实现出来,写成代码是有一些难度,多理解理解一下过程。

public class HanLuoTa
{
    public static void main(String[] args)
    {
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入盘子数量");
        int n = scan.nextInt();
        deal(n, "A", "B", "C");
    }
    private static void deal(int n, String left, String mid, String right)
    {
        if (n == 0)//结束条件
            return;
        
        // 把 n - 1 个盘子从left借助right,移动到mid
        deal(n - 1, left, right, mid);

        String step = left + " -> " + right; // 把剩下的一个从left移动到right

        System.out.println(step);

        // 把中间 n - 1个盘子,从mid,借助left,移动到right
        deal(n - 1, mid, left, right);
    }
}

棋盘分割

        将一个8*8的棋盘进行如下分割:将原棋盘割下一块矩形棋盘并使剩下部分也是矩形,再将剩下的部分继续如此分割,这样割了(n-1)次后,连同最后剩下的矩形棋盘共有n块矩形棋盘。(每次切割都只能沿着棋盘格子的边进行)。
        原棋盘上每一格有一个分值,一块矩形棋盘的总分为其所含各格分值之和。现在需要把棋盘按上述规则分割成n块矩形棋盘,并使各矩形棋盘总分的均方差最小。

 一刀分两份,一份扔掉,一份继续切。代码现实如下:

public class FenGe
{
    //声明一个数组用来当棋盘
    static int[][] s = new int[9][9];
    //存放每个格子的分数
    static int[][] sum = new int[9][9];
    /* 用于存放递归式计算结果的记录表 */
    static int[][][][][] res = new int[15][9][9][9][9];

    public static void main(String[] args)
    {
        fillRes(res);
        // System.out.println(Arrays.deepToString(res));
        Scanner scan = new Scanner(System.in);
        System.out.println("请输入您要将棋盘切成的份数");
        int n = scan.nextInt();
        // 给棋盘的格子赋值
        for(int i = 1; i < 9; i++)
        {
            System.out.println("请输入第" + i + "行的8个数");
            String str = scan.next();
            for(int j = 1, rowSum = 0; j < 9; j++)
            {
                s[i][j] = Integer.parseInt(String.valueOf(str.charAt(j - 1)));
                rowSum += s[i][j];
                sum[i][j] = sum[i - 1][j] + rowSum;
            }
        }
        // 开始计算最小均方差
        double result = n * fun(n, 1, 1, 8, 8) - sum[8][8] * sum[8][8];
        result = Math.sqrt(result / (n * n));  // 这个是最小均方差,但没有舍去位数
        String s_result = String.valueOf(result);
        BigDecimal bd = new BigDecimal(s_result);
        result = bd.setScale(3, BigDecimal.ROUND_HALF_EVEN).doubleValue();
        System.out.println("result -> " + result);
    }

    /**
     * 给记录表的空间赋初值
     * @param res 记录表
     */
    private static void fillRes(int[][][][][] res)
    {
        for(int[][][][] v1 : res)
        {
            for(int[][][] v2 : v1)
            {
                for(int[][] v3 : v2)
                {
                    for(int[] v4 : v3)
                        Arrays.fill(v4, -1);
                }
            }
        }
    }

    /**
     * 计算出(x1, y1) 和 (x2, y2)确定的那个范围的矩形棋盘的分值
     * @param x1 矩形棋盘左上角的x坐标
     * @param y1 矩形棋盘左上角的y坐标
     * @param x2 矩形棋盘右下角的x坐标
     * @param y2 矩形棋盘右下角的y坐标
     * @return 返回矩形棋盘的分值
     */
    private static int calSum(int x1, int y1, int x2, int y2)
    {
        return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
    }

    /**
     * 返回(x1, y1)~(x2,y2)这个大盘被切成n份后,各份的分值的平方和
     * @param n     被切成的份数
     * @param x1    左上角的x坐标
     * @param y1    左上角的y坐标
     * @param x2    右下角的x坐标
     * @param y2    右下角的y坐标
     * @return 被切割的n块矩形盘的分值平方和
     */
    private static int fun(int n, int x1, int y1, int x2, int y2)
    {
        int t, a, b, c, e, min = 1000000;
        // 先去记录表这些参数指定的那个位置看看,有没有已经存放结果,如果有,直接返回结果
        if(res[n][x1][y1][x2][y2] != -1)
            return res[n][x1][y1][x2][y2];
        // 递归的出口,n = 1
        if(n == 1)
        {
            t = calSum(x1, y1, x2, y2);
            res[n][x1][y1][x2][y2] = t * t;
            return t * t;
        }
        // 竖着切看看
        for(a = x1; a < x2; a++)
        {
            c = calSum(a + 1, y1, x2, y2);
            e = calSum(x1, y1, a, y2);
            t = Math.min(fun(n - 1, x1, y1, a, y2) + c * c, fun(n - 1, a + 1, y1, x2, y2) + e * e);
            if(min > t)
                min = t;
        }
        // 横着切看看
        for(b = y1; b < y2; b++)
        {
            c = calSum(x1, b + 1, x2, y2);
            e = calSum(x1, y1, x2, b);
            t = Math.min(fun(n - 1, x1, y1, x2, b) + c * c, fun(n - 1, x1, b + 1, x2, y2) + e * e);
            if(min > t)
                min = t;
        }
        // 将求出来的结果保存到记录表的对应位置
        res[n][x1][y1][x2][y2] = min;
        return min;
    }
}

小游戏【POJ2802】

        编写一个小游戏,游戏在一个分割成w*h个正方形格子的矩形板上进行的,每个正方格子上可以有一张游戏卡片,当然也可以没有。

注意:矩形板左上角的坐标是(1,1)

输入保证这两个游戏卡片所处的位置是不相同的

如果一行上有4个0,表示这组测试数据的结束

如果一行上给出w = h = 0,那么表示所有的输入结束了 

void search(int now_x, int now_y, int end_x, int end_y, int step, int f);

now_x, now_y--当前位置

end_x, end_y--结束位置 

step--表示已经走过的路径数目 

f--表示从上一步走到(now_x, now_y)时的方向

package com.suanfa.DiGui;

import java.util.Arrays;
import java.util.Scanner;

public class XiaoYouXi
{
    /* 最小路径数 */
    static int minStep;

    //用一个数组,代表着行走的四个方向
    static int[][] to = {
   
   {0, 1}, {1, 0}, {0, -1}, {-1, 0}};

    //游戏面板的宽度
    static int w, h;

    /* 根据矩形板的宽高,定义矩形板数组与标记数组 */
    static char[][] board = null;
    static boolean[][] mark = null;

    public static void main(String[] args)
    {
        int boardNum = 0; // 配合输出用的普通变量
        Scanner scan = new Scanner(System.in);
        do {
            System.out.println("请输入矩形板的宽度");
            w = scan.nextInt();
            System.out.println("请输入矩形板的高度");
            h = scan.nextInt();

            if(w == 0 && h == 0)
                break;

            boardNum++;
            System.out.println("第 #" + boardNum+" 组数据");

            board = new char[h + 2][w + 2];//最外面多两层,来模拟路径短暂离开面板
            mark = new boolean[h + 2][w + 2];

            // 让用户输入格子上的卡片布局
            for(int i = 1; i <= h; i++)
            {
                System.out.println("请输入第" + i + "行的" + w + "个卡片的存在情况:X表示有卡片,Y表示没有");
                        String str = scan.next();
                for(int j = 1; j <= w; j++)
                {
                    if(str.charAt(j - 1) == 'X')
                        board[i][j] = 'X';
                    else
                        board[i][j] = ' ';
                }
            }

            // 将board的四外圈赋值为' '
            for(int i = 0; i < h + 2; i++)
                board[i][0] = board[i][w + 1] = ' ';

            for(int j = 1; j < w + 1; j++)
                board[0][j] = board[h + 1][j] = ' ';

            // 下两行输出仅用于测试
            System.out.println(Arrays.deepToString(board));
            System.out.println(Arrays.deepToString(mark));

            // 定义开始那个卡片的坐标和结束那个卡片的坐标,以及用于记录线段数的变量
            int begin_x, begin_y, end_x, end_y, count = 0;

            do {
                System.out.println("请输入起点的x坐标");
                begin_x = scan.nextInt();
                System.out.println("请输入起点的y坐标");
                begin_y = scan.nextInt();

                System.out.println("请输入终点的x坐标");
                end_x = scan.nextInt();
                System.out.println("请输入终点的y坐标");
                end_y = scan.nextInt();

                // 根据需求,要这样写。考虑合理性,只要其中一个是0,就不应继续了(不考虑合理性了- ^ -!)
                if(begin_x == 0 && begin_y == 0 && end_x == 0 && end_y == 0)
                    break;

                count++;

                // 先让他是一个比较大的值,参考需求中的数据,暂定为1000
                minStep = 1000;

                // 递归搜索
                search(begin_x, begin_y, end_x, end_y, 0, -1);

                // 输出结果
                if(minStep < 1000)
                    System.out.printf("Pair%d: %d segments.\r\n", count, minStep);
                else
                    System.out.printf("Pair%d: impossible.\r\n", count);
            } while(begin_x > 0);
        } while (w != 0 || h != 0);
    }
    //传入的开始和结束的位置,目前最优解,路径方向
    private static void search(int now_x, int now_y, int end_x, int end_y, int step, int f)
    {

        // 优化策略:当前路径数大于minStep,就不继续走下去了直接退出
        if(step > minStep)
        {
            return;
        }

        // 到达终点后进行判断
        if(now_x == end_x && now_y == end_y)
        {
            if(minStep > step) // 如果当前路径比已找到最优路径还小,就替换最优路径
            {
                minStep = step;
            }
            return;
        }

        // 枚举方向(0、1、2、3表示东、南、西、北)
        for(int i = 0; i < 4; i++)
        {
            // 得到当前位置的下一个位置
            int x = now_x + to[i][0];
            int y = now_y + to[i][1];

            // 如果新位置有效
            if(   ((x > -1) && (x < w + 2) && (y > -1) && (y < h + 2)) &&
                    ( ((board[y][x] == ' ') && !mark[y][x]) || ((x == end_x)
                            && (y == end_y) && board[y][x] == 'X') )   )
            {
                // 标记该位置
                mark[y][x] = true;

                // 已经过上一步方向 和 当前方向相同
                // 则递归搜索时,step不变,否则step + 1
                if(f == i)
                    search(x, y, end_x, end_y, step, i);
                else
                    search(x, y, end_x, end_y, step + 1, i);

                // 回溯,该位置未曾走过
                mark[y][x] = false;
            }
        }
    }
}

算24(POJ 2787)

         给出4个小于10的整数,可以使用加减乘除四种运算符以及括号把这些数组合起来成一个表达式,并且这个表达式的结果是24,例如:对于5,5,5,1,可知5x(5-1/5)=24

暂无

猜你喜欢

转载自blog.csdn.net/qq_64552181/article/details/127890485
今日推荐