递归(回溯之迷宫问题+八皇后)

回顾知识点

递归概念:递归就是方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

递归调用机制:

1)打印问题

2)阶乘问题

// 打印问题
private static void test(int n){
    if(n > 1){
        test(n - 1); // 到1为止
    }
    System.out.println("n = " + n);
}

// 阶乘问题
private static int factorial(int n){
    if(n == 1){ // 递归停止条件
        return 1;
    }else{
        return n * factorial(n - 1);
    }
}

注意点:递归方法一定要有递归停止条件,或递归限制条件。

递归的应用场景

1)各种数学问题:八皇后问题,汉诺塔,阶乘问题,迷宫问题,球和篮子问题。

2)各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等。

3)将用栈解决的问题-->递归代码比较简洁。

递归需要遵守的重要规则

1)执行一个方法时,就创建一个新的受保护的独立空间(栈空间);

2)方法的局部变量是独立的,不会相互影响;(如果方法中使用的是引用类型变量,就会共享该引用类型的数据,因为对象new出来放在了堆里)

3)递归必须向退出递归的条件逼近,否则就是无限递归;

4)当一个方法执行完毕,或者遇到return,返回,遵守谁调用就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

 

什么是回溯法?

回溯法的基本思想:对一个包括有很多结点,每一个结点有若干个搜索分支的问题,把原问题分解为对若干个子问题求解的算法。

我们简单分析一下这句话,其实就是当搜索到某个结点,发现无法再继续搜索下去的时候,就让搜索过程回溯(也称退回)到该结点的上一个结点继续搜索这个结点的其他尚未搜索过的分支,然后一遍一遍重复这个步骤,直到搜索到问题的解或搜索完了全部可搜索分支没有解存在为止

1.迷宫问题

public static void main(String[] args) {
    // 先创建一个二维数组,模拟迷宫
    int[][] map = new int[8][7]; // 8行7列 0-7,0-6
    // 使用1表示墙
    // 将上下两行置为墙
    for (int i = 0; i < 7; i++) {
        map[0][i] = 1;
        map[7][i] = 1;
    }
    // 将左右两列置为墙
    for (int i = 0; i < 8; i++) {
        map[i][0] = 1;
        map[i][6] = 1;
    }
    // 设置挡板
    map[3][1] = 1;
    map[3][2] = 1;
    //输出这个地图
    System.out.println("地图情况");
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 7; j++) {
            System.out.print(map[i][j] + " ");
        }
        System.out.println(); // 换行
    }

    // 使用递归回溯给小球走路
    getWay(map,1,1);
    // 输出新的地图,表示小球走过,并标识过的地图
    System.out.println("小球走过的地图:");
    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 7; j++) {
            System.out.print(map[i][j] + " ");
        }
        System.out.println(); // 换行
    }
}

// 使用递归回溯来给小球找路
/*
约定:当map[i][j]为0表示该带你没有走过
为1表示墙,为2表示通路可以走
为3表示该位置已经走过,但走不通
走迷宫时的策略:下-->右-->上-->左
 */
// 输入/** ,点击“Enter”,自动根据参数和返回值生成注释模板
/**
 *
 * @param map 表示地图
 * @param i 从哪个位置开始找(行)
 * @param j  从哪个位置开始找(列)
 * @return 如果找到通路,返回true,否则,返回false
 */
public static boolean getWay(int[][] map,int i,int j){
    // 终点定为[6][5]
    // 如果终点被标记为2,则找到通路
    if(map[6][5] == 2){ // 递归停止条件
        return true;
    }
    else{ // 还没有找到
        if(map[i][j] == 0){ // 如果当前这个点还没走过
            // 按策略下-->右-->上-->左
            map[i][j] = 2; // 先假定这个点能走通
            /*注意此处虽然是优先级等同的else if,但实质Java代码执行判断的时候
            是按写的顺序来的,走了上面的if或者else if走不通才来下一个,所以策略没问题*/
            // 向下走,行+1,列不变
            if(getWay(map,i+1,j)){ // 走得通
                return true; // true表示可以往这个没走过的点走
                // 暂定是通路的一个点,如果不是,回溯的时候会将其置3和false的
            }
            else if(getWay(map,i,j+1)){ // 往下走不通,走右
                return true;
            }
            else if(getWay(map,i-1,j)){ // 向右走不通,走上
                return true;
            }
            else if(getWay(map,i,j-1)){ // 向上走不通,走左
                return true;
            }
            else{ // 上下左右都走不通
                map[i][j] = 3; // 说明该点是走不通的,是死路
                return false; // false表示不能往这个点走
            }
        }else{ // 如果当前这个点已经走过,这个点可能是1,2,3
            /*
            如果这个点是1,表示是墙,不能走;
            是3,表示走过,走不通;
            是2,表示是已经走过的通路了,不要再重复走了
            所以统一返回false
             */
            return false; // false表示不能往这个点走
        }
    }
}

