八皇后問題是一個經典的遞歸回溯問題。
描述
八皇后問題是在一個 8*8 的棋盤上放置皇后,要求其放置后滿足同一行,同一列,同一對角線上沒有重復的皇后出現。試問有多少種擺盤方式?
思路
我們的主要思路是通過一行一行的放置皇后,來使得每一行都有一個皇后。當然,這些皇后在放置時都必須要滿足規定的要求才行。
因此就會出先如下情況:
- 放置時不符合規則,繼續檢索同一行的下一列位置是否合理
- 如果符合規則就將其放置,然后進行下一行的嘗試(遞歸)
- 如果有某一行沒有可行的解,則退回上一行,消除上一行擺放的皇后,檢索剩余的列,看是否有合理的位置,然后繼續進行。(回溯)
- 直到所有的行都被放置為止。
需要注意的是,我們在放置皇后時需要檢測其防止和理性的判斷條件為:
- 同一列的上方所有行中是否有皇后
- 左上方對角線上是否有皇后
- 右上方對角線上是否有皇后
算法實現
public class EightQueen {
private static final int num = 8; // 可以拓展為N皇后問題
private static int[][] item = new int[num][num];
private static int methods = 0; // 總方法數
public static void main(String[] args) {
buildQueen(0);
System.out.println(methods);
}
/**
* 構建棋盤的第row行
*
* @param row
*/
private static void buildQueen(int row) {
if (row == num) {
methods++;
// System.out.println("第" + methods + "種解法:");
// for (int i = 0; i < num; i++) {
// for (int j = 0; j < num; j++) {
// System.out.print(item[i][j] + " ");
// }
// System.out.print("\n");
// }
return;
} else {
for (int col = 0; col < num; col++) { // 每一列進行檢查,試探性放置
if (isSatisfy(row, col)) {
item[row][col] = 1;
buildQueen(row + 1);
item[row][col] = 0;
}
}
}
}
/**
* 檢查row行col列元素是否滿足要求
* 因為是一行行的放置皇后,所以不需要檢測同一行是否存在重復皇后
* 在判斷重復元素時,只需要判斷上半部分的區域即可
*
* @param row
* @param col
* @return
*/
private static boolean isSatisfy(int row, int col) {
for (int i = 0; i < row; i++) {
if (item[i][col] == 1) { // 同一列的上方元素
return false;
}
}
for (int i = row, j = col; i >= 0 && j >= 0; i--, j--) { // 左上方斜對角線
if (item[i][j] == 1) {
return false;
}
}
for (int i = row, j = col; i >= 0 && j < num; i--, j++) { // 右上方斜對角線
if (item[i][j] == 1) {
return false;
}
}
return true;
}
}
優雅的位運算解法
我們直接從一個例子來講解思路吧。先看看下圖的情況:
我們可以看到,前三行已經放置了皇后,我們需要在第四行選擇放置皇后的點。陰影部分表示會出現沖突的格子,而沖突我們主要分為三種:同列沖突、右下方沖突和左下方沖突。
而就這對這種情況而言(此例為八皇后問題,可拓展到N皇后),一行剛好8個格子,對應8位二進制數字。因此我們可以首先定義沖突:
同列沖突: A = 1000 1001;
右下沖突: B = 0001 0010;
左下沖突: C = 0010 0010;
其中1表示沖突的格子,0表示可以放置皇后的格子。因此我們可以輕松得出綜合的沖突情況:
D = (A | B | C) = 1011 1011;
對于我們將要放置的第四行而言,現在有兩個0,意味著有兩個可以放置皇后的位置,我們需要將所有的情況都考慮到,這里有一個神奇的式子:bit = (D + 1) & ~D; 它計算得出的結果是: 0000 0100;
其實它能夠得到最右邊一個可以放置皇后的位置,并用1來表示,其余位是0。 這樣做是有好處的...
我們現在得出 bit = 0000 0100,便能夠輕松得到下一行的沖突 A' = (A | bit); B' = (B | bit) >> 1; C' = (C | bit) << 1; 便能夠很輕易地寫出遞歸式了。
而我們的第4行試探其實并沒有結束,只是從左向右的第一個可以放置的位置進行了試探,那想要取到第二個可以放置的位置怎么辦呢?很簡單,只需要做如下運算:
D = D + bit 將剛才試過的那一位設置為不能放置皇后狀態,然后繼續做 bit = (D + 1) & ~D 即可。
一直循環的試探,知道D 全部為1 為止。
下面是整個程序的代碼:
public class NQueen {
private static final int N = 8; // 皇后數量,可拓展為N皇后
private static int count = 0; // 總方法數
private static int limit;
public static void main(String[] args) {
limit = (1 << N) - 1;
backtracking(0, 0, 0, 0);
System.out.println(count);
}
private static void backtracking(int a, int b, int c, int depth) {
if (depth == N) {
count++;
return;
}
int d = a | b | c;
while (d < limit) {
int bit = (d + 1) & ~d;
backtracking(a | bit, limit & ((b | bit) >> 1), limit & ((c | bit) << 1), depth + 1);
d |= bit;
}
}
}