回溯算法之八皇后问题

Posted lyn4ever

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了回溯算法之八皇后问题相关的知识,希望对你有一定的参考价值。

一、什么是回溯算法?

我们肯定都玩过迷宫游戏吧,比较复杂的迷宫,肯定是不可能第一遍就直接过了,只能一步一步地进行尝试。当走到一个死胡同时,只能退回到上一个分岔口进行重新选择。
数独游戏也是这样的,对于一个不确定的方格,我们就会先将这个方格可能出现的问题记录下来,一个一个地尝试,直到得到正确解。有着“通用解”称呼
所以,回溯算法就是类似于枚举的算法,将这一步的所以可能性一个一个地进行尝试。上边迷宫中的分岔口和数独中的可能出现多个数字的方格就是“回溯点”

二、有什么优缺点?

  1. 因为要对每一个点的可能情况都进行枚举测试,所以效率特别低,比如后边下边例子中的“八皇后问题”中,总共要进行15000次左右的运算,虽然对于计算机来说是很快的,但是更加复杂的问题,可能会更多
  2. 它可是有着“通用解”称呼,基本上大多数的这类问题都可以用此方法解决。

技术图片

三、经典的“八皇后”问题

八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。

使用Java来解决此问题主要有以下步骤:

  1. 设计一个数据模型来解决此问题,
private int result[] = new int[8];

首先想到的就是一个二维数组,但是我在学习的时候,看到老师用了一个长度为8的一维数组就解决了,思想如下:

  • 使用int[8]来表示一个正确解,也就是说,这个数组中的8个数组分别表示第一行、第二行、第三行......直到第八行中正确的棋子的位置,比如其中一个解如下图,它所表示的结果就是:{1,5,8,6,3,7,2,4}
    技术图片
  1. 找到“回溯点”
  • 第一种回溯情况:
    这个问题的解决方案就是从每一行的每一个点开始摆放棋子,如果这个棋子可以话这里,就继续放置下一行。如果不能放置在这里,就往后移,直到这一行的8个位置全都不能的话,就往上回溯一行,将上一行的棋子往右移一行,再继续放置这一行,如果上一行全部放了个遍还是不行,就再往上回溯一行,就这样继续。。。
  • 第二种回溯情况:
    因为要找到所有的问题,第二个回溯情况就是当一种摆法摆放完成后。从最后一行进行回溯,比如上边的解中{1,5,8,6,3,7,2,4},当最后一个4摆放完成后,就将最后一个放在5这个位置上,不行的话就放6,7,8,如果全都不行,就将上一行中的2放在3位置上,再从第八行的1,2,3...这样放
  1. 写出这个判断这个回溯点的条件方法
 /**
     * 判断这一行中的棋子摆放的是否可用
     * 主要就是判断从第一行到第n-1行的这几个皇后会不会和第n行的皇后相互攻击
     * 因为之前的n-1行之间之前就已经判断了的,如果它们之前不满足条件就不会到第n行的
     *
     * @param n 传入的这一行
     * @return
     */
    private boolean check(int n) {
        timeCount++;
        for (int i = 0; i < n; i++) {
            /*
            第一个条件就是判断它们是不是在同一行
            这里主要想一个如何判断它们是不是在同一斜行上,
            因为我们的模型就是使用数组的角标来表示它当前在第i+1行,用元素表示它在这一行的第i+1个位置上
             */
            if (result[i] == result[n] || Math.abs(n - i) == Math.abs(result[n] - result[i])) {
                return false;
            }
        }
        return true;
    }

4.开始写放置棋子的方法

/**
     * 最关键的问题,使用递归来进行回溯算法
     * 我们的思路就是一行一行的放置
     *
     * @param row 在第几行摆放棋子
     */
    public void calResult(int row) {

        if (row > 7) {
            //此时就说明第8行已经摆完了,且是正确的
            sum++;
            printResult();
            return;
        }

        //这个for循环就是在这一行从第1个开始放,一直放到第max个,
        for (int i = 0; i < max; i++) {
            result[row] = i;
            if (check(row)) {
                //说明这一行检验是通过的
                calResult(row+1);
            }
            //如果这个位置不满足,就会自动执行下一次循环,也就是i++
            //如果这一行全部试完之后,还是不合格,就会自动放到回溯到上一行去
        }
    }

最后的代码如下:

package cn.lyn4ever.structure.excerise;

public class EightQueenDemo {


    /**
     * 用来表示摆放皇后的结果,
     * 数组的下标表示第i+1行的棋子,值表示棋子放在这一行的第i+1个位置
     * 如:
     * result[0]=0  表示在第一行的第一个位置上有一个棋子
     */
    private int max = 8;
    private int result[] = new int[max];
    private int sum = 0;//用来记录总结果数
    private int timeCount = 0;//用来记录所有操作的次数


    /**
     * 判断这一行中的棋子摆放的是否可用
     * 主要就是判断从第一行到第n-1行的这几个皇后会不会和第n行的皇后相互攻击
     * 因为之前的n-1行之间之前就已经判断了的,如果它们之前不满足条件就不会到第n行的
     *
     * @param n 传入的这一行
     * @return
     */
    private boolean check(int n) {
        timeCount++;
        for (int i = 0; i < n; i++) {
            /*
            第一个条件就是判断它们是不是在同一行
            这里主要想一个如何判断它们是不是在同一斜行上,
            因为我们的模型就是使用数组的角标来表示它当前在第i+1行,用元素表示它在这一行的第i+1个位置上
             */
            if (result[i] == result[n] || Math.abs(n - i) == Math.abs(result[n] - result[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * 打印这个结果数组
     */
    private void printResult() {
        for (int i = 0, length = result.length; i < length; i++) {
            System.out.print(result[i] + " ");
        }
        System.out.println();
    }

    /**
     * 最关键的问题,使用递归来进行回溯算法
     * 我们的思路就是一行一行的放置
     *
     * @param row 在第几行摆放棋子
     */
    public void calResult(int row) {

        if (row > 7) {
            //此时就说明第8行已经摆完了,且是正确的
            sum++;
            printResult();
            return;
        }

        //这个for循环就是在这一行从第1个开始放,一直放到第max个,
        for (int i = 0; i < max; i++) {
            result[row] = i;
            if (check(row)) {
                //说明这一行检验是通过的
                calResult(row+1);
            }
            //如果这个位置不满足,就会自动执行下一次循环,也就是i++
            //如果这一行会部试完之后,还是不合格,就会自动放到回溯到上一行去
        }
    }


    public static void main(String[] args) {

        EightQueenDemo eightQueenDemo = new EightQueenDemo();
        eightQueenDemo.calResult(0);
        System.out.printf("八皇后问题的解法一共有%d", eightQueenDemo.sum);
        System.out.printf("一共进行了%d次运算", eightQueenDemo.timeCount);
    }

}

最后的运行结果如下:
技术图片

以上是关于回溯算法之八皇后问题的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript之八皇后问题(归纳)

数据结构之八皇后

回溯法-N皇后问题-C++算法

8皇后问题SQL求解(回溯算法)

八皇后问题求解java(回溯算法)

暴力穷举和回溯法(八皇后问题)