leetcode刷題之——N皇后
題目:
n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。
上圖為 8 皇后問題的一種解法。
給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。
每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 'Q'
和 '.'
分別代表了皇后和空位。
示例:
輸入: 4 輸出: [ [".Q..", // 解法 1 "...Q", "Q...", "..Q."], ["..Q.", // 解法 2 "Q...", "...Q", ".Q.."] ] 解釋: 4 皇后問題存在兩個不同的解法。
思路:在看到這類題目的時候,首先想到的就是:圖搜尋。這裡我使用的就是圖搜尋,使用回溯演算法進行解答。而在圖搜尋中選擇深度優先搜尋的難點主要在於以下幾點:
1、結束時,記錄返回結果
2、抉擇之後對應狀態的改變
3、還原現場
其中對於困難點1,我們可以通過建立全域性變數List<List<String>> 物件來進行填充,而dfs()的返回值可以置為void。
對於困難點2是有一點麻煩的。我們可以通過對列進行搜尋,每次dfs的時候將列+1(idx + 1),但是對於行,和斜線,我們需要專門考慮,為此我們可以再設立幾個全域性的陣列來記錄對應的行和斜線的狀態。
困難點3,這個點是容易忽視掉的,類似於深搜和遞迴等,在改呼叫的方法結束的時候記得要還原現場,將狀態還原,避免影響之後的操作·。
以下是虛擬碼:
List<List<String>> ans = new ArrayList<List<Stirng>>(); int[] path = new int[n]; void dfs(int idx,int n)// idx當前遍歷的列,n表示棋盤的邊長 if(idx >= n) // 邊界條件,當搜尋到邊界的時候,也就是改輸出結果的時候 // 記錄結果 並結束 return ; for(i...n)//對於每一列,皇后所在的行都是未知的、 if(狀態是否符合搜尋條件) //剪枝判斷 path[idx] = i; //表示該列所對應的行 //記錄狀態 dfs(idx+1,n); //還原狀態 public List<List<Stirng>> solveNQueens(int n){ dfs(0,n); return ans }
觀上面的虛擬碼,可以知道主要難點在於剪枝判斷和記錄結果。其實記錄結果還是很簡單的,只需要遍歷整個棋盤,記錄該列所對應的行並標記為Q就行。所以,對於整個演算法的難點就落在了剪枝操作和記錄狀態上面了。因為剪枝操作其實就是判斷對應的狀態,所以所有的難點也就是記錄狀態。
記錄狀態的難點:由於皇后可以走八個方向,四條直線。因此可以歸結為一下四種情況
1、行
2、列
3、正斜線
4、反斜線
由於我們的搜尋是以列展開的,因此列這條直線可以直接省去。而行的考慮還是比較簡單的,我們只需要在記錄狀態的時候標記改行已經被佔用就可以了。可以定義一個全域性變數rows[i] = true,來表示改行是否被佔用,記得之後要還原現場。
對於正斜線和反斜線,我們就要仔細考慮了。我們可以參照行進行操作。由於這個狀態的記錄主要是表示那些未知被皇后佔用了。所以我們可以將斜線也類似於行一樣標記出來。對於一個n*n的棋盤來說正斜線和反斜線的數量都是 2n-1。而我們想要將被佔用的斜線表示出來,可以才用數學中的思維來進行解決,構件座標軸如下:
雖然醜了點,不過可以看= =。我們將正斜線作為x+y=k固定值,那麼對於不同的k值,即可以表示為不同的正斜線。那麼對於正斜線的全域性變數slash陣列的下標可以記為slash[x+y],即slash[idx + i] = true;而反斜線的方程是-x+y = k。同理可以記為backlash[idx-i] = true;不過idx - i可能是負數,所以我們要將其設定為正數,不然下標就出錯了,因此可以定為backlash[idx-i+n-i] = true;記錄完狀態之後,再呼叫dfs之後需要還原現場。
由以上的思考方式最後可以得到最後的程式碼為以下:
List<List<String>> ans = new ArrayList<List<String>>();
int[] row;
boolean[] line;
boolean[] slash;
boolean[] backlash;
/**
* 對列進行深度優先搜尋
* @param idx
* @param n
*/
void dfs(int idx,int n){
//結束條件 記錄結果
if(idx >= n){
List<String> tmp = new ArrayList<String>();
for(int i = 0 ; i < n ; i++){
String str = "";
for(int j = 0 ; j < n ; j++){
if(row[i] == j){
str +="Q";
}else{
str +=".";
}
}
tmp.add(str);
}
ans.add(tmp);
return ;
}
for(int i = 0 ; i < n ; i++){
if(!line[i] && !slash[idx + i] && !backlash[idx-i+n-1]){
row[idx] = i;
line[i] = true;
slash[i+idx] = true;
backlash[idx-i+n-1] = true;
dfs(idx + 1 , n);
line[i] = false; // 還原現場
slash[i+idx] = false;
backlash[idx-i+n-1] = false;
}
}
}
public List<List<String>> solveNQueens(int n) {
row = new int[n];
line = new boolean[n];
slash = new boolean[2*n];
backlash = new boolean[2*n];
dfs(0,n);
return ans;
}
這道題目還是挺有意思的,主要是很有做的意義。在剛剛拿到題目的時候感覺考慮的點有很多,但是在一步一步解析之後,再看這道題目,就會發現並沒有多難。而且這道題目很具有代表性,可以說是圖的深搜的代表了。筆者目前也是慢慢在學習圖的相關演算法,解題思路比較簡陋,程式碼還有很多可以優化的點。歡迎大家在閱讀完之後,如果有什麼想法和意見的話,能夠及時告訴我,我好及時修正。