迷宫问题优化:存在最短路径

思路:改变策略,小球走过的路径和程序员设置的策略有关,将所有的策略都走一遍,将置成2的点放入一个ArrayList中,比较每个策略得出的结果的长度,最短的即最优,但时间复杂度过高,不推荐,后续学习深度遍历和广度遍历后有更优解法。

2.八皇后问题

八皇后问题也是一个回溯算法的典型案例。该问题描述为:

在8*8格的国际象棋上摆放八皇后,使其不能互相攻击,即任意两个皇后不能处于同一行、同一列或同一斜线上,问有多少种摆法。

思路:

1)第一个皇后放在第一行第一列。

2)第二个皇后放在第二行第一列,,然后判断是否ok,如果不ok,继续放在第二列、第三列、...,依次把所有列都放完,找到一个合适的。

3)继续第三个皇后,还是第一列、第二列、...直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。

4)当得到了一个正确解,在栈回退到上一个栈时,就会开始回溯,即将第一个皇后,放到第一列的所有正确解,全部得到。

5)然后回头继续第一个皇后放第二列,后面继续循环执行1,2,3,4的步骤。

说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题。如arr[8] = {0,4,7,5,2,6,1,3},行和下标对应,表示第几+1个皇后

因为是要找解法,所以回溯过程中不止是要判断是否被攻击,即使找到了通路,也要从栈顶元素开始继续遍历下一个点,查看是否有新的通路,比如说1234能走通,1235也能走通。

代码实现:

/*
思路:第一个皇后,放在第一行第一列;第二个皇后放在第二行第一列,判断是否冲突,如果冲突,后移;
不冲突,继续放第三个皇后,直到放完8个皇后
那么就在不冲突时:递归回溯放桓侯
同时,因为冲突的时候,需要后移,所以需要一个for循环
设置一个一维数组放置皇后放置位置的列数,下标就是列数,故不用二维数组
 */
public class DemoQueue {
    static int max = 8; // 八皇后
    static int[] arr = new int[max]; // 放置皇后,成员变量,共享列表
    static int count = 0; // 有多少种解法
    public static void main(String[] args) {
        // 测试
        putQueue(0); // 从第0个皇后放起
        System.out.println("一共有" + count + "种结果");
    }

    // 放置当前皇后(递归回溯+for循环)
    private static void putQueue(int n){
        // 回溯+循环停止条件
        if(n == max){ // 8个都放好了
            count++;
            print();
            return;
        }
        // 循环,方便回溯的时候,下移
        for (int i = 0; i < max; i++) {
            // 每个皇后都从该行第一列放起
            arr[n] = i;
            // 然后判断位置是否冲突,冲突,下移(即继续循环),不冲突,递归下一个
            if(judge(n)){ // 不冲突
                putQueue(n + 1);
            }
            // 冲突,继续遍历,故啥也不写
        }
    }

    // 判断当前皇后是否与前面的皇后位置冲突
    private static boolean judge(int n){ // n表示第几个皇后(从0开始数)
        /*行冲突不用判断,放置方法决定了不会行冲突
        列冲突:arr[n] == arr[i]
        斜线冲突:| n - i | == | arr[n] - arr[i] |
         */
        for (int i = 0; i < n; i++) { // 当前皇后和前面已经放好了的皇后进行比较
            if(arr[n] == arr[i] || Math.abs(n - i) == Math.abs(arr[n] - arr[i])){ // 冲突
                return false; // 冲突
            }
        }
        return true; // 不冲突
    }

