在個人上一篇文章《前端電商 sku 的全排列算法很難嗎?學會這個套路,完全掌握排列組合。》中詳細的講解了排列組合的遞歸回溯解法,相信看過的小夥伴們對這個套路已經有了必定程度的掌握(沒看過的同窗快回頭學習~)。javascript
這是一道 LeetCode 上難度爲 hard 的題目,聽起來很嚇人,可是看過我上一篇文章的同窗應該還記得我有提到過,我解決電商 sku 問題用的是排列組合的萬能模板,這個萬能模板可否用來解決這個經典的計算機問題「N 皇后」呢?答案是確定的。前端
先來看問題,其實問題不難理解:java
n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,而且使皇后彼此之間不能相互攻擊。git
上圖爲 8 皇后問題的一種解法。github
給定一個整數 n,返回全部不一樣的 n 皇后問題的解決方案。面試
每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 'Q' 和 '.' 分別表明了皇后和空位。算法
示例:數組
輸入: 4
輸出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解釋: 4 皇后問題存在兩個不一樣的解法。
複製代碼
提示:bash
皇后,是國際象棋中的棋子,意味着國王的妻子。皇后只作一件事,那就是「吃子」。當她碰見能夠吃的棋子時,就迅速衝上去吃掉棋子。固然,她橫、豎、斜均可走一到七步,可進可退。(引用自 百度百科 - 皇后 )函數
乍一看這種選出所有方案的問題有點難找到頭緒,可是其實仔細看一下,題目已經限定了皇后之間不能互相攻擊,轉化成代碼思惟的語言其實就是說每一行只能有一個皇后,每條對角線上也只能有一個皇后,
也就是說:
[
'Q', 0
'Q', 0
]
複製代碼
[
'Q', 0
0, 'Q'
]
複製代碼
[
0, 'Q'
'Q', 0
]
複製代碼
那麼以這個思路爲基準,咱們就能夠把這個問題轉化成一個「逐行放置皇后」的問題,思考一下遞歸函數應該怎麼設計?
對於 n皇后
的求解,咱們能夠設計一個接受以下參數的函數:
rowIndex
參數,表明當前正在嘗試第幾行放置皇后。prev
參數,表明以前的行已經放置的皇后位置,好比 [1, 3]
就表明第 0 行(數組下標)的皇后放置在位置 1,第 1 行的皇后放置在位置 3。當 rowIndex === n
即說明這個遞歸成功的放置了 n 個皇后,一路暢通無阻的到達了終點,每次的放置都順利的經過了咱們的限制條件,那麼就把此次的 prev
作爲一個結果放置到一個全局的 res
結果數組中。
這裏我嘗試用工具畫出了 4皇后
的其中的一個解遞歸的樹狀圖,第一行我直接選擇了以把皇后放在2
爲起點,省略了以 放在1
、放在3
、放在4
爲起點的樹狀圖,不然遞歸樹太大了圖片根本放不下。
注意這裏的 放在x
,爲了方便理解,這個 x
並非數組下標,而是從 1 開始的計數。
在此次遞歸以後,就求出了一個結果:[1, 3, 0, 2]
。
你能夠在紙上按照個人這種方式繼續畫一畫嘗試以其餘起點開始的解法,來看看這個算法的具體流程。
理想老是美好的,雖然目前爲止咱們的思路很清晰了,可是具體的編碼仍是會遇到幾個頭疼的問題的。
當前一行已經落下一個皇后以後,下一行須要判斷三個條件:
難點在於判斷對角線上是否擺放過皇后了,其實找到規律後也不難了,看圖:
對角線1
:
直接經過這個點的橫縱座標 rowIndex + columnIndex
相加,相等的話就在同在對角線 1 上:
對角線2
:
直接經過這個點的橫縱座標 rowIndex - columnIndex
相減,相等的話就在同在對角線 2 上:
因此:
columns
數組記錄擺放過的列下標,擺放事後直接標記爲 true 便可。dia1
數組記錄擺放過的對角線 1下標,擺放事後直接把下標 rowIndex + columnIndex
標記爲 true 便可。dia2
數組記錄擺放過的對角線 2下標,擺放事後直接把下標 rowIndex - columnIndex
標記爲 true 便可。prev
表明每一行中皇后放置的列數,好比 prev[0] = 3
表明第 0 行皇后放在第 3 列,以此類推。有了這幾個輔助知識點,就能夠開始編寫遞歸函數了,在每一行,咱們都不斷的嘗試一個座標點,只要它和以前已有的結果都不衝突,那麼就能夠放入數組中做爲下一次遞歸的開始值。
這樣,若是遞歸函數順利的來到了 rowIndex === n
的狀況,說明以前的條件所有知足了,一個 n皇后
的解就產生了。把 prev
這個一維數組經過輔助函數恢復成題目要求的完整的「二維數組」便可。
/** * @param {number} n * @return {string[][]} */
let solveNQueens = function (n) {
let res = []
// 已擺放皇后的的列下標
let columns = []
// 已擺放皇后的對角線1下標 左下 -> 右上
// 計算某個座標是否在這個對角線的方式是「行下標 + 列下標」是否相等
let dia1 = []
// 已擺放皇后的對角線2下標 左上 -> 右下
// 計算某個座標是否在這個對角線的方式是「行下標 - 列下標」是否相等
let dia2 = []
// 在選擇當前的格子後 記錄狀態
let record = (rowIndex, columnIndex, bool) => {
columns[columnIndex] = bool
dia1[rowIndex + columnIndex] = bool
dia2[rowIndex - columnIndex] = bool
}
// 嘗試在一個n皇后問題中 擺放第index行內的皇后位置
let putQueen = (rowIndex, prev) => {
if (rowIndex === n) {
res.push(generateBoard(prev))
return
}
// 嘗試擺第index行的皇后 嘗試[0, n-1]列
for (let columnIndex = 0; columnIndex < n; columnIndex++) {
// 在列上不衝突
let columnNotConflict = !columns[columnIndex]
// 在對角線1上不衝突
let dia1NotConflict = !dia1[rowIndex + columnIndex]
// 在對角線2上不衝突
let dia2NotConflict = !dia2[rowIndex - columnIndex]
if (columnNotConflict && dia1NotConflict && dia2NotConflict) {
// 都不衝突的話,先記錄當前已選位置,進入下一輪遞歸
record(rowIndex, columnIndex, true)
putQueen(rowIndex + 1, prev.concat(columnIndex))
// 遞歸出棧後,在狀態中清除這個位置的記錄,下一輪循環應該是一個全新的開始。
record(rowIndex, columnIndex, false)
}
}
}
putQueen(0, [])
return res
}
// 生成二維數組的輔助函數
function generateBoard(row) {
let n = row.length
let res = []
for (let y = 0; y < n; y++) {
let cur = ""
for (let x = 0; x < n; x++) {
if (x === row[y]) {
cur += "Q"
} else {
cur += "."
}
}
res.push(cur)
}
return res
}
複製代碼
對遞歸回溯的類似 LeetCode 題型感興趣的同窗,能夠去我維護的 力扣題解-遞歸與回溯 這個 Github 倉庫分類下查看其它的經典類似題目,先嚐試本身用個人兩篇遞歸回溯文章中的思路求解,若是仍是答不出來的話,就去看題解總結概括,直到你能真正的本身作出相似的題型爲止。
至此爲止,年輕前端的第一道 hard 題就解出來了,是否是有種任督二脈打通的感受呢?
遞歸回溯的問題本質上就是,遞歸進入下一層後,若是發現不知足條件,就經過 return 等方式回溯到上一層遞歸,繼續尋求合適的解。
掌握了這個思路之後,相信你在現實編碼中遇到的不少遞歸難題均可以輕鬆的降維打擊,迎刃而解了。
也祝正在籌備換工做的小夥伴們順利經過面試筆試的廝殺,拿到理想的 offer,你們加油。
1.若是本文對你有幫助,就點個贊支持下吧,你的「贊」是我創做的動力。
2.關注公衆號「前端從進階到入院」便可加我好友,我拉你進「前端進階交流羣」,你們一塊兒共同交流和進步。