394,經典的八皇后問題和N皇后問題

2020-09-15 數據結構和算法

八皇后的來源

八皇后問題是一個以西洋棋為背景的問題:如何能夠在8×8的西洋棋棋盤上放置八個皇后,使得任何一個皇后都無法直接吃掉其他的皇后?為了達到此目的,任兩個皇后都不能處於同一條橫行、縱行或斜線上。八皇后問題可以推廣為更一般的n皇后擺放問題:這時棋盤的大小變為n×n,而皇后個數也變成n。若且唯若n = 1或n ≥ 4時問題有解。

八皇后問題最早是由西洋棋棋手馬克斯·貝瑟爾(Max Bezzel)於1848年提出。第一個解在1850年由弗朗茲·諾克(Franz Nauck)給出。並且將其推廣為更一般的n皇后擺放問題。諾克也是首先將問題推廣到更一般的n皇后擺放問題的人之一。

問題分析

我們先不看8皇后,我們先來看一下4皇后,其實原理都是一樣的,比如在下面的4*4的格子裡,如果我們在其中一個格子裡輸入了皇后,那麼在這一行這一列和這左右兩邊的對角線上都不能有皇后。

所以有一種方式就是我們一個個去試

第一行

比如我們在第一行第一列輸入了一個皇后

第二行

第二行我們就不能在第一列和第二列輸入皇后了,因為有衝突了。但我們可以在第3列輸入皇后

第三行

第三行我們發現在任何位置輸入都會有衝突。這說明我們之前選擇的是錯誤的,再回到上一步,我們發現第二步不光能選擇第3列,而且還能選擇第4列,既然選擇第3列不合適,那我們就選擇第4列吧


第二行(重新選擇)

第二行我們選擇第4列

第三行(重新選擇)

第3行我們只有選擇第2列不會有衝突

第四行

我們發現第4行又沒有可選擇的了。第一次重試失敗


第二次重試

到這裡我們只有重新回到第一步了,這說明我們之前第一行選擇第一列是無解的,所以我們第一行不應該選擇第一列,我們再來選擇第二列來試試

第一行

這一行我們選擇第2列

第二行

第二行我們前3個都不能選,只能選第4列

第三行

第三行我們只能選第1列

第四行

第四行我們只能選第3列

最後我們終於找到了一組解。除了這組解還有沒有其他解呢,肯定還是有的,因為4皇后是有兩組解的,這裡我們就不在一個個試了。


我們來看一下他查找的過程,就是先從第1行的第1列開始往下找,然後再從第1行的第2列……,一直到第1行的第n列。代碼該怎麼寫呢,看到這裡估計大家都已經想到了,這不就是一棵N叉樹的前序遍歷嗎,我們來看下,是不是下面這樣的。

我們先來看一下二叉樹的前序遍歷怎麼寫,不明白的可以參考下

public static void preOrder(TreeNode tree) { if (tree == null) return; System.out.printf(tree.val + &34;); preOrder(tree.left); preOrder(tree.right);}

二叉樹是有兩個子節點,那麼N叉樹當然是有N個子節點了,所以N叉樹的前序遍歷是這樣的

public static void preOrder(TreeNode tree) { if (tree == null) return; System.out.printf(tree.val + &34;); preOrder(&34;); preOrder(&34;); …… preOrder(&34;);}

如果N是一個很大的值,這樣寫要寫死人了,我們一般使用循環的方式

public static void preOrder(TreeNode tree) { if (tree == null) return; System.out.printf(tree.val + &34;); for (int i = 0; i <n ; i++) { preOrder(&34;); }}

搞懂了上面的分析過程,那麼這題的代碼輪廓就呼之欲出了

