生活日用算法——八皇后问题

八皇后问题也算是比较经典的回溯算法的经典案例。题干描述如下:

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

对此首先我们使用array[][]来构建一个棋盘,然后尝试落子,此时算法如下:

   /**
    * 寻找皇后节点
    * @param row
    * @param size
    */
    public static void findQueen(int row, int size) {
        // 如果皇后行已达到最后一行,结果数量自增,并未输出当前棋盘情况
        if (row == size) {
            resultCount++;
            print(size);
            return;
        }

        // 递归回溯
        for (int column = 0; column < size; column++) {
            // 检查当前节点是否可以放皇后
            if (check(row, column, size)) {
                // 使用皇后占位
                array[row][column] = 1;
                // 查找下一列的可占位节点
                findQueen(row + 1, size);
                // 继续尝试同一行其他列占位前,清空临时占位
                array[row][column] = 0;
            }
        }
    }

其中check方法实现如下:

 /**
     * 判断节点是否合适
     *
     * @param row    行
     * @param column 列
     * @param size   棋盘尺寸
     * @return
     */
    public static boolean check(int row, int column, int size) {

        // 遍历
        for (int rowTemp = row - 1; rowTemp >= 0; rowTemp--) {
            // 验证纵向
            if (array[rowTemp][column] == 1) {
                return false;
            }

            int offset = row - rowTemp;
            int columnLeft = column - offset, columnRight = column + offset;
            // 验证左向
            if (columnLeft >= 0 && array[rowTemp][columnLeft] == 1) {
                return false;
            }

            // 验证右向
            if (columnRight < size && array[rowTemp][columnRight] == 1) {
                return false;
            }
        }

        return true;
    }

精简空间复杂度后,完整算法如下:

public class EightTest {
    public static int[] array;//棋盘,放皇后
    public static int resultCount = 0;//存储方案结果数量

    public static void main(String[] args) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        int size = 14;
        array = new int[size];
        findQueen(0, size);
        System.out.println(size + "皇后问题共有:" + resultCount + "种可能,耗时:"+stopwatch.stop().toString());
    }

    /**
     * 寻找皇后节点
     *
     * @param row
     * @param size
     */
    public static void findQueen(int row, int size) {
        // 如果皇后行已达到最后一行,结果数量自增,并未输出当前棋盘情况
        if (row == size) {
            resultCount++;
            print(size);
            return;
        }

        // 递归回溯
        for (int column = 0; column < size; column++) {
            // 检查当前节点是否可以放皇后
            if (check(row, column, size)) {
                // 使用皇后占位
                array[row] = column;
                // 查找下一列的可占位节点
                findQueen(row + 1, size);
                // 继续尝试同一行其他列占位前,清空临时占位
                array[row] = 0;
            }
        }
    }

    /**
     * 判断节点是否合适
     *
     * @param row    行
     * @param column 列
     * @param size   棋盘尺寸
     * @return
     */
    public static boolean check(int row, int column, int size) {

        // 遍历
        for (int rowTemp = row - 1; rowTemp >= 0; rowTemp--) {
            // 验证纵向
            if (array[rowTemp] == column) {
                return false;
            }

            int offset = row - rowTemp;
            int columnLeft = column - offset, columnRight = column + offset;
            // 验证左向
            if (columnLeft >= 0 && array[rowTemp] == columnLeft) {
                return false;
            }

            // 验证右向
            if (columnRight < size && array[rowTemp] == columnRight) {
                return false;
            }
        }

        return true;
    }

    public static void print(int size) {//打印结果
        System.out.println("方案" + resultCount + ":");
        for (int i = 0; i < size; i++) {
            for (int m = 0; m < size; m++) {
                System.out.print((array[i] == m ? 1 : 0) + " ");
            }
            System.out.println();
        }
        System.out.println();
    }
}

继续优化时间复杂度,我们仔细观察输出的棋盘信息,举个例子:

方案92:
0 0 0 0 0 0 0 1 
0 0 0 1 0 0 0 0 
1 0 0 0 0 0 0 0 
0 0 1 0 0 0 0 0 
0 0 0 0 0 1 0 0 
0 1 0 0 0 0 0 0 
0 0 0 0 0 0 1 0 
0 0 0 0 1 0 0 0 

此时优化的着手点可以可以着眼为,当我对第n行遍历寻找位置时,实际上对于每一个位置是否能放皇后,我们是否能通过一次运算等到结论。

仔细观上述结果,对第三行寻找位置时,第一行与第二行皇后控制的地三行的格子,其实已经确定了。如果我们把每一行当做一个八位的二进制数,那么第一行的皇后控制的第三行为00000101,第二行的皇后控制的第三行为00111000。此时只需要一个或运算,即可得到两个皇后控制的第三行数字为00111101。0的位置为皇后可放位置,即放皇后后的二进制数与上述00111101进行一次与运算,如果值为0即代表此处可放皇后。具体代码如下:

public class EightTest {

    public static Integer size = 14;
    // 棋盘,放皇后
    public static int[] array = new int[size];

    // 映射
    public static int[] arrayMapping = new int[size];

    // 映射深度
    public static int[][] arrayMappingDeep = new int[size][size];

    // 已使用缓存
    public static int[] useArray = new int[size];

    // 存储方案结果数量
    public static int resultCount = 0;


    public static void main(String[] args) {
        Stopwatch stopwatch = Stopwatch.createStarted();
        arrayMapping[0] = 0b1;

        for (int i = 1; i < size; i++) {
            arrayMapping[i] = arrayMapping[i - 1] << 1;
        }

        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                arrayMappingDeep[i][j] = arrayMapping[i] | (i + j >= size ? 0 : arrayMapping[i] << j) | arrayMapping[i] >> j;
            }
        }

        findQueen(0, size);
        System.out.println(size + "皇后问题共有:" + resultCount + "种可能,耗时:" + stopwatch.stop().toString());
    }

    /**
     * 寻找皇后节点
     *
     * @param row
     * @param size
     */
    public static void findQueen(int row, int size) {
        // 如果皇后行已达到最后一行,结果数量自增,并未输出当前棋盘情况
        if (row == size) {
            resultCount++;
            //print(size);
            return;
        }

        useArray[row] = 0;
        for (int i = 0; i < row; i++) {
            useArray[row] = useArray[row] | arrayMappingDeep[array[i]][row - i];
        }

        // 递归回溯
        for (int column = 0; column < size; column++) {

            // 检查当前节点是否可以放皇后
            if (check(column, row)) {
                // 使用皇后占位
                array[row] = column;
                // 查找下一列的可占位节点
                findQueen(row + 1, size);
                // 继续尝试同一行其他列占位前,清空临时占位
                array[row] = 0;
            }
        }
    }

    /**
     * 判断节点是否合适
     *
     * @param column 列
     * @return
     */
    public static boolean check(int column, int row) {

        return (useArray[row] & arrayMapping[column]) == 0;
    }

    public static void print(int size) {//打印结果
        System.out.println("方案" + resultCount + ":");
        for (int i = 0; i < size; i++) {
            for (int m = 0; m < size; m++) {
                System.out.print((array[i] == m ? 1 : 0) + " ");
            }
            System.out.println();
        }
        System.out.println();
    }
}

本地执行14皇后时,前者速度为30S,位运算写法1S,leetcode上前者写法7ms,后者3ms。

posted @ 2020-03-27 20:54  豆豆323  阅读(263)  评论(0编辑  收藏  举报