本內容轉載來自:回溯法求解N皇后問題(Java實現)
回溯法:也稱爲試探法,它並不考慮問題規模的大小,而是從問題的最明顯的最小規模開始逐步求解出可能的答案,並以此慢慢地擴大問題規模,迭代地逼近最終問題的解。這種迭代類似於窮舉並且是試探性的,因爲當目前的可能答案被測試出不可能可以獲得最終解時,則撤銷當前的這一步求解過程,回溯到上一步尋找其他求解路徑。
爲了能夠撤銷當前的求解過程,必須保存上一步以來的求解路徑,這一點相當重要。
光說不做沒意思,用學過的算法題來運用一下。
N 皇后問題:在一個 N * N 的國際象棋棋盤中,怎樣放置 N 個皇后才能使 N 個皇后之間不會互相有威脅而共同存在於棋局中,即在 N * N 個格子的棋盤中沒有任何兩個皇后是在同一行、同一列、同一斜線上。
求解思路:最容易想到的方法就是有序地從第 1 列的第 1 行開始,嘗試放上一個皇后,然後再嘗試第 2 列的第幾行能夠放上一個皇后,如果第 2 列也放置成功,那麼就繼續放置第 3 列,如果此時第 3 列沒有一行可以放置一個皇后,說明目前爲止的嘗試是無效的(即不可能得到最終解),那麼此時就應該回溯到上一步(即第
2 步),將上一步(第 2 步)所放置的皇后的位置再重新取走放在另一個符合要求的地方…如此嘗試性地遍歷加上回溯,就可以慢慢地逼近最終解了。
需要解決的問題:如何表示一個 N * N 方格棋盤能夠更有效?怎樣測試當前所走的試探路徑是否符合要求?這兩個問題都需要考慮到使用怎樣的數據結構,使用恰當的數據結構有利於簡化編程求解問題的難度。
爲此,我們使用以下的數據結構:
int column[col] = row 表示第 col 列的第 row 行放置一個皇后
boolean rowExists[i] = true 表示第 i 行有皇后
boolean a[i] = true 表示右高左低的第
i 條斜線有皇后(按 → ↓ 順序從1~ 2*N -1 依次編號)
boolean b[i] = true 表示左高右低的第
i 條斜線有皇后(按 → ↑ 順序從1~ 2*N -1 依次編號)
對應這個數據結構的算法實現如下:
/**
* 回溯法求解 N 皇后問題
* @author haolloyin
*/
public class N_Queens {
// 皇后的個數
private int queensNum = 5;
// column[i] = j 表示第 i 列的第 j 行放置一個皇后
private int[] queens = new int[queensNum + 1];
// rowExists[i] = true 表示第 i 行有皇后
private boolean[] rowExists = new boolean[queensNum + 1];
// a[i] = true 表示右高左低的第 i 條斜線有皇后
private boolean[] a = new boolean[queensNum * 2];
// b[i] = true 表示左高右低的第 i 條斜線有皇后
private boolean[] b = new boolean[queensNum * 2];
// 初始化變量
private void init() {
for (int i = 0; i < queensNum + 1; i++) {
rowExists[i] = false;
}
for(int i = 0; i < queensNum * 2; i++) {
a[i] = b[i] = false;
}
}
// 判斷該位置是否已經存在一個皇后,存在則返回 true
private boolean isExists(int row, int col) {
return (rowExists[row] || a[row + col - 1] || b[queensNum + col - row]);
}
// 主方法:測試放置皇后
public void testing(int column) {
// 遍歷每一行
for (int row = 1; row < queensNum + 1; row++) {
// 如果第 row 行第 column 列可以放置皇后
if (!isExists(row, column)) {
// 設置第 row 行第 column 列有皇后
queens[column] = row;
// 設置以第 row 行第 column 列爲交叉點的斜線不可放置皇后
rowExists[row] = a[row + column - 1] = b[queensNum + column - row] = true;
// 全部嘗試過,打印
if(column == queensNum) {
for(int col = 1; col <= queensNum; col++) {
System.out.print("("+col + "," + queens[col] + ") ");
}
System.out.println();
}else {
// 放置下一列的皇后
testing(column + 1);
}
// 撤銷上一步所放置的皇后,即回溯
rowExists[row] = a[row + column - 1] = b[queensNum + column - row] = false;
}
}
}
// 測試
public static void main(String[] args) {
N_Queens queen = new N_Queens();
queen.init();
// 從第 1 列開始求解
queen.testing(1);
}
}
當 N = 5 時,求解結果如下(注:橫座標爲
列數, 縱座標爲 行數):
(1,1) (2,3) (3,5) (4,2) (5,4)
(1,1) (2,4) (3,2) (4,5) (5,3)
(1,2) (2,4) (3,1) (4,3) (5,5)
(1,2) (2,5) (3,3) (4,1) (5,4)
(1,3) (2,1) (3,4) (4,2) (5,5)
(1,3) (2,5) (3,2) (4,4) (5,1)
(1,4) (2,1) (3,3) (4,5) (5,2)
(1,4) (2,2) (3,5) (4,3) (5,1)
(1,5) (2,2) (3,4) (4,1) (5,3)
(1,5) (2,3) (3,1) (4,4) (5,2)
當 N = 4 時,求解結果如下:
(1,2) (2,4) (3,1) (4,3)
(1,3) (2,1) (3,4) (4,2)
有時間的話將輸出的結果打印爲直觀一點的符號形式或界面形式更好。
小結:
1、根據問題選擇恰當的數據結構非常重要,就像上面 a 、b 標誌數組來表示每一條斜線的編號順序以及方向都相當重要。看書的時候也是費了些時間來理解的,呼…另外,queens [col] = row 數組只是用了一維而不是二維來表示縱橫交錯的方格棋盤上特定位置是否有皇后也是比較經濟而有意思的。
2、正確運用、組織所確定的數據結構到算法的實現邏輯中也是很重要的,就像代碼中的 isExists(int row, int col) 方法內的 (rowExists[row] || a[row + col - 1] || b[queensNum + col - row]) 就是很明確的理解了嘗試放置皇后的位置的
x ,y 座標與斜線之間的數值關係,才使得算法得以正確執行。當然,對於斜線的編號、順序也是相當重要的。