如何解决八皇后问题

问题

八皇后问题指的是在 8*8 的棋盘上,放入 8 个皇后,并且保证在每一行、每一列、以及对角线上都不会同时出现两个皇后(国际象棋的规则里面,皇后的攻击范围是其所在的横竖两条线以及所在的两条对角线),那么该如何摆放这 8 个皇后呢?一共有多少种摆放方法?

思路

第一种思路,8*8 的棋盘上一共有 64 个格子,现在要将 8 个皇后放入到这 64 个格子当中,就是数学里面的组合数 ,然后从这些组合里面挑选出符合条件的摆放方法。这种做法虽然没错,但是 这个组合数的计算结果太大了,一共有 4426165368 种组合,计算量偏大。

第二种思路,由于每个皇后不能在同一行、同一列上,我们在放入第一个皇后时,随便选择一行,我们有 8 种选择;再放入第二个皇时,此时我们肯定不能放在第一个皇后所在的行和列了,所以只有 7 种选择了;再接着我们放入第 3 个皇后,同样的道理,只能有 6 种选择了;以此类推,当我们放入第 8 个皇后的时候,就只有 1 种选择了。所以这种思路,我们一共有 8 的阶乘种选择,即 40320 种选择,然后我们再从这 4 万多种组合里面选择出符合条件的摆放方式,看上去 4 万多相比前面的 44 亿,小了很多,也算是一种不错的解法了。

第三种思路:在第二种思路的基础上,我们在放入第二个皇后的时候,只考虑了与第一个皇后不同列、不同行,没有考虑对角线的问题,所以认为第 3 个皇后在放入的时候有 7 中选择,那如果我们把对角线的问题考虑进去,那第二个皇后的选择就少了一点。同样,在放第 3 个皇后的时候,我们再把前面两个皇后的对角线考虑进去,第三个皇后的选择也少了,以此类推,到放第 8 个皇后的时候,选择就更少了,甚至在放第 5 个、第 6 个的时候,就出现了没有任何选择可以选的情况了,也就是死路了。这样总体下来,出现的组合情况肯定是远远小于第二种思路的 4 万多种组合。

对于第三种思路,如果我们出现了死路怎么办?假如我们在放第 5 个皇后的时候,出现了无论怎么放第五个皇后,都会不满足要求,这个时候就说明,前面 4 个皇后的摆放有问题。那怎么呢?我们可以先回退到第 4 步,修改第 4 个皇后的摆放位置,然后再继续尝试摆放第 5 个皇后。如果第 5 个皇后仍然无法摆放,那再回到第 4 步,再修改第 4 个皇后的位置。如果出现第 4 个皇后可以摆的地方全都尝试了,第 5 个皇后还是没有地方可放,那这个时候就说明,第 3 个皇后的位置摆放出了问题,所以重新回到第 3 步,如果后面依旧出现死路,那就依次往后回退,直到找到合适的摆法。

熟悉数据结构与算法的同学这个时候肯定想到了,这种思路就是回溯思想,先从某一条路开始走,一条路走到黑,出现死胡同了,就回到上一个路口(回溯),从另一个方向再次出发,又出现死胡同了,就再返回刚刚的路口,直到将该路口的所有岔道走遍了,如果还是走不通,就继续向前回溯,回到上上个路口,直到找到出路为止。

回溯的思想很简单,那么代码该如何实现呢?实现回溯法最常用的方式就是使用递归了,下面使用回溯思想,用递归代码来实现上面 8 个皇后的摆放问题。

回溯法代码实现

首先我们定义一个长度为 8 的数组:int[] result,用来存放每个皇后摆放在哪个地方,数组的下标表示存放的是第几行的皇后,元素的值表示的是该行的皇后摆放在哪一列。例如:result[0] = 4 表示第 0 行中的皇后放在第 4 列。

另外再定义一个方法 putQueen(row),含义是往第 row 行中放入一个皇后,代码的骨架如下:

// 用一个数组存放每一行的皇后的位置,数组的下标表示的是行,元素的值表示的是该行的皇后摆放在哪一列
public int[] result = new int[8];

/**
 * 往第row行中放入皇后,row最开始从0开始
 * @param row 第几行
 */

public void putQueen(int row) {
    if (row == 8) {   // 如果等于8表示8个皇后都摆放完了,直接返回即可
        printQueen();   // 打印
        return;
    }
    for (int column = 0; column < 8; column++) {    // 尝试分别将皇后放在第0-7列中
        if (isAccessible(row, column)) {   // 在真正将皇后放进棋盘之前,先判断这个位置能不能摆放皇后
            result[row] = column;   // 放入皇后
            putQueen(row + 1);    // 在下一行中放入皇后
        }
    }
}
复制代码

在每次往某一行的某一列中放入皇后之前,我们需要判断一下,该行该列是不是在前面几个皇后的攻击范围之内(行、列、对角线)。所以定义了一个方法 isAccessible(row, column),该方法就是来判断该行该列能不能放入皇后。判断逻辑是什么呢?就是从当前行依次向上遍历,判断前面几行的皇后的攻击范围是不是会覆盖到第 row 行第 column 列,具体代码如下。

/**
 * 判断第row行,第column列能不能摆放皇后
 * @return
 */

private boolean isAccessible(int row, int column) {
    int left = column - 1;  // 对角线左上
    int right = column + 1// 对角线右上
    // 从当前行的上一行开始,向上遍历 (没有必要判断当前行的下面几行了,因为下面几行肯定没有放皇后啊)
    for (int i = row - 1; i >= 0; i--) {
        if (result[i] == column) return false;   // 当前列上不能有皇后
        if (left >= 0 && result[i] == left) return false;  // 左上对角线上不能有皇后
        if (right < 8 && result[i] == right) return false// 右上对角线上不能有皇后
        left--;
        right++;
    }
    return true;
}
复制代码

最后为了方便显示皇后的摆放位置,写了一个打印 8*8 棋盘的方法。

private void printQueen() {
    for (int row = 0; row < 8; row++) {
        for (int column = 0; column < 8; column++) {
            if (result[row] == column) System.out.print("Q ");
            else System.out.print("* ");
        }
        System.out.println();
    }
    System.out.println("=========================");
}
复制代码

最终运行结果一共有 92 中摆放方法。

总结

回溯法的思想很简单,就是在岔口上先选择一条路走下去,走不通了,就回退到上一步,继续走,直到尝试所有的选择为止。虽然思路简单,但实现回溯法的代码相对而言,不是那么好写,经常出现一看就会,一些就错,笔者作为一名菜鸟就是如此,经常写错,尤其是边界值的地方,因此也建议大家多亲自动手敲敲代码。

其他

微信公众号

猜你喜欢

转载自juejin.im/post/5ea1d035518825737f1a849c
今日推荐