回溯法之八皇后问题(Java)

5,298 阅读8分钟

什么是回溯法?

回溯法(探索与回溯法)是一种选优搜索法,又称为试探法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为“回溯点”。

百度百科
维基百科

回溯算法设计过程

1. 确定问题的解空间
2. 确定结点的扩展规则
3. 搜索解空间

回溯法例子

经典八皇后问题
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出:在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
  1. 简单的四皇后问题

    四皇后与八皇后解法相同,只是放置的旗子比较少,用这个来讲解,容易理解。

盗的图

上图为四皇后遍历循环解法的全图。
1. 第一行为一个棋盘
2. 第二行为 放置第一个皇后,一共可以在第一行的四个位置。
3. 第三行为 放置了第一个皇后之后,放置第二个皇后的位置,也可以放置在四个位置。(第一行的每个放置的位置,第三行都有四个对应的分支,篇幅所限,没有画出。)
4. 所以第三行有4X4=16种方法,一次类推,第四行(也就是放置第三个皇后之后)有 16X4=64中方法,第五行(第四个皇后)有64X4=256中方法。
现在来去掉不符合解法的方法:首先看上图第三行,放置第二个皇后的时候,明显第三行图片的第一、第二、第三小图都不符合要求,所以他们之下的所有解法都应该排除。

总结: 循环放置皇后,在第二个皇后不符合的情况下,(如上图第三行 第一小图)也会继续放置其他皇后,这样浪费了资源,而回溯法就是在出现这种情况的时候,不在继续往下放置,而是切换到第二个小图。(以此类推,直到发现第四个小图符合要求,才会继续放置第三个皇后)
四皇后回溯法的步骤如下图所示:

盗图

上图为 四皇后解法的代码执行顺序。可以在下边的代码理解之后,在对照着来理解。

八皇后的代码步骤:
  1. 从第0行0列开始,检查第0行能否放置一个皇后
  2. 如果可以,那么放置一个皇后 (也就是0行0列置为1)
  3. 接下来放置第1行的皇后,依次类推,直到放置到第8的时候,说明找到一个解
  4. 如果没有找到一个解,那么就将上一步放置的皇后置为0(也就是第0行0列置为0)
  5. 然后开始0行1列,重复以上步骤(也就是0行开始,循环把皇后放置到0行的每一列,然后看能否找到一个解)
Talk is cheap, show me the code.
  1. 创建对象,初始化棋盘。

    /**
     * @author colter
     * 2018/3/25
     */
    public class QueenSolution {
        //模拟一个8X8的棋盘,0代表没有放置,1代表放置了一个皇后
        private int[][] board = new int[8][8];
        //解法的数量
        private int total = 0;
    }
    
  2. 增加放置第K个皇后的方法

        /**
         * 放置皇后的时候从第0行开始,依次放置一个
         * 如果放置成功,那么继续放置下一行。(0-7)
         * 当要放置k=8的时候说明已经全部放置完
         * 毕了,找到了一个对应的解
         *
         * @param k 放置第几个皇后,K从0开始
         */
        //放置第K个皇后
        public void putQueen(int k) {
            int max = board.length;
            //放置第8个,说明棋盘已经放置完毕了,输出结果。
            if (k >= max) {
                //找到一个解,打印出来。
                total++;
                //打印解
                System.out.println(String.format("=============%s===============", total));
                for (int i = 0; i < max; i++) {
                    System.out.println(Arrays.toString(board[i]));
                }
                System.out.println("=============================");
            } else {
                /**
                 * A:
                 * 1. 从第0行,0列开始放置一个皇后
                 * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
                 * 3. 如果全部顺利,那么直到找到一个解
                 * 4. 然后0行1列放置一个皇后,找到一个解。以此类推
                 *
                 * B:
                 * 1. 从第0行,0列开始放置一个皇后
                 * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
                 * 3. 如果下一列没有可以放置的位置,那么将刚才放置的位置 置为0
                 * (也就是皇后不能放在这里,此时会回溯到上一层循环,重新放置)
                 * 例子:
                 * k = 6 的时候,假如board[6][0]放置了一个皇后(k=6,i=0),
                 * 那么在接下来执行putQueen(6+1)
                 * 假如遍历board[7]之后发现没有位置能够放置一个皇后,
                 * 那么会执行board[k][i] = 0 (k=6,i=0)
                 * 此时第6行第0列的皇后被拿走了,此时i自增为1
                 * 然后执行check(k,i) (k=6,i=1)如果能放置的话,重复上边的动作,
                 * 如果不能放置的话继续自增i,放置到第6行的下一列。
                 */
                for (int i = 0; i < max; i++) {
                    if (check(k, i)) {
                        board[k][i] = 1;
                        putQueen(k + 1);
                        board[k][i] = 0;
                    }
                }
            }
        }
    
  3. 增加检查是否满足的方法

        /**
         * 皇后放置的时候是从上到下每一行放置的,所以不用检查改行以及之后的行
         * 所以只用检查列以及左上右上对角线
         *
         * @param row 检查的对应行
         * @param col 检查的对应列
         * @return 返回改点是否满足可以放置一个皇后
         */
        private boolean check(int row, int col) {
            //检查列是否有皇后
            for (int i = 0; i < row; i++) {
                if (board[i][col] == 1) {
                    return false;
                }
            }
            //检查左上对角线是否有皇后
            for (int m = row - 1, n = col - 1; m >= 0 && n >= 0; m--, n--) {
                if (board[m][n] == 1) {
                    return false;
                }
            }
            //检查右上对角线是否有皇后
            for (int m = row - 1, n = col + 1; m >= 0 && n < board[0].length; m--, n++) {
                if (board[m][n] == 1) {
                    return false;
                }
            }
            return true;
        }
    

