看完 資料結構與演算法——遞迴-迷宮問題 後,我們對遞迴和回溯演算法有了一個基本的認識,本篇將講解 一個著名的問題:八皇后問題,能讓我們對遞迴和回溯有一個更深刻的認識。
八皇后問題,是一個古老而著名的問題,是 回溯演算法 的典型案例。
該問題是國際西洋棋棋手馬克斯·貝瑟爾於 1848 年提出:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,即:任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法。
高斯認為有 76 種方案。1854年在柏林的象棋雜誌上不同的作者發表了 40 種不同的解,後來有人用圖論的方法解出 92 種結果。計算機發明後,有多種計算機語言可以解決此問題。
可以去百度搜尋下這個小遊戲,自己玩幾下感受下,還是很難的
八皇后思路分析
用遞迴回溯演算法找出八皇后有多少種擺法,其實就是暴力窮舉,思路如下:
-
第一個皇后先放第一行第一列
-
第二個皇后放在第二行第一列,然後判斷是否符合規則,如果不符合規則,則繼續放在第 2 列,依次的測試下去,直到符合規則後,放第三個皇后。
-
繼續第 3 個皇后,直到 8 個皇后都放到了棋盤上,並且沒有違反規則,就算是一個解答
-
當得到一個正確解時,記錄下棋盤上的皇后的所有座標(在每一次放皇后符合規則後都會記錄本座標),方便我們列印檢視結果,在棧回退到上一個棧時,就會開始回溯,即將第一個皇后放在第一列的所有正確解全部拿到
-
然後回頭繼續第一個皇后放第二列,後面繼續迴圈執行前面 4 步,以此類推,當求完第一行的皇后在每個列上的所有答案,就是全部解了(一共92種)
如果這裡讀不明白,就繼續往下看,結合後面的細節分析和程式碼註釋 看懂程式碼,就好理解了。
理論上應該建立一個二維陣列來表示棋盤,但是實際上可以通過演算法,用一個一維陣列即可解決問題. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3}
,這個是儲存結果,陣列下標表示第幾行,對應的值則為在那一列上擺放著
/**
* 八皇后問題:
*
* 規則:在 8×8 格的國際象棋上擺放八個皇后,使其不能互相攻擊,
* 即:任意 兩個皇后 都不能處於同一行、同一列或同一斜線上,問有多少種擺法。
*
*/
public class Queue8 {
// 共有多少個皇后
int max = 8;
/**
* 存放皇后位置的結果
*
* 下標:表示棋盤中的某一行
* 對應的值:表示在這一行上,該皇后擺放在哪一列
* 比如:array[0] = 1,表示在第 1 行的第 2 列上擺放了一個皇后
*
* 由於規則,一行只能有一個皇后,所以可以使用一維陣列來代替二維陣列的棋盤結果
*
*/
int[] array = new int[max];
int count = 0; // 統計有多少個結果
public static void main(String[] args) {
Queue8 queue8 = new Queue8();
queue8.check(0); // 從第 1 行開始放置
}
/**
* 放置第 n 個(行)皇后
*特別注意: check 是 每一次遞迴時,進入到check中都有 for(int i = 0; i < max; i++),因此會有回溯
* @param n
*/
private void check(int n) {
// n = 8,那麼表示放第 9 個皇后,8 個皇后已經放完了(n 從0開始計數的)
// 表示找到了一個正確的結果,列印這個結果,並返回
if (n == max) {
count++;
print();
return;
}
// 開始暴力對比,從該行的第一列開始嘗試放置皇后,直到與前面所放置的不衝突
for (int i = 0; i < max; i++) {
// 在該行的第 i 列上放置一個皇后
array[n] = i;
// 檢測與已經放置的是否衝突
if (judge(n)) {
// 如果不衝突,則表示該行的皇后放置沒有問題
// 開始進入下一行的皇后放置
check(n + 1);
}
// 如果衝突,這裡什麼也不做
// 因為是從第一列開始放置,如果衝突,則嘗試放置到第 2 列上,直到放置成功
}
}
/**
* 按遊戲規則要求,判定要放置的這一個皇后,和前面已經擺放的位置是否衝突
*
* @param n 第 n 個皇后
* @return
*/
private boolean judge(int n) {
for (int i = 0; i < n; i++) {
if (
/*
如果他們的擺放位置一樣,說明是在同一列
注:圖是從下往上看
x .......
x .......
*/
array[i] == array[n]
/*
檢測是否是同一斜列
array[下標] = 值
下標: 代表的是第幾行
值:代表的是第幾列
注:圖是從下往上看
. x . . . . . . n = 1,value = 1
x . . . . . . . i = 0,value = 0
Math.abs(n - i) = 1
Math.abs(array[n] - array[i]) = 1
. . x . . . . . n = 1,value = 2
. x . . . . . . i = 0,value = 1
Math.abs(n - i) = 1
Math.abs(array[n] - array[i]) = 1
注:這裡運用了角的斜率問題,如果斜率為1,則角就是45°,所以兩點在一斜列上。
想不明白可以畫圖看看,如果兩點在一斜列上的話,就會組成一個正方形。很巧妙
*/
|| Math.abs(n - i) == Math.abs(array[n] - array[i])
) {
return false;
}
}
return true;
}
/**
* 列印皇后的位置
*/
private void print() {
System.out.printf("第 %02d 個結果 :", count);
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
/**
* 下面是對於判定是否是同行,同列、同一斜列的分步驟測試,比較好理解
*/
@Test
public void judgeTest() {
//注:圖是從下往上看
/*
* . . . . . . . .
* x . . . . . . .
*/
Queue8 queue8 = new Queue8();
queue8.array[0] = 0;
//======== 放置第 1 個皇后
// 判斷是否是同一列
/*
* x . . . . . . . // 計劃放到這裡
* x . . . . . . .
*/
queue8.array[1] = 0; // 從第一列開始放置
System.out.println("同一列,是否通過:" + queue8.judge(1));
/*
* . x . . . . . . // 計劃放到這裡
* x . . . . . . .
*/
queue8.array[1] = 1;
// 第一列不行,放置到第 2 列上
System.out.println("同一斜列,是否通過:" + queue8.judge(1));
/*
* . . x . . . . . // 計劃放到這裡
* x . . . . . . .
*/
queue8.array[1] = 2;
// 第 2 列不行,放置到第 3 列上,這個肯定是可以的
System.out.println("同一列或同一斜列,是否通過:" + queue8.judge(1));
//======== 放置第 3 個皇后
/*
* x . . . . . . . // 計劃放到這裡
* . . x . . . . .
* x . . . . . . .
*/
queue8.array[2] = 0;
// 與第一行的在同一列上了
System.out.println("同一列,是否通過:" + queue8.judge(2));
/*
* . x . . . . . . // 計劃放到這裡
* . . x . . . . .
* x . . . . . . .
*/
queue8.array[2] = 1;
// 第一列不行,放置到第 2 列
// 這裡與第 2 行的同一斜列了,也是不行的
System.out.println("同一斜列,是否通過:" + queue8.judge(2));
}
}
輸出結果如下
第 01 個結果 :0 4 7 5 2 6 1 3
第 02 個結果 :0 5 7 2 6 3 1 4
第 03 個結果 :0 6 3 5 7 1 4 2
第 04 個結果 :0 6 4 7 1 3 5 2
第 05 個結果 :1 3 5 7 2 0 6 4
第 06 個結果 :1 4 6 0 2 7 5 3
第 07 個結果 :1 4 6 3 0 7 5 2
第 08 個結果 :1 5 0 6 3 7 2 4
第 09 個結果 :1 5 7 2 0 3 6 4
第 10 個結果 :1 6 2 5 7 4 0 3
第 11 個結果 :1 6 4 7 0 3 5 2
第 12 個結果 :1 7 5 0 2 4 6 3
第 13 個結果 :2 0 6 4 7 1 3 5
第 14 個結果 :2 4 1 7 0 6 3 5
第 15 個結果 :2 4 1 7 5 3 6 0
第 16 個結果 :2 4 6 0 3 1 7 5
第 17 個結果 :2 4 7 3 0 6 1 5
第 18 個結果 :2 5 1 4 7 0 6 3
第 19 個結果 :2 5 1 6 0 3 7 4
第 20 個結果 :2 5 1 6 4 0 7 3
第 21 個結果 :2 5 3 0 7 4 6 1
第 22 個結果 :2 5 3 1 7 4 6 0
第 23 個結果 :2 5 7 0 3 6 4 1
第 24 個結果 :2 5 7 0 4 6 1 3
第 25 個結果 :2 5 7 1 3 0 6 4
第 26 個結果 :2 6 1 7 4 0 3 5
第 27 個結果 :2 6 1 7 5 3 0 4
第 28 個結果 :2 7 3 6 0 5 1 4
第 29 個結果 :3 0 4 7 1 6 2 5
第 30 個結果 :3 0 4 7 5 2 6 1
第 31 個結果 :3 1 4 7 5 0 2 6
第 32 個結果 :3 1 6 2 5 7 0 4
第 33 個結果 :3 1 6 2 5 7 4 0
第 34 個結果 :3 1 6 4 0 7 5 2
第 35 個結果 :3 1 7 4 6 0 2 5
第 36 個結果 :3 1 7 5 0 2 4 6
第 37 個結果 :3 5 0 4 1 7 2 6
第 38 個結果 :3 5 7 1 6 0 2 4
第 39 個結果 :3 5 7 2 0 6 4 1
第 40 個結果 :3 6 0 7 4 1 5 2
第 41 個結果 :3 6 2 7 1 4 0 5
第 42 個結果 :3 6 4 1 5 0 2 7
第 43 個結果 :3 6 4 2 0 5 7 1
第 44 個結果 :3 7 0 2 5 1 6 4
第 45 個結果 :3 7 0 4 6 1 5 2
第 46 個結果 :3 7 4 2 0 6 1 5
第 47 個結果 :4 0 3 5 7 1 6 2
第 48 個結果 :4 0 7 3 1 6 2 5
第 49 個結果 :4 0 7 5 2 6 1 3
第 50 個結果 :4 1 3 5 7 2 0 6
第 51 個結果 :4 1 3 6 2 7 5 0
第 52 個結果 :4 1 5 0 6 3 7 2
第 53 個結果 :4 1 7 0 3 6 2 5
第 54 個結果 :4 2 0 5 7 1 3 6
第 55 個結果 :4 2 0 6 1 7 5 3
第 56 個結果 :4 2 7 3 6 0 5 1
第 57 個結果 :4 6 0 2 7 5 3 1
第 58 個結果 :4 6 0 3 1 7 5 2
第 59 個結果 :4 6 1 3 7 0 2 5
第 60 個結果 :4 6 1 5 2 0 3 7
第 61 個結果 :4 6 1 5 2 0 7 3
第 62 個結果 :4 6 3 0 2 7 5 1
第 63 個結果 :4 7 3 0 2 5 1 6
第 64 個結果 :4 7 3 0 6 1 5 2
第 65 個結果 :5 0 4 1 7 2 6 3
第 66 個結果 :5 1 6 0 2 4 7 3
第 67 個結果 :5 1 6 0 3 7 4 2
第 68 個結果 :5 2 0 6 4 7 1 3
第 69 個結果 :5 2 0 7 3 1 6 4
第 70 個結果 :5 2 0 7 4 1 3 6
第 71 個結果 :5 2 4 6 0 3 1 7
第 72 個結果 :5 2 4 7 0 3 1 6
第 73 個結果 :5 2 6 1 3 7 0 4
第 74 個結果 :5 2 6 1 7 4 0 3
第 75 個結果 :5 2 6 3 0 7 1 4
第 76 個結果 :5 3 0 4 7 1 6 2
第 77 個結果 :5 3 1 7 4 6 0 2
第 78 個結果 :5 3 6 0 2 4 1 7
第 79 個結果 :5 3 6 0 7 1 4 2
第 80 個結果 :5 7 1 3 0 6 4 2
第 81 個結果 :6 0 2 7 5 3 1 4
第 82 個結果 :6 1 3 0 7 4 2 5
第 83 個結果 :6 1 5 2 0 3 7 4
第 84 個結果 :6 2 0 5 7 4 1 3
第 85 個結果 :6 2 7 1 4 0 5 3
第 86 個結果 :6 3 1 4 7 0 2 5
第 87 個結果 :6 3 1 7 5 0 2 4
第 88 個結果 :6 4 2 0 5 7 1 3
第 89 個結果 :7 1 3 0 6 4 2 5
第 90 個結果 :7 1 4 2 0 6 3 5
第 91 個結果 :7 2 0 5 1 4 6 3
第 92 個結果 :7 3 0 2 5 1 6 4
可以在百度找到 8皇后 這個遊戲,隨便選一兩個資料測試正確性,如果你想全部測試我沒意見,結果是對的,你開心就好。
細節分析
一定要理解它這個 回溯 機制,比如拿其中這兩個來分析(依據上面程式碼)
第 06 個結果 :1 4 6 0 2 7 5 3
第 07 個結果 :1 4 6 3 0 7 5 2
- 從第 1 行,到最後的第 8 行,這時 n+1 進入到第 9 行,發現已經放完 8 個皇后了,拿到一個完整的結果,
- 就返回到第 8 行的那個
check()
方法中的check(n+1)
哪裡,因為第 8 行放置結果是 3 是一個正確解,那麼回到這裡來的時候,就會嘗試剩下沒有測試的列,比如第 4 列 - 從結果來看,肯定不滿足,就一直會嘗試直到,第 8 行的 3 變成 7(從0開始計數),這個 check 方法嘗試完了,都沒有出一個結果,就自動退出這個方法
- 回到了第 7 行的第 5 列這個結果上,又繼續嘗試,像第 2 步和第 3 步那樣
- 從結果上來看,直到回到了第 4 行上,將 0 變成了 3,才發現這個位置與之前的不衝突,然後往下,進入到第 5 個皇后的放置上,重複遞迴操作
- 依次類推
其實這裡遞迴的原理就是窮舉法一樣的思想,挨個的嘗試,直到把所有的可能都嘗試完。就像手機密碼忘了,你一個一個去嘗試,暴力破解。
*有一個 細節需要知道,這裡的 不同行、不同列、不同斜列,不要求非連續的,也就是說,即使不是連續的斜列也算
x . . . . . . .
. . . . . . . .
. . x . . . . .
這樣不連續的斜列,也算是在一條斜線上。一定要明白這個規則,才能使用那個判定斜列的演算法
小結
-
重點 1 :判定是否在棋盤是符合規則:是否是同一列、同一行(同行實際上是不存在的,所以不用做判斷)、同一斜列
這裡使用一維陣列,來儲存二維陣列中的點的位置,使用如下的演算法,來檢驗這個規則,這裡設定的很巧妙
/* 如果他們的擺放位置一樣,說明是在同一列 注:圖是從下往上看 x ....... x ....... */ array[i] == array[n] /* 檢測是否是同一斜列 array[下標] = 值 下標: 代表的是第幾行 值:代表的是第幾列 注:圖是從下往上看 . x . . . . . . n = 1,value = 1 x . . . . . . . i = 0,value = 0 Math.abs(n - i) = 1 Math.abs(array[n] - array[i]) = 1 . . x . . . . . n = 1,value = 2 . x . . . . . . i = 0,value = 1 Math.abs(n - i) = 1 Math.abs(array[n] - array[i]) = 1 注:這裡運用了角的斜率問題,如果斜率為1,則角就是45°,所以兩點在一斜列上。 想不明白可以畫圖看看,如果兩點在一斜列上的話,就會組成一個正方形。很巧妙 */ || Math.abs(n - i) == Math.abs(array[n] - array[i])
-
重點 2:遞迴的回溯流程,一定要明白是怎麼回溯的