private void solve(char[][] chess, int row) {    &34;    return;    for (int col = 0; col < chess.length; col++) {        //判斷這個位置是否可以放皇后        if (valid(chess, row, col)) {            //如果可以放,我們就把原來的數組chess複製一份,            char[][] temp = copy(chess);            //然後在這個位置放上皇后            temp[row][col] = &39;;            //下一行            solve(temp, row + 1);        }    }}

我們來分析下上面的代碼,因為是遞歸所以必須要有終止條件,那麼這題的終止條件就是我們最後一行已經走完了,也就是

if (row == chess.length) { return;}

第7行就是判斷在這個位置我們能不能放皇后,如果不能放我們就判斷這一行的下一列能不能放……,如果能放我們就把原來數組chess複製一份,然後把皇后放到這個位置,然後再判斷下一行,這和我們上面畫圖的過程非常類似。注意這裡的第9行為什麼要複製一份,因為數組是引用傳遞,這涉及到遞歸的時候分支汙染問題(後面有時間我會專門寫一篇關於遞歸的時候分支汙染問題)。當然不複製一份也是可以的,我們下面再講。當我們把上面的問題都搞懂的時候,代碼也就很容易寫出來了,我們來看下N皇后的最終代碼

public void solveNQueens(int n) {    char[][] chess = new char[n][n];    for (int i = 0; i < n; i++)        for (int j = 0; j < n; j++)            chess[i][j] = &39;;    solve(chess, 0);}private void solve(char[][] chess, int row) {    if (row == chess.length) {        //自己寫的一個公共方法,列印二維數組的,        // 主要用於測試數據用的        Util.printTwoCharArrays(chess);        System.out.println();        return;    }    for (int col = 0; col < chess.length; col++) {        if (valid(chess, row, col)) {            char[][] temp = copy(chess);            temp[row][col] = &39;;            solve(temp, row + 1);        }    }}//把二維數組chess中的數據測下copy一份private char[][] copy(char[][] chess) {    char[][] temp = new char[chess.length][chess[0].length];    for (int i = 0; i < chess.length; i++) {        for (int j = 0; j < chess[0].length; j++) {            temp[i][j] = chess[i][j];        }    }    return temp;}//row表示第幾行,col表示第幾列private boolean valid(char[][] chess, int row, int col) {    //判斷當前列有沒有皇后,因為他是一行一行往下走的,    //我們只需要檢查走過的行數即可,通俗一點就是判斷當前    //坐標位置的上面有沒有皇后    for (int i = 0; i < row; i++) {        if (chess[i][col] == &39;) {            return false;        }    }    //判斷當前坐標的右上角有沒有皇后    for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {        if (chess[i][j] == &39;) {            return false;        }    }    //判斷當前坐標的左上角有沒有皇后    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {        if (chess[i][j] == &39;) {            return false;        }    }    return true;}

代碼看起來比較多,我們主要看下solve方法即可,其他的方法不看也可以,知道有這個功能就行。solve代碼中其核心代碼是在17-23行,上面是終止條件的判斷,我們就用4皇后來測試一下

solveNQueens(4);

看一下列印的結果

我們看到4皇后的時候有兩組解,其中第一組和我們上面圖中分析的完全一樣。

4皇后解決了,那麼8皇后也一樣,我們只要在函數調用的時候傳入8就可以了。同理,要想計算10皇后,20皇后,100皇后……也都是可以的。

回溯解決

上面代碼中每次遇到能放皇后的時候,我們都會把原數組複製一份,這樣對新數據的修改就不會影響到原來的,也就是不會造成分支汙染。但這樣每次嘗試的時候都都把原數組複製一份,影響效率,有沒有其他的方法不複製呢,是有的。就是每次我們選擇把這個位置放置皇后的時候,如果最終不能成功,那麼返回的時候我們就還要把這個位置還原。這就是回溯算法,也是試探算法。我們來看下代碼

private void solve(char[][] chess, int row) {    if (row == chess.length) {        //自己寫的一個公共方法,列印二維數組的,        // 主要用於測試數據用的        Util.printTwoCharArrays(chess);        System.out.println();        return;    }    for (int col = 0; col < chess.length; col++) {        if (valid(chess, row, col)) {            chess[row][col] = &39;;            solve(chess, row + 1);            chess[row][col] = &39;;        }    }}

主要來看下11-13行,其他的都沒變,還和上面的一樣。這和我們之前講的391,回溯算法求組合問題很類似。他是先假設[row][col]這個位置可以放皇后,然後往下找,無論找到找不到最後都會回到這個地方,因為這裡是遞歸調用,回到這個地方的時候我們再把它復原,然後走下一個分支。最後我們再來看下使用回溯算法解N皇后的完整代碼

public void solveNQueens(int n) {    char[][] chess = new char[n][n];    for (int i = 0; i < n; i++)        for (int j = 0; j < n; j++)            chess[i][j] = &39;;    solve(chess, 0);}private void solve(char[][] chess, int row) {    if (row == chess.length) {        //自己寫的一個公共方法,列印二維數組的,        // 主要用於測試數據用的        Util.printTwoCharArrays(chess);        System.out.println();        return;    }    for (int col = 0; col < chess.length; col++) {        if (valid(chess, row, col)) {            chess[row][col] = &39;;            solve(chess, row + 1);            chess[row][col] = &39;;        }    }}//row表示第幾行,col表示第幾列private boolean valid(char[][] chess, int row, int col) {    //判斷當前列有沒有皇后,因為他是一行一行往下走的,    //我們只需要檢查走過的行數即可,通俗一點就是判斷當前    //坐標位置的上面有沒有皇后    for (int i = 0; i < row; i++) {        if (chess[i][col] == &39;) {            return false;        }    }    //判斷當前坐標的右上角有沒有皇后    for (int i = row - 1, j = col + 1; i >= 0 && j < chess.length; i--, j++) {        if (chess[i][j] == &39;) {            return false;        }    }    //判斷當前坐標的左上角有沒有皇后    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {        if (chess[i][j] == &39;) {            return false;        }    }    return true;}

總結

8皇后問題其實是一道很經典的回溯算法題,我們這裡並沒有專門針對8皇后來講,我們這裡講的是N皇后,如果這道題搞懂了,那麼8皇后自然也就懂了,因為這裡的N可以是任何正整數。

遞歸一般可能會有多個分支,我們要保證每個分支的修改不會汙染其他分支,也就是不要對其他分支造成影響,這一點很重要。由於一些語言中除了基本類型以外,其他的大部分都是引用傳遞,所以我們在一個分支修改之後可能就會對其他分支產生一些我們不想要的垃圾數據,這個時候我們就有兩中解決方式,一種就是我們上面講到的第一種,複製一份新的數據,這樣每個分支都會產生一些新的數據,所以即使修改了也不會對其他分支有影響。還一種方式就是我們每次使用完之後再把它復原,一般的情況下我們都會選擇第二種,因為這種代碼更簡潔一些,也不會重複的複製數據,造成大量的垃圾數據。

如果喜歡這篇文章還可以關注微信公眾號「數據結構和算法」,查看更多的算法題

相關焦點

  • 八皇后問題
  • 破解"八皇后問題"
    "八皇后問題",是一個古老而著名的問題,裡面的"皇后"指的是西洋棋裡面的一種棋子,可以橫著豎著斜著走(所以比中國象棋的車威力更大
  • 漫畫:什麼是八皇后問題?
    什麼是八皇后問題?八皇后問題是一個古老的問題,於1848年由一位西洋棋棋手提出:在8×8格的西洋棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,如何求解?以高斯為代表的許多數學家先後研究過這個問題。後來,當計算機問世,通過電腦程式的運算可以輕鬆解出這個問題。
  • 量子計算機,只需要幾個量子,就足以解決西洋棋中n皇后問題!
    因斯布魯克大學物理學家們提出了一個新模型,該模型可以證明量子計算機在解決優化問題方面優於經典超級計算機。在新一篇研究論文中證明,即使對於大型棋盤,只要幾個量子粒子就足以解決西洋棋中的n皇后問題。n皇后問題是一項數學任務,偉大的數學家卡爾·弗裡德裡希·高斯(Carl Friedrich Gauss)著手解決這個問題,但令人驚訝的是,他沒有找到正確的答案。這裡的挑戰是如何在一個8乘8方的古典棋盤上排列8個皇后,這樣就不會有兩個皇后互相威脅。從數學上講,比較容易確定有92種不同的排列方式。
  • 回溯算法解決八皇后問題(包含C語言實現代碼)
    八皇后問題是以西洋棋為背景的問題:有八個皇后(可以當成八個棋子),如何在 8*8 的棋盤中放置八個皇后,使得任意兩個皇后都不在同一條橫線、縱線或者斜線上
  • 《波西米亞狂想曲》:永不磨滅的音樂史,再現皇后樂隊永恆的經典
    然後也是非常喜歡的一首勁爆的搖滾歌曲,皇后樂隊的《we will rock you》(《我們將震撼你》)。這首歌是流傳度非常廣的一首經典搖滾音樂,自此,我繼認識老鷹樂隊之後認識第二支搖滾樂隊——皇后樂隊。而皇后樂隊最經典評價度更好的歌曲是《波西米亞狂想曲》,它被譽為「搖滾歌劇」。從歌曲到電影:《波西米亞狂想曲》。
  • 本-威士肖接人物傳記片 演繹「皇后樂隊」主唱
    弗雷迪1946年9月5日出生於贊茲巴爾島的一個印度波斯族家庭,他在1970年與布萊恩-梅和羅傑-泰勒相遇,成立了「皇后」樂隊。該樂隊成員包括主弗雷迪、吉他手布萊恩-梅、鼓手羅傑-泰勒、貝斯手約翰-德克恩,曾對世界樂壇留下了非常深遠的影響。比較戲劇化的是,弗雷迪是一個同性戀者,1991年因愛滋病去世。
  • 皇后的日常生活:從《清平樂》的曹皇后,看古代皇后在宮中的工作
    這個需要從皇后的這個職位稱呼來說起。皇后的「後」,並非「後」的簡體字,在古漢字中,「後」是「後」,「後」是「後」,兩不相干。「後」是一個專用於頭銜的字,其實我們都很熟悉,比如「夏後啟」,再比如「后羿射日」。後啟,也就是叫啟的首領或君主。夏後啟,就是夏的部落或部落聯盟的首領。后羿,名叫「羿」,「後」是頭銜和稱呼,也是部落首領。
  • 康熙皇帝第三任皇后——孝懿仁皇后佟佳氏
    康熙是皇帝,他自然是有皇后的,康熙的嫡後髮妻是孝誠仁皇后赫舍裡氏(索尼的孫女)、孝昭仁皇后鈕鈷祿氏(遏必隆之女)、孝懿仁皇后佟佳氏(佟國維之女)和孝恭仁皇后烏雅氏(雍正生母)。這裡要說的就是孝懿仁皇后佟佳氏。孝懿仁皇后是康熙皇帝的第三任皇后,她的父親是一等公佟國維。
  • 獨孤皇后:史上第一位堅持一夫一妻的皇后.賢明皇后還是禍國紅顏?
    平心而論,獨孤皇后是個傑出的政治家。與隋文帝一起開創開皇之治,恢復漢制;北破突厥、南平陳朝,統一了分裂將近四百年的中華大地;改革官制;開創科舉;廢除大量酷刑;減輕農民負擔……文帝時期朝野豐足,隋朝國富程度歷代矚目。說獨孤伽羅是一代賢后並不為過。但獨孤皇后的性格缺陷也是十分矚目的。首先,就是獨孤皇后對愛情是挺有潔癖的。
  • 【歷史學堂】明代宮廷服飾系列(八)皇后常服
    如皇后冊立之後,具禮服行謝恩禮畢,回宮更換燕居冠服,接受在內親屬和六尚女官、各監局內使的慶賀禮。皇后常服制度經過了多次修訂,洪武元年,定皇后燕居服雙鳳翊龍冠、諸色團衫、金玉帶等,洪武四年改為龍鳳珠翠冠、真紅大袖衣、霞帔等。《明會典》永樂三年的制度中,皇后常服定為雙鳳翊龍冠、大衫、霞帔、鞠衣等。
  • 《清平樂》宋仁宗太渣了,自己初戀八回了,和曹皇后五年都沒圓房
    《清平樂》宋仁宗太渣了,自己初戀八回了,和曹皇后五年都沒圓房。官家真的太多情了,後宮佳麗三千,真的是見一個愛一個啊,但是,偏偏為什麼不能愛皇后呢?他為了子嗣,可以十天宣一次後宮的娘子,卻就是不和皇后圓房,官家和曹皇后成親五年了,兩個人竟然還沒有同房,官家是讓皇后活守寡一輩子嗎?
  • 經典再現!NG騎士檸檬汽水&40 金戰士&皇后獅狼評測 - 評測...
    經典再現!TV故事分上下兩大篇,上篇講述主角搜尋八位守護騎士阻止「妖神頌五力」復活;下篇蘋果酒化敵為友,一起尋找八塊石板來擊敗「妖神頌五力」       《NG騎士檸檬汽水&40》是大陸和臺灣的翻譯,基本上很多人名都涉及飲料的名字,很不習慣。而標題的意義,我當初還不知道,後來惡補後才知道。原來是指檸檬汽水(主角)和40臺故事裡面登場的機器人。
  • 清朝一皇后在位僅八小時,只因皇帝太愛她
    康熙在位時雖然有3位皇后,但是史料明她與孝懿仁皇后感情最好,最為親近。史書記載康熙二十八年(公元1689年)七月初八日,皇貴妃病重。初九日,立為皇后,頒詔天下。初十日,申刻(下午三點至五點),皇后於承乾宮逝世。因此佟佳氏共在後位八個小時,成為清朝在位最短的皇后。
  • 「皇后樂隊」主唱,和他的傳奇人生
    然而他們經典的歌曲遠不止這些,片名《波西米亞狂想曲》(Bohemian Rhapsody),就是皇后樂隊最經典的歌曲之一,也是英國史上目前銷售量最高的單曲(沒有之一)。 佛萊迪是一個有野心的人,他不滿足於小打小鬧,想盡辦法去做他們的第一張專輯,甚至將四個男性的樂隊組合名字從溫和的「smile」改成了霸氣的「Queen」,他說:這源於至高無上的皇后,代表著肆無忌憚。
  • 這十首皇后樂隊經典歌曲 絕對顛覆大家對他們以往的印象!
    一提起搖滾樂隊——Queen(皇后樂隊),絕大多數人首先想到的大概就是他們的那首經典的熱血搖滾曲目《We Will Rock You》,這首歌曲甚至一度成為「搖滾」這一綜合體音樂風格的代名詞。而來自皇后樂隊另外兩首被絕大多數人所熟知的經典曲目,則是和《We Will Rock You》創作於同一時期的《We Are the Champions》,以及另外一首歌劇搖滾風格的《Bohemian Rhapsody》。前者成為各種運動賽場的常備曲目,而後者則被大眾譽為皇后樂隊的代表作。
  • 被廢了五次的皇后,第六次成為了別人的皇后
    羊獻容是泰山郡南城縣人,是當時尚書右僕射羊玄之的女兒,後被立為晉惠帝司馬衷的皇后。 永康元年的時候,司馬衷的第一任皇后賈南風被趙王司馬倫殺害,當時一個臣子孫秀便上諫說要在新立一位皇后,而羊獻容的外祖父孫旗和孫秀是同族,孫旗的幾個兒子也都和孫秀結交,當時的孫秀是趙王司馬倫的寵臣,於是便直接立了羊獻容為皇后,立為皇后之後羊獻容為晉惠帝生下了一個女兒,起名清河公主。
  • 《知否》原著:從八王妃,到太子妃,到沈皇后,果然是糟糠不下堂
    八王爺的生母李淑儀,本是浣衣局宮女,一夕得幸生下一子,才得封淑儀,但仁宗皇帝從未寵過她,封了淑儀後差不多就徘徊在冷宮邊緣養老了。母賤子不貴,李淑儀生的八皇子就藩蜀邊,原是打算一輩子當個冷落王爺,因此娶的沈家女,家世很一般。
  • 成吉思汗公主皇后雜考
    一 公主皇后為金帝衛紹王幼女,宮中稱為小姐姐,這是諸家補傳都已提到的,可公主皇后在衛紹王諸女中到底排行第幾,諸家補傳卻沒有交代清楚。其實,這一問題是可以得到解決的。據《建炎以來朝野雜記》乙集卷一九《女真南徙》:「金主珣厚賂忒沒貞(即鐵木真~引者注),以允濟第四女小姐姐者妻之。」
  • 皇后樂隊,憑什麼這麼牛?
    ——弗雷迪·墨丘利 (皇后樂隊主唱)1985年,Live Aid大型演唱會在倫敦和費城同時舉行,這場演唱會啟發於為衣索比亞饑荒籌集善款。演出持續超過16個小時,現場觀眾超過15萬人,全球150多個國家的觀眾通過電視觀看,13個衛星用來直播,彼時的奧運會也只使用了三個。