很久沒聊算法啦!此次咱們來聊聊n皇后問題。n 皇后問題,研究的是如何將 n 個皇后放置在 n×n 的棋盤上,而且使皇后彼此之間不能相互攻擊。好多同窗對這樣的問題都比較慌張,以爲規則多燒腦抗拒,祈禱面試中不要遇到,別急,咱們今天就來嘗試把這其中的邏輯給說道說道。面試
一個皇后能夠向水平、垂直以及向斜對角方向移動,若是一個皇后出如今另外一個皇后的同一行,同一列或者斜對角,那它就能夠被這個皇后攻擊。算法
那咱們直覺裏的一個比較粗暴的辦法,就是列舉出在棋盤上全部的狀況,而後咱們判斷每一個組合是否是符合咱們的條件。可是實際上咱們不須要嘗試全部的組合,咱們知道當咱們在某一列上放置了一個皇后以後,其它的皇后就不能放在這一列了,在它的同一個水平線上跟四個斜對角也放不了。這樣咱們能夠最先發現「此路不通」。數組
當咱們發現一條路行不通時,咱們趕忙回到前面一步,而後嘗試另一個不一樣的選擇,咱們稱之爲回溯
。安全
針對n皇后問題咱們把這個思路再展開一下:markdown
是否是還以爲有點抽象?函數
那咱們拿其中一個場景來具體說說:優化
如今是否是以爲眼睛會了?🤔,接下來咱們可讓手來試試了。spa
首先咱們須要一個方法來判斷某一個位置能不能放皇后,這樣若是一個位置會被棋盤上已有的皇后攻擊的話,咱們能夠直接跳過這個位置:code
// 這個方法用來判斷咱們接下來要放置的皇后在不在某個已經放置的皇后的水平方向、垂直方向或者斜對角,
// 若是都不,那咱們找到了一個合適的位置來放一個新皇后了
static boolean isValidPosition(int proposedRow, int proposedCol, List<Integer> solution) {
//對當前棋盤上的全部皇后,咱們都要作判斷
for (int oldRow = 0; oldRow < proposedRow; ++oldRow) {
int oldCol = solution.get(oldRow);
int diagonalOffset = proposedRow - oldRow;
if (oldCol == proposedCol ||
oldCol == proposedCol - diagonalOffset ||
oldCol == proposedCol + diagonalOffset) {
return false;
}
}
return true;
}
複製代碼
有了這個方法以後,咱們就須要實現逐行搜索以及在全部路不通時回到前一行的搜索裏面來,繼續尋找其它可能性。每一行的搜索方式都一致,因此這邊適合使用遞歸來實現咱們的邏輯:orm
static void solveNQueensRec(int n, List<Integer> solution, int row, List<List<Integer>> results) {
// 最後一行也找到了一個合適的位置,咱們成功找到了一種解決方案
if (row == n) {
results.add(new ArrayList<Integer>(solution));
return;
}
// 從每一行的第一列開始嘗試
for (int i = 0; i < n; ++i) {
// 對於走到最後一列還沒都沒有找到合適的點的狀況, 當前遞歸結束,調用棧回到上一層的遞歸流程,會回去執行前面一行裏剩餘的狀況
if (isValidPosition(row, i, solution)) {
solution.set(row, i);
solveNQueensRec(n, solution, row + 1, results);
}
}
}
複製代碼
當有條路走不通時,調用棧裏當前遞歸就執行結束了,它會回到上一個遞歸的調用邏輯裏,也就實現了咱們的回溯
。咱們的目的很簡單,這一行走到最後沒路走了,就繼續回到前一行繼續日後走,直到全部的路都嘗試過。
最後執行一下咱們的遞歸函數就好啦:
static int solveNQueens(int n, List<List<Integer>> results) {
List<Integer> solution = new ArrayList<Integer>(n);
for (int i = 0; i < n; ++i) {
solution.add(-1);
}
solveNQueensRec(n, solution, 0, results);
return results.size();
}
複製代碼
這邊至關於從N×N的數組裏,選出N個數,時間複雜度是O(n^n)
,而空間複雜度是O(n!)
。
上面咱們搜索的過程當中,一行一行上升去尋找合適的位置,而後在某個條件下又回到前一行,有點像棧的入棧出棧操做,其實咱們也是能夠用棧來實現整個回溯過程的。咱們在某一行裏找到一個合適的位置時就把它的列push
到棧中,回溯到前一行時再把它pop
出來。
// 這邊棧裏咱們只存放列的值
static int solveNQueens(int n, List<List<Integer>> results) {
List<Integer> solution = new ArrayList<Integer>(n);
Stack<Integer> solStack = new Stack<Integer>();
for (int i = 0; i < n; ++i) {
solution.add(-1);
}
int row = 0;
int col = 0;
while(row < n){
while(col < n){
// 當前能夠放置皇后,繼續下一行的尋找
if(isValidPosition(row, col, solution)){
solStack.push(col);
solution.set(row, col);
row++;
col = 0;
break;
}
// 當前位置不行,嘗試在下一列放置皇后
col++;
}
// 找到了當前行的最後一列
if(col == n){
// 說明前面一行還沒到最後一列,執行pop操做回到前一行尋找過程
if(!solStack.empty()){
col = solStack.peek() + 1;
solStack.pop();
row--;
}
else{
// 只有第一行也走到了最後一列,而且全部路徑都嘗試過了,此時因爲上面if裏的邏輯棧空了,說明咱們的尋找過程該結束了
break;
}
}
if(row == n){
// 咱們找到一種符合條件的擺放位置
results.add(new ArrayList<Integer>(solution));
// 回溯到前一行
row--;
col = solStack.peek() + 1;
solStack.pop();
}
}
return results.size();
}
複製代碼
時間複雜度是O(n^n)
,空間複雜度是O(n!)
。這邊整個邏輯仍是比較直接的,咱們依舊須要isValidPosition
這個輔助方法來判斷某個位置能不能放置皇后,而後對每一個位置逐一判斷,用棧來配合尋找過程當中的回溯操做,核心思想仍是不變的。
咱們兩種辦法裏都把全部的符合規則的擺放記錄下來了,若是咱們只須要最後求得有多少種可能性,那咱們其實能夠把數組換成一個變量來計數,這樣咱們的空間複雜度能夠優化成O(n)
。
好啦,相信你們這會兒對回溯算法
有了一個感性的認識,也能明白回溯
只是咱們面對問題時常規的思路,並非什麼高大上的概念,咱們不用去畏懼它~