美文网首页算法
从八皇后开始说回溯法

从八皇后开始说回溯法

作者: 怀念小兔 | 来源:发表于2019-01-08 14:03 被阅读2次

    定义

    开头先上一个让人懵逼的定义:把问题的解空间转化成图或树,再按深度优先遍历它,在遍历时记录所有解再根据条件选择找出最优解或展示所有可行解。

    解释

    解空间是相对的,它取决于约束条件。
    举个栗子吧,八皇后问题。国际象棋的棋盘上放8个皇后,要求皇后不能互相吃掉(皇后吃东西的范围是横竖线加两个对角线),那么,在以皇后不能互相吃掉的前提为前提条件下,我们从头开始往下放皇后,放到没有位置可以放为止,我们放下的这些皇后的位置,就构成了一个解,所有的解就构成了解空间,每个解都是由若干个位置组成的,所以这个解空间是一个树形结构,以x轴右方向为正,y轴下方向为正,左上角为原点在棋盘上建立坐标系(因为后面二位数组我们按这个顺序打印输出),如果我们第一个皇后放在0,0位置,那么第二行的皇后可以放在除0,1和1,1外的所有位置,以此类推,这是一个明显的树形结构。
    但这只是以皇后不能互相吃掉为约束条件,我们还有一个条件就是“八”,所以如果放不了八个皇后,换言之就是这个解空间树的遍历深度达不到第八层,就是失败解,这种情况如果出现了,我们就要回退到上一层去寻找其他解,再找不到再向上,重复这个过程。下图所示就是一种失败解:


    失败解

    可以看到,第六层任何位置都没法放皇后了(请别吐槽皇后的长相)。

    算法思路

    首先确定我们要干什么,我们要打印出所有可行解。
    1.从第一行开始,逐个格子测试是否可以放下皇后(满足不互相吃的条件)。
    2.放下之后在下一行重复过程1,因为本行不可能放两个皇后,否则就互相吃了。
    3.如果出现某行无法放下皇后,就退回上一行删掉之前放下的皇后,换个位置尝试,如果没位置了再回退一行。
    4.如果在第八行成功放下皇后,就是可行解,打印它之后回退,寻找其他可行解。

    代码实现

    根据以上思路,代码实现如下:

    public class EightQueen {
        public static final int TYPE_QUEEN = 5;
        public static final int TYPE_EMPTY = 0;
        private int length = 8;
        private int [][] chessboard = new int[length][length];
        public void startPut(){
            putQueenRecursive(0);
        }
    
        private void printMatrix(){
            for (int y = 0; y < length; y++) {
                for (int x = 0; x < length; x++) {
                    System.out.print(chessboard[y][x] + " ");
                }
                System.out.println();
            }
            System.out.println("=========================");
        }
    
        private void putQueenRecursive(int row){
            if(row == length){
                printMatrix();
                return;
            }
            for (int column = 0; column < length; column++) {
                if(!isConflict(row, column)){
                    chessboard[row][column] = TYPE_QUEEN;
                    //System.out.println(String.format("==========在[%s][%s]位置放下皇后", row, column));
                    putQueenRecursive(row + 1);
                    System.out.println("这个解是失败的");
                    printMatrix();
                    //这行代码非常关键,下层返回了就证明无解或已经产生解,继续求解则必须把这次放的皇后消灭
                    chessboard[row][column] = TYPE_EMPTY;
                }
            }
    
        }
        private boolean isConflict(int row, int column){
            //检测纵向是否有皇后
            for (int i = 0; i < length; i++) {
                if(i != column && chessboard[row][i] != 0){
                    //System.out.println(String.format("[%s][%s]横向有皇后,位置[%s][%s]", row, column, row, i));
                    return true;
                }
            }
            //检测横向是否有皇后
            for (int j = 0; j < length; j++) {
                if(j != row && chessboard[j][column] != 0){
                    //System.out.println(String.format("[%s][%s]纵向有皇后,位置[%s][%s]", row, column, j, column));
                    return true;
                }
            }
            //检测右下走向对角线是否有皇后
            int min = Math.min(row, column);
            for (int r1 = row - min, c1 = column - min; r1 < length && c1 < length; r1++, c1++) {
                if(r1 != row && c1 != column && chessboard[r1][c1] != 0){
                    //System.out.println(String.format("[%s][%s]右斜向有皇后,位置[%s][%s]", row, column, r1, c1));
                    return true;
                }
            }
            //检测左下走向对角线是否有皇后
            //因为是向左下角走,y增方向不变,x值则相反了,所以这里初始值x应该取length - x
            min = Math.min(row, (length - 1 - column));
            //同样,x值的初始值应该加最小值,x的边界是0,增量是-1
            for (int r1 = row - min, c1 = column + min; r1 < length && c1 > -1; r1++, c1--) {
                if(r1 != row && c1 != column && chessboard[r1][c1] != 0){
                    //System.out.println(String.format("[%s][%s]左斜向有皇后,位置[%s][%s]", row, column, r1, c1));
                    return true;
                }
            }
            return false;
        }
        private int previousIndex(int index){
            if(index > 0)
                return index - 1;
            else
                return length - 1;
        }
        private int nextIndex(int index){
            if(index < length - 1)
                return index + 1;
            else
                return 0;
        }
    }
    

    优化

    以上代码我们采用二维数组建模,但是通过观察可以发现,二维数组中只有两种状态,一种是有皇后,另一种是空的(同时也是数组的默认值),同时每行只有一个位置有皇后,符合这种情况的二维数组可以优化为一维数组,数组中存放的是本行中皇后的位置。
    那么,这种情况下怎么判定在横竖行和对角线是否有皇后呢?我们还是先画图观察:


    冲突位置皇后合影(局部)

    上图中,我们以x轴皇后位置为值转化为一维数组,可以得出以下结果:
    1.横行的皇后我们直接不用考虑了。因为上面的思路里就不会在横行放两个皇后。
    2.对角线的皇后符合一个特征,我们来看 带草帽的皇后,如果它的坐标是(x, y),跟她冲突的皇后坐标是(x1, y1),转化为一维数组时它的坐标是(arr[i], i),跟她冲突的皇后坐标是(arr[i1], i1),跟她在对角线的皇后都符合一个特征:abs(x - x1)等于abs(y - y1)。这点当我们转化为一维数组时就是abs(i - i1)等于abs(arr[i] - arr[i1])。
    3.竖行的皇后就是x坐标相等,在转化为一维数组后就是arr[i]等于arr[i1]。

    按照以上思路重写代码,如下:

    public class GracefulEightQueen {
        private int[] queenXLocations = new int[8];
        public void dropQueen(){
            dropQueen(0);
        }
        private void dropQueen(int row){
            if(row == 8){
                printQueenXLocations();
                return;
            }
            for (int column = 0; column < queenXLocations.length; column++) {
                queenXLocations[row] = column;
                if(canDropQueen(row)){
                    dropQueen(row + 1);
                    //这里为什么不用擦除之前的值?
                    //因为转化为一维数组之后,每次对同一行不同位置的赋值都是覆盖操作
                }
            }
        }
        private boolean canDropQueen(int row){
            for (int i = 0; i < row; i++) {
                if(queenXLocations[row] == queenXLocations[i] || Math.abs(row - i) == Math.abs(queenXLocations[row] - queenXLocations[i])){
                    return false;
                }
            }
            return true;
        }
        private void printQueenXLocations(){
            System.out.println(Arrays.toString(queenXLocations));
        }
    }
    

    经测试,以上代码运行结果均符合预期。

    相关文章

      网友评论

        本文标题:从八皇后开始说回溯法

        本文链接:https://www.haomeiwen.com/subject/kgqhrqtx.html