經典面試題目「回溯算法」N皇后問題

2020-11-17 代碼隨想錄

通知:我將公眾號文章和學習相關的資料整理到了Github :https://github.com/youngyangyang04/leetcode-master,方便大家在電腦上學習,可以fork到自己倉庫,順便也給個star支持一波吧!

第51題. N皇后

題目連結:https://leetcode-cn.com/problems/n-queens/

n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。

上圖為 8 皇后問題的一種解法。

給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。

每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 'Q' 和 '.' 分別代表了皇后和空位。

示例: 輸入: 4
輸出: [
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],

["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解釋: 4 皇后問題存在兩個不同的解法。

提示:
皇后,是西洋棋中的棋子,意味著國王的妻子。皇后只做一件事,那就是「吃子」。當她遇見可以吃的棋子時,就迅速衝上去吃掉棋子。當然,她橫、豎、斜都可走一到七步,可進可退。(引用自 百度百科 - 皇后 )

思路

都知道n皇后問題是回溯算法解決的經典問題,但是用回溯解決多了組合、切割、子集、排列問題之後,遇到這種二位矩陣還會有點不知所措。

首先來看一下皇后們的約束條件:

  1. 不能同行
  2. 不能同列
  3. 不能同斜線

確定完約束條件,來看看究竟要怎麼去搜索皇后們的位置,其實搜索皇后的位置,可以抽象為一棵樹。

下面我用一個3 * 3 的棋牌,將搜索過程抽象為一顆樹,如圖:

51.N皇后

從圖中,可以看出,二維矩陣中矩陣的高就是這顆樹的高度,矩陣的寬就是樹型結構中每一個節點的寬度。

那麼我們用皇后們的約束條件,來回溯搜索這顆樹,「只要搜索到了樹的葉子節點,說明就找到了皇后們的合理位置了」

回溯三部曲

按照我總結的如下回溯模板,我們來依次分析:

void backtracking(參數) {    if (終止條件) {        存放結果;        return;    }    for (選擇:本層集合中元素(樹中節點孩子的數量就是集合的大小)) {        處理節點;        backtracking(路徑,選擇列表); // 遞歸        回溯,撤銷處理結果    }}

  • 遞歸函數參數

我依然是定義全局變量二維數組result來記錄最終結果。

參數n是棋牌的大小,然後用row來記錄當前遍歷到棋盤的第幾層了。

代碼如下:

vector<vector<string>> result;void backtracking(int n, int row, vector<string>& chessboard) {

  • 遞歸終止條件

在如下樹形結構中:

可以看出,當遞歸到棋盤最底層(也就是葉子節點)的時候,就可以收集結果並返回了。

代碼如下:

if (row == n) {    result.push_back(chessboard);    return;}

  • 單層搜索的邏輯

遞歸深度就是row控制棋盤的行,每一層裡for循環的col控制棋盤的列,一行一列,確定了放置皇后的位置。

每次都是要從新的一行的起始位置開始搜,所以都是從0開始。

代碼如下:

for (int col = 0; col < n; col++) {    if (isValid(row, col, chessboard, n)) { // 驗證合法就可以放        chessboard[row][col] = 'Q'; // 放置皇后        backtracking(n, row + 1, chessboard);        chessboard[row][col] = '.'; // 回溯,撤銷皇后    }}

  • 驗證棋牌是否合法

按照如下標準去重:

  1. 不能同行
  2. 不能同列
  3. 不能同斜線 (45度和135度角)

代碼如下:

bool isValid(int row, int col, vector<string>& chessboard, int n) {    int count = 0;    // 檢查列    for (int i = 0; i < row; i++) { // 這是一個剪枝        if (chessboard[i][col] == 'Q') {            return false;        }    }    // 檢查 45度角是否有皇后    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {        if (chessboard[i][j] == 'Q') {            return false;        }    }    // 檢查 135度角是否有皇后    for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {        if (chessboard[i][j] == 'Q') {            return false;        }    }    return true;}

在這份代碼中,細心的同學可以發現為什麼沒有在同行進行檢查呢?

因為在單層搜索的過程中,每一層遞歸,只會選for循環(也就是同一行)裡的一個元素,所以不用去重了。

那麼按照這個模板不難寫出如下代碼:

C++代碼

class Solution {private:vector<vector<string>> result;// n 為輸入的棋盤大小// row 是當前遞歸到棋牌的第幾行了void backtracking(int n, int row, vector<string>& chessboard) {    if (row == n) {        result.push_back(chessboard);        return;    }    for (int col = 0; col < n; col++) {        if (isValid(row, col, chessboard, n)) { // 驗證合法就可以放            chessboard[row][col] = 'Q'; // 放置皇后            backtracking(n, row + 1, chessboard);            chessboard[row][col] = '.'; // 回溯,撤銷皇后        }    }}bool isValid(int row, int col, vector<string>& chessboard, int n) {    int count = 0;    // 檢查列    for (int i = 0; i < row; i++) { // 這是一個剪枝        if (chessboard[i][col] == 'Q') {            return false;        }    }    // 檢查 45度角是否有皇后    for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) {        if (chessboard[i][j] == 'Q') {            return false;        }    }    // 檢查 135度角是否有皇后    for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {        if (chessboard[i][j] == 'Q') {            return false;        }    }    return true;}public:    vector<vector<string>> solveNQueens(int n) {        result.clear();        std::vector<std::string> chessboard(n, std::string(n, '.'));        backtracking(n, 0, chessboard);        return result;    }};

可以看出,除了驗證棋盤合法性的代碼,省下來部分就是按照回溯法模板來的。

總結

本題是我們解決棋盤問題的第一道題目。

如果從來沒有接觸過N皇后問題的同學看著這樣的題會感覺無從下手,可能知道要用回溯法,但也不知道該怎麼去搜。

「這裡我明確給出了棋盤的寬度就是for循環的長度,遞歸的深度就是棋盤的高度,這樣就可以套進回溯法的模板裡了」

大家可以在仔細體會體會!

就醬,如果感覺「代碼隨想錄」乾貨滿滿,就分享給身邊的朋友同學吧,他們可能也需要!

打算從頭開始打卡的錄友,可以在公眾號「算法匯總」這裡找到歷史文章,很多錄友都在從頭打卡,你並不孤單!

就醬,一直都是乾貨滿滿,公眾號裡的一抹清流,值得推薦給身邊的每一位同學朋友!

相關焦點

  • 網際網路公司經典面試題目:「回溯算法」求組合問題
    思路本題這是回溯法的經典題目。那麼回溯法怎麼暴力搜呢?上面我們說了「要解決 n為100,k為50的情況,暴力寫法需要嵌套50層for循環,那麼回溯法就用遞歸來解決嵌套層數的問題」。總結組合問題是回溯法解決的經典問題,我們開始的時候給大家列舉一個很形象的例子,就是n為100,k為50的話,直接想法就需要50層for循環。
  • 經典面試題目本周小結!(回溯算法系列一)
    「回溯是遞歸的副產品,只要有遞歸就會有回溯」。回溯法就是暴力搜索,並不是什麼高效的算法,最多在剪枝一下。棋盤問題:N皇后,解數獨等等是不是感覺回溯算法有點厲害了。「因為本題每一個數字代表的是不同集合,也就是求不同集合之間的組合,而回溯算法:求組合問題!和回溯算法:求組合總和!都是是求同一個集合中的組合!」
  • 經典面試題目「回溯算法」解數獨
    大家已經跟著「代碼隨想錄」刷過了如下回溯法題目,例如:77.組合(組合問題),131.分割回文串(分割問題),78.子集(子集問題),46.全排列(排列問題),以及51.N皇后(N皇后問題),其實這些題目都是一維遞歸。
  • 網際網路高頻面試題目:「回溯算法」求組合總和
    「確定遞歸函數參數」和一樣,依然需要一維數組path來存放符合條件的結果,二維數組result來存放結果集。= targetSum 直接返回}「單層搜索過程」本題和回溯算法:求組合問題!     path.pop_back(); // 回溯 }「別忘了處理過程 和 回溯過程是一一對應的,處理有加,回溯就要有減!」
  • 騰訊面試題目「回溯算法」排列問題
    相信這個排列問題就算是讓你用for循環暴力把結果搜索出來,這個暴力也不是很好寫。所以正如我們在關於 所講的為什麼回溯法是暴力搜索,效率這麼低,還要用它?「因為一些問題能暴力搜出來就已經很不錯了!」「而used數組,其實就是記錄此時path裡都有哪些元素使用了,一個排列裡一個元素只能使用一次」。
  • 每天一道前端算法題--回溯算法--N皇后問題
    什麼是回溯算法先來了解一下什麼時回溯算法。回溯的處理思想,有點類似枚舉搜索。我們枚舉所有的解,找到滿足期望的解。為了有規律地枚舉所有可能的解,避免遺漏和重複,我們把問題求解的過程分為多個階段。每個階段,我們都會面對一個岔路口,我們先隨意選一條路走,當發現這條路走不通的時候(不符合期望的解),就回退到上一個岔路口,另選一種走法繼續走。
  • 網際網路公司高頻面試題目:「回溯算法」電話號碼的字母組合
    大家應該感覺出和回溯算法:求組合問題!遇到的一樣的問題,就是這for循環的層數如何寫出來,此時又是回溯法登場的時候了。n個for循環的問題對於回溯法還不了解的同學看這篇:關於回溯算法,你該了解這些!}「注意這裡for循環,可不像是在回溯算法:和
  • 經典面試題目「回溯算法」求組合總和(二)
    「本題還需要startIndex來控制for循環的起始位置,對於組合問題,什麼時候需要startIndex呢?」我舉過例子,如果是一個集合來求組合的話,就需要startIndex,例如:,。並且給出了對於組合問題,什麼時候用startIndex,什麼時候不用,並用回溯算法:電話號碼的字母組合做了對比。最後還給出了本題的剪枝優化,這個優化如果是初學者的話並不容易想到。
  • 網際網路公司高頻面試題目「回溯算法」:分割回文串
    這種題目,想用for循環暴力解法,可能都不那麼容易寫出來,所以要換一種暴力的方式,就是回溯。一些同學可能想不清楚 回溯究竟是如果切割字符串呢?我們來分析一下切割,「其實切割問題類似組合問題」。此時可以發現,切割問題的回溯搜索的過程和組合問題的回溯搜索的過程是差不多的。
  • 網際網路公司高頻面試題目「回溯算法」求子集問題
    如果把 子集問題、組合問題、分割問題都抽象為一棵樹的話,「那麼組合問題和分割問題都是收集樹的葉子節點,而子集問題是找樹的所有節點!」「那麼既然是無序,取過的元素不會重複取,寫回溯算法的時候,for就要從startIndex開始,而不是從0開始!」有同學問了,什麼時候for可以從0開始呢?
  • 騰訊面試題目「回溯算法」排列問題(二)
    「給定一個可包含重複數字的序列」,要返回「所有不重複的全排列」。在 、我們分別詳細講解了組合問題和子集問題如何去重。那麼排列問題其實也是一樣的套路。「還要強調的是去重一定要對元素進行排序,這樣我們才方便通過相鄰的節點來判斷是否重複使用了」。
  • 回溯算法:組合問題如何剪枝?
    「那麼重點來了,來都來了,順便給一個star吧,哈哈」在中,我們通過回溯搜索法,解決了n個數中求k個數的組合問題。文中的回溯法是可以剪枝優化的,本篇我們繼續來看一下題目。連結:https://leetcode-cn.com/problems/combinations/「看本篇之前,需要先看回溯算法:求組合問題!」。大家先回憶一下[77.
  • 網際網路公司高頻面試題目「回溯算法」求子集問題(二)
    那麼關於回溯算法中的去重問題,「在中已經詳細講解過了,和本題是一個套路」「劇透一下,後期要講解的排列問題裡去重也是這個套路,所以理解「樹層去重」和「樹枝去重」非常重要」。本題就是其實就是的基礎上加上了去重,去重我們在回溯算法:求組合總和(三)也講過了,所以我就直接給出代碼了:C++代碼class
  • 網際網路公司高頻面試題目「回溯算法」遞增子序列
    「其實用數組來做哈希,效率就高了很多」。注意題目中說了,數值範圍[-100,100],所以完全可以用數組來做哈希。「對於養成思維定式或者套模板套嗨了的同學,這道題起到了很好的警醒作用。更重要的是拓展了大家的思路!」
  • 網際網路公司高頻面試題目「回溯算法」:求組合總和II
    很多同學在去重的問題上想不明白,其實很多題解也沒有講清楚,反正代碼是能過的,感覺是那麼回事,稀裡糊塗的先把題目過了。這個去重為什麼很難理解呢,「所謂去重,其實就是使用過的元素不能重複選取。」那麼問題來了,我們是要同一樹層上使用過,還是統一樹枝上使用過呢?回看一下題目,元素在同一個組合內是可以重複的,怎麼重複都沒事,但兩個組合不能相同。
  • (回溯算法系列三)
    這塊網上的資料魚龍混雜,一些所謂的經典面試書籍根本不講回溯算法,算法書籍對這塊也避而不談,感覺就像是算法裡模糊的邊界。「所以這塊就說一說我個人理解,對內容持開放態度,集思廣益,歡迎大家來討論!」「一般說到回溯算法的複雜度,都說是指數級別的時間複雜度,這也算是一個概括吧!」
  • 「力扣」鍊表經典面試題目大總結!(每逢總結必經典)
    鍊表的一大問題就是操作當前節點必須要找前一個節點才能操作。這就造成了,頭結點的尷尬,因為頭結點沒有前一個節點了。「每次對應頭結點的情況都要單獨處理,所以使用虛擬頭結點的技巧,就可以解決這個問題」。因為反轉鍊表的代碼相對簡單,有的同學可能直接背下來了,但一寫還是容易出問題。反轉鍊表是面試中高頻題目,很考察面試者對鍊表操作的熟練程度。我在文章中,給出了兩種反轉的方式,迭代法和遞歸法。建議大家先學透迭代法,然後再看遞歸法,因為遞歸法比較繞,如果迭代還寫不明白,遞歸基本也寫不明白了。
  • 關於回溯算法,你該了解這些
    「雖然回溯法很難,很不好理解,但是回溯法並不是什麼高效的算法」。>棋盤問題:N皇后,解數獨等等「相信大家看著這些之後會發現,每個問題,都不簡單!」「回溯法解決的問題都可以抽象為樹形結構」,是的,我指的是所有回溯法的問題都可以抽象為樹形結構!
  • 面試刷題:經典算法思想之回溯法 | 第88期
    我們所遇到的算法問題五花八門,形態各異,但是從已有的那些解決方案中歸納出的經典算法思想卻是屈指可數,而這些經典的前人經驗基本涵蓋了絕大多數計算機算法問題的解法。掌握這些經典的算法思想,對於解決程序設計問題而言,具有提綱挈領、綱舉目張的效果。事實上,不僅僅對於算法問題,這些思想很大程度上也為解決現實世界的一些問題提供了思路。
  • LeetCode筆記51:N 皇后
    ┐題目描述n 皇后問題研究的是如何將 n 個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。給定一個整數 n,返回所有不同的 n 皇后問題的解決方案。每一種解法包含一個明確的 n 皇后問題的棋子放置方案,該方案中 'Q' 和 '.' 分別代表了皇后和空位。