生活日用算法——八皇后问题
八皇后问题也算是比较经典的回溯算法的经典案例。题干描述如下:
在 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。