八皇后问题是一个以国际象棋为背景的问题:如何能够在 8×8 的国际象棋棋盘上放置八个皇后,使得任何一个皇后都无法直接吃掉其他的皇后?为了达到此目的,任两个皇后都不能处于同一条横行、纵行或斜线上。八皇后问题可以推广为更一般的n皇后摆放问题:这时棋盘的大小变为n1×n1,而皇后个数也变成n2。而且仅当 n2 ≥ 1 或 n1 ≥ 4 时问题有解。
皇后问题是非常著名的问题,作为一个棋盘类问题,毫无疑问,用暴力搜索的方法来做是一定可以得到正确答案的,但在有限的运行时间内,我们很难写出速度可以忍受的搜索,部分棋盘问题的最优解不是搜索,而是动态规划,某些棋盘问题也很适合作为状态压缩思想的解释例题。
进一步说,皇后问题可以用人工智能相关算法和遗传算法求解,可以用多线程技术缩短运行时间。本文不做讨论。
(本文不展开讲状态压缩,以后再说)
一般思路:
N*N的二维数组,在每一个位置进行尝试,在当前位置上判断是否满足放置皇后的条件(这一点的行、列、对角线上,没有皇后)。
优化1:
既然知道多个皇后不能在同一行,我们何必要在同一行的不同位置放多个来尝试呢?
我们生成一维数组record,record[i]表示第i行的皇后放在了第几列。对于每一行,确定当前record值即可,因为每行只能且必须放一个皇后,放了一个就无需继续尝试。那么对于当前的record[i],查看record[0...i-1]的值,是否有j = record[k](同列)、|record[k] - j| = | k-i |(同一斜线)的情况。由于我们的策略,无需检查行(每行只放一个)。
public class NQueens {
public static int num1(int n) {
if (n < 1) {
return 0;
}
int[] record = new int[n];
return process1(0, record, n);
}
public static int process1(int i, int[] record, int n) {
if (i == n) {
return 1;
}
int res = 0;
for (int j = 0; j < n; j++) {
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);
}
}//对于当前行,依次尝试每列
return res;
}
//判断当前位置是否可以放置
public static boolean isValid(int[] record, int i, int j) {
for (int k = 0; k < i; k++) {
if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
public static void main(String[] args) {
int n = 8;
System.out.println(num1(n));
}
}
优化2:
分析:棋子对后续过程的影响范围:本行、本列、左右斜线。
黑色棋子影响区域为红色
本行影响不提,根据优化一已经避免
本列影响,一直影响D列,直到第一行在D放棋子的所有情况结束。
左斜线:每向下一行,实际上对当前行的影响区域就向左移动
比如:
尝试第二行时,黑色棋子影响的是我们的第三列;
尝试第三行时,黑色棋子影响的是我们的第二列;
尝试第四行时,黑色棋子影响的是我们的第一列;
尝试第五行及以后几行,黑色棋子对我们并无影响。
右斜线则相反:
随着行序号增加,影响的列序号也增加,直到影响的列序号大于8就不再影响。
我们对于之前棋子影响的区域,可以用二进制数字来表示,比如:
每一位,用01代表是否影响。
比如上图,对于第一行,就是00010000
尝试第二行时,数字变为00100000
第三行:01000000
第四行:10000000
对于右斜线的数字,同理:
第一行00010000,之后向右移:00001000,00000100,00000010,00000001,直到全0不影响。
同理,我们对于多行数据,也同样可以记录了
比如在第一行我们放在了第四列:
第二行放在了G列,这时左斜线记录为00100000(第一个棋子的影响)+00000010(当前棋子的影响)=00100010。
到第三行数字继续左移:01000100,然后继续加上我们的选择,如此反复。
这样,我们对于当前位置的判断,其实可以通过左斜线变量、右斜线变量、列变量,按位或运算求出(每一位中,三个数有一个是1就不能再放)。
具体看代码:
注:怎么排版就炸了呢。。。贴一张图吧
public class NQueens {
public static int num2(int n) {
// 因为本方法中位运算的载体是int型变量,所以该方法只能算1~32皇后问题
// 如果想计算更多的皇后问题,需使用包含更多位的变量
if (n < 1 || n > 32) {
return 0;
}
int upperLim = n == 32 ? -1 : (1 << n) - 1;
//upperLim的作用为棋盘大小,比如8皇后为00000000 00000000 00000000 11111111
//32皇后为11111111 11111111 11111111 11111111
return process2(upperLim, 0, 0, 0);
}
public static int process2(int upperLim, int colLim, int leftDiaLim,
int rightDiaLim) {
if (colLim == upperLim) {
return 1;
}
int pos = 0; //pos:所有的合法位置
int mostRightOne = 0; //所有合法位置的最右位置
//所有记录按位或之后取反,并与全1按位与,得出所有合法位置
pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
int res = 0;//计数
while (pos != 0) {
mostRightOne = pos & (~pos + 1);//取最右的合法位置
pos = pos - mostRightOne; //去掉本位置并尝试
res += process2(
upperLim, //全局
colLim | mostRightOne, //列记录
//之前列+本位置
(leftDiaLim | mostRightOne) << 1, //左斜线记录
//(左斜线变量+本位置)左移
(rightDiaLim | mostRightOne) >>> 1); //右斜线记录
//(右斜线变量+本位置)右移(高位补零)
}
return res;
}
public static void main(String[] args) {
int n = 8;
System.out.println(num2(n));
}
}
完整测试代码:
32皇后:结果/时间
暴力搜:时间就太长了,懒得测。。。
public class NQueens {
public static int num1(int n) {
if (n < 1) {
return 0;
}
int[] record = new int[n];
return process1(0, record, n);
}
public static int process1(int i, int[] record, int n) {
if (i == n) {
return 1;
}
int res = 0;
for (int j = 0; j < n; j++) {
if (isValid(record, i, j)) {
record[i] = j;
res += process1(i + 1, record, n);
}
}
return res;
}
public static boolean isValid(int[] record, int i, int j) {
for (int k = 0; k < i; k++) {
if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {
return false;
}
}
return true;
}
public static int num2(int n) {
if (n < 1 || n > 32) {
return 0;
}
int upperLim = n == 32 ? -1 : (1 << n) - 1;
return process2(upperLim, 0, 0, 0);
}
public static int process2(int upperLim, int colLim, int leftDiaLim,
int rightDiaLim) {
if (colLim == upperLim) {
return 1;
}
int pos = 0;
int mostRightOne = 0;
pos = upperLim & (~(colLim | leftDiaLim | rightDiaLim));
int res = 0;
while (pos != 0) {
mostRightOne = pos & (~pos + 1);
pos = pos - mostRightOne;
res += process2(upperLim, colLim | mostRightOne,
(leftDiaLim | mostRightOne) << 1,
(rightDiaLim | mostRightOne) >>> 1);
}
return res;
}
public static void main(String[] args) {
int n = 32;
long start = System.currentTimeMillis();
System.out.println(num2(n));
long end = System.currentTimeMillis();
System.out.println("cost time: " + (end - start) + "ms");
start = System.currentTimeMillis();
System.out.println(num1(n));
end = System.currentTimeMillis();
System.out.println("cost time: " + (end - start) + "ms");
}
}