    // 输出arr结果
    private static void print(){
        for(int i : arr){
            System.out.print(i + " ");
        }
        System.out.println(); // 输出一种结果之后,就换行
    }
}

结果:

0 4 7 5 2 6 1 3 
0 5 7 2 6 3 1 4 
0 6 3 5 7 1 4 2 
0 6 4 7 1 3 5 2 
1 3 5 7 2 0 6 4 
1 4 6 0 2 7 5 3 
1 4 6 3 0 7 5 2 
1 5 0 6 3 7 2 4 
1 5 7 2 0 3 6 4 
1 6 2 5 7 4 0 3 
1 6 4 7 0 3 5 2 
1 7 5 0 2 4 6 3 
2 0 6 4 7 1 3 5 
2 4 1 7 0 6 3 5 
2 4 1 7 5 3 6 0 
2 4 6 0 3 1 7 5 
2 4 7 3 0 6 1 5 
2 5 1 4 7 0 6 3 
2 5 1 6 0 3 7 4 
2 5 1 6 4 0 7 3 
2 5 3 0 7 4 6 1 
2 5 3 1 7 4 6 0 
2 5 7 0 3 6 4 1 
2 5 7 0 4 6 1 3 
2 5 7 1 3 0 6 4 
2 6 1 7 4 0 3 5 
2 6 1 7 5 3 0 4 
2 7 3 6 0 5 1 4 
3 0 4 7 1 6 2 5 
3 0 4 7 5 2 6 1 
3 1 4 7 5 0 2 6 
3 1 6 2 5 7 0 4 
3 1 6 2 5 7 4 0 
3 1 6 4 0 7 5 2 
3 1 7 4 6 0 2 5 
3 1 7 5 0 2 4 6 
3 5 0 4 1 7 2 6 
3 5 7 1 6 0 2 4 
3 5 7 2 0 6 4 1 
3 6 0 7 4 1 5 2 
3 6 2 7 1 4 0 5 
3 6 4 1 5 0 2 7 
3 6 4 2 0 5 7 1 
3 7 0 2 5 1 6 4 
3 7 0 4 6 1 5 2 
3 7 4 2 0 6 1 5 
4 0 3 5 7 1 6 2 
4 0 7 3 1 6 2 5 
4 0 7 5 2 6 1 3 
4 1 3 5 7 2 0 6 
4 1 3 6 2 7 5 0 
4 1 5 0 6 3 7 2 
4 1 7 0 3 6 2 5 
4 2 0 5 7 1 3 6 
4 2 0 6 1 7 5 3 
4 2 7 3 6 0 5 1 
4 6 0 2 7 5 3 1 
4 6 0 3 1 7 5 2 
4 6 1 3 7 0 2 5 
4 6 1 5 2 0 3 7 
4 6 1 5 2 0 7 3 
4 6 3 0 2 7 5 1 
4 7 3 0 2 5 1 6 
4 7 3 0 6 1 5 2 
5 0 4 1 7 2 6 3 
5 1 6 0 2 4 7 3 
5 1 6 0 3 7 4 2 
5 2 0 6 4 7 1 3 
5 2 0 7 3 1 6 4 
5 2 0 7 4 1 3 6 
5 2 4 6 0 3 1 7 
5 2 4 7 0 3 1 6 
5 2 6 1 3 7 0 4 
5 2 6 1 7 4 0 3 
5 2 6 3 0 7 1 4 
5 3 0 4 7 1 6 2 
5 3 1 7 4 6 0 2 
5 3 6 0 2 4 1 7 
5 3 6 0 7 1 4 2 
5 7 1 3 0 6 4 2 
6 0 2 7 5 3 1 4 
6 1 3 0 7 4 2 5 
6 1 5 2 0 3 7 4 
6 2 0 5 7 4 1 3 
6 2 7 1 4 0 5 3 
6 3 1 4 7 0 2 5 
6 3 1 7 5 0 2 4 
6 4 2 0 5 7 1 3 
7 1 3 0 6 4 2 5 
7 1 4 2 0 6 3 5 
7 2 0 5 1 4 6 3 
7 3 0 2 5 1 6 4 
一共有92种结果

 

 

 

 

 

参考博客:https://www.jianshu.com/p/d5ebf873cc6f

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值