整个解法已经完毕了,整个文章只是为了讲解回溯法以及其应用,所以并不会用到很复杂的代码,以及代码结构的不合理之处,还请见谅。

以下为完整代码(可直接运行):

import java.util.Arrays;

/**
 * @author colter
 * 2018/3/25
 */
public class QueenSolution {
	//修改棋盘的大小,可模拟其他皇后类似问题
    //模拟一个8X8的棋盘,0代表没有放置,1代表放置了一个皇后
    private int[][] board = new int[8][8];

    //解法的数量
    private int total = 0;

    /**
     * 放置皇后的时候从第0行开始,依次放置一个
     * 如果放置成功,那么继续放置下一行。(0-7)
     * 当要放置k=8的时候说明已经全部放置完
     * 毕了,找到了一个对应的解
     *
     * @param k 放置第几个皇后,K从0开始
     */
    //放置第K个皇后
    public void putQueen(int k) {
        int max = board.length;
        //放置第8个,说明棋盘已经放置完毕了,输出结果。
        if (k >= max) {
            //找到一个解,打印出来。
            total++;
            System.out.println(String.format("=============%s===============", total));
            for (int i = 0; i < max; i++) {
                System.out.println(Arrays.toString(board[i]));
            }
            System.out.println("=============================");
        } else {
            /**
             * A:
             * 1. 从第0行,0列开始放置一个皇后
             * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
             * 3. 如果全部顺利,那么直到找到一个解
             * 4. 然后0行1列放置一个皇后,找到一个解。以此类推
             *
             * B:
             * 1. 从第0行,0列开始放置一个皇后
             * 2. 如果可以放置,那么就先将该位置置为1,然后放置下一行
             * 3. 如果下一列没有可以放置的位置,那么将刚才放置的位置 置为0
             * (也就是皇后不能放在这里,此时会回溯到上一层循环,重新放置)
             * 例子:
             * k = 6 的时候,假如board[6][0]放置了一个皇后(k=6,i=0),
             * 那么在接下来执行putQueen(6+1)
             * 假如遍历board[7]之后发现没有位置能够放置一个皇后,
             * 那么会执行board[k][i] = 0 (k=6,i=0)
             * 此时第6行第0列的皇后被拿走了,此时i自增为1
             * 然后执行check(k,i) (k=6,i=1)如果能放置的话,重复上边的动作,
             * 如果不能放置的话继续自增i,放置到第6行的下一列。
             */
            for (int i = 0; i < max; i++) {
                if (check(k, i)) {
                    board[k][i] = 1;
                    putQueen(k + 1);
                    board[k][i] = 0;
                }
            }
        }
    }

    /**
     * 皇后放置的时候是从上到下每一行放置的,所以不用检查改行以及之后的行
     * 所以只用检查列以及左上右上对角线
     *
     * @param row 检查的对应行
     * @param col 检查的对应列
     * @return 返回改点是否满足可以放置一个皇后
     */
    private boolean check(int row, int col) {
        //检查列是否有皇后
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 1) {
                return false;
            }
        }

        //检查左上对角线是否有皇后
        for (int m = row - 1, n = col - 1; m >= 0 && n >= 0; m--, n--) {
            if (board[m][n] == 1) {
                return false;
            }
        }

        //检查右上对角线是否有皇后
        for (int m = row - 1, n = col + 1; m >= 0 && n < board[0].length; m--, n++) {
            if (board[m][n] == 1) {
                return false;
            }
        }
        return true;

    }

    public static void main(String[] args) {
        QueenSolution solution = new QueenSolution();
        solution.putQueen(0);
    }
}