還記得八皇后的解法嗎

 

 

「還記得八皇后的解法嗎?」數組

「上個世紀的事情,不記得了。」app

「…… 如今回憶一下?」函數

「開會,回頭說。」性能

「 fuck u 」spa

「 u shit 」3d

 

  我有一個C++基友,這麼稱呼是由於他入行時用的是C++。雖然在遊走於騰訊、金山以後,現在已經十八般武藝略懂了,但提及來仍是C++的標籤最刺眼。code

  當你有一個C++基友時,QQ裏的平常,不免就會碰到上面那種聊天記錄了。orm

 

  八皇后是一個古老的經典問題:如何在一張國際象棋的棋盤上,擺放8個皇后,使其任意兩個皇后互相不受攻擊。blog

  該問題由一位德國國際象棋排局家 Max Bezzel 於 1848年提出。嚴格來講,那個年代,尚未「德國」這個國家,彼時稱做「普魯士」。遞歸

 Max Bezzel

  1850年,Franz Nauck 給出了第一個解,並將其擴展成了「 n皇后 」問題,即在一張 n x n 的棋盤上,如何擺放 n 個皇后,使其兩兩互不攻擊

  歷史上,八皇后問題曾驚動過「數學王子」高斯(Gauss),並且正是上面這個 Franz Nauck 寫信找高斯請教的。

 

高斯和八皇后問題

 

  在那天被基友問到時,我並不是真的不記得了,而是我壓根就沒有作過。但我第一次碰見這個問題時,確實是在上個世紀,那是在小學微機教室裏,參加市級計算機奧林匹克小學組競賽的培訓課上。

  我還記得初次看到這個問題的第一反應——怎麼可能擺8個!要知道我初學國際象棋時,常常爲了簡化局面,早早地就乘機兌掉皇后,由於皇后的威力實在是溢出了我童年的腦殼。一個皇后感受就能夠 hold 住全場了,怎麼還能夠擺8個互不干擾的呢?這確定無解。

  因此說,童言無忌這個說法,是有必要的。

 

  一晃好多年。現在基友問過來了,我琢磨着是該補上這份跨世紀的做業了。

 

  給老爸撥了個電話——

  「喂,爸,家裏的國際象棋放哪了?」

  「…… 壓箱底了吧,得找找。怎麼忽然問這,你要研究西西里防護?」

  「我如今開局都不走 e4 了,要研究也是後翼棄兵。」

  「別特麼瞎扯,給你根杆子你就爬啊,快說,有什麼屁事?」

  「我要研究八皇后問題。」

  「講中文!」

  「我有個問題想研究一下,要在國際象棋棋盤上擺放八個皇后,而且互相不受攻擊,求擺法。」

  「哦,這樣啊…… 那你要國際象棋幹嗎?」

  「我想在國際象棋上試着擺擺啊。」

  「國際象棋沒有八個皇后,你要國際象棋幹嗎?」

  「呃…… 那我能夠拿八個兵當皇后作試驗。」

  「那你直接畫個棋盤擺八個硬幣不是一回事?非要用國際象棋?脫褲子放屁,畫蛇添足!」

  「…… ……」

  「老子懶得翻箱子跟你找了,你乾脆去買四副國際象棋,而後就有八個皇后了。還有事嗎?」

  「沒…… 沒了,爸。」

  「早點休息,多喝水,別熬夜。天氣冷了,注意加衣服……」

  「好,好好。」

   —— 對方掛斷,通話結束。

 

  我默默地打開了淘寶,搜索「國際象棋」,準備買 4 副…… 

  轉念一想,仍是算了,本身畫吧。

  轉念二想,懶得畫了,就在腦子裏擺擺看吧。

 

  首先,作點分析工做。

  雖然還不知道最終的答案長什麼樣,有多少個,但利用國際象棋的規則,能夠知道的是,最終8個皇后的分佈一定是:

  每行有且只有1個,每列有且只有1個

  由於若是有某一行(或列)空置的話,則必然致使另有一行(或列)存在2個皇后。這顯而易見的結果背後,有一個數學概念叫作「抽屜原則」。

  藉助這個「抽屜」,接下來要作的就是一行一行地找出8個位置。

  固然,按一列一列來作也能夠,但在處理圖形圖像等信息時,優先水平方向彷佛更符合人的思惟慣性,聽說這是由於人的兩隻眼睛是水平的

  (跑題了……)

 

  心算8皇后感受有點累,我打算簡化問題。

  從2皇后開始,不過2皇后無解得太昭然若揭了。

  換成3皇后,無解得也是一目瞭然。

  進而思考4皇后的狀況(在4X4的棋盤上放4個皇后)。因而,有點思考的空間了。

 

  一開始,棋盤是空的,第1個皇后能夠任意放置,但爲了思考方便,最好是按照秩序來嘗試,因而先將第1個皇后放置在棋盤第1行第1列格子裏。

  

  BTW,若是你以爲圖中的皇后圖標長得很像 ROLEX 的 Logo,是由於我用的就是 ROLEX 的 Logo 。

  畢竟,他們長得實在是太像了。 

 

  第1行已經有了皇后,下一步是尋找第2行皇后的位置。在這以前,須要計算出第2行此時未被第1個皇后攻擊的棋格分佈。

  

  上圖中呈現的是整個棋盤的狀態,但此時關注的重點在第2行。接下來,將第2個皇后放置於第2行第3列棋格中。

  

  如今,第1行和第2行都有皇后了,從新計算棋盤狀態,以尋找第3行的皇后位置。

  

  通過計算,第3行的全部棋格已經所有處於第1個和第2個皇后的聯合攻擊範圍內了,雖然第4行還有空位,但已經可有可無,當前嘗試能夠宣告 Game Over 了。

  換句話說,剛纔的第2個皇后位置不對。調整一下,將第2個皇后從第3列挪到第4列再試試。

  

  調整以後,繼續更新棋盤狀態。

  

  此時,第3行有一個可用的空位,因而將第3個皇后放在這個棋格中。

  

  而後再次更新棋盤狀態。

  

   Oops,又遇到了相似的狀況,第4行已經沒有棋格能夠用了,因此,剛纔的第3個皇后位置不對。

  但第3行只有一個空位能夠用,而這惟一的一個空位又是錯誤的,這說明,問題仍是出在第2個皇后的位置上。

  再進一步回溯分析,能夠發現,第2行可用的棋格已經都嘗試過了,然而都不對。

  因此,問題其實出在第1個皇后的位置上。也就是說,第一步將皇后放置於第1行第1列的擺法就錯了。

 

  知錯就改,善莫大焉。將第1個皇后挪到第1行第2列,重頭再來。

  

  繼續,更新棋盤狀態。

  

  根據上圖,將第2個皇后置於第2行第4列。

  

  繼續,更新棋盤狀態。

  

  看上去不錯,接着,將第3個皇后置於第3行第1列。

  

  繼續,更新棋盤狀態。

  

  咦,彷佛成了。

  

  BINGO!4皇后的第一個解,找到了。

 

  如今,回顧上面的整個過程,作點抽象,引入一點計算機的思惟,就能夠得出解題流程了。

  

  

  步驟清楚了,如今須要思考的就是過程當中很關鍵的一步——根據已放置的皇后計算下一行棋格狀態的邏輯實現。

 

  這裏須要回到國際象棋的規則自己了。

  一個皇后在棋盤上的攻擊範圍以下圖所示:

  

  

  對這個圖作點數學上的抽象分析:棋盤自己是一個標準的座標平面,每一個棋格都有着很明顯的座標位置。

  因此,上圖能夠轉換成下面的模型:

 

  

  受皇后攻擊的點,按照和皇后(Q點)的相對位置,能夠分紅4類

  • 橫向(A1)
  • 縱向(A2)
  • 正斜(A3)
  • 反斜(A4)

  橫向攻擊其實不用考慮,由於解題的思路自己就是按行來推動的,先天就過濾掉橫向攻擊點了。

  縱向攻擊很容易判斷,Q點 和 A2點 的 x座標 相等,就處於攻擊範圍內。

  不那麼直觀的是兩條斜線的狀況,須要算一下。

  將正斜線攻擊(A3類點)和反斜線攻擊(A4類點)的座標轉換一下,表示成基於Q點的偏移——

  Q:( x0, y0 )

  正斜線 A3:( x0 + m, y0 + m )

  反斜線 A4:( x0 - m, y0 + m )

 

  經過觀察不可貴出規律——

  正斜線上的點: (x0 + m) – x0 = (y0 + m) – y0

  即:A3點的橫座標值 - Q點的橫座標值 = A3點的縱座標值 – Q點的縱座標值

  反斜線上的點: x0 + y0 = (x0 – m) + (y0 + m)

  即:Q點橫座標值 + Q點縱座標值 = A4點橫座標值 + A4點縱座標值

 

  自此,經過皇后所在的棋格判斷棋盤上另外一處方格是否處於被攻擊狀態的邏輯就所有搞清楚了。

  流程和方法都有了,是時候寫代碼實現具體程序了。

 

  用什麼語言來作這事呢?

  QBasic,C,C#,Java,Python,Lua,JavaScript,PHP, ……

  我在腦殼裏慢慢遍歷着我所精通的20門語言,俗話說藝不壓身,但俗話卻沒說選擇困難症,哎……

  (以上這段純屬虛構)

 

  最終,我決定用最近的新歡—— Go 語言來寫這個程序。

 

  延續以前的思路,依然將重心放到4皇后的狀況,直譯上面的分析過程,而後代碼差很少長這樣: 

// 4皇后
package main

import (
    "fmt"
)

func main() {

    // 定義4個皇后,初始化座標爲[-1,-1],即未放置於棋格中。
    var (
        queen1 = [2]int{-1, -1}
        queen2 = [2]int{-1, -1}
        queen3 = [2]int{-1, -1}
        queen4 = [2]int{-1, -1}
    )

    // 放置第1個皇后
    for i := 0; i < 4; i++ { // 遍歷棋盤上的第一行方格(rank1)
        queen1[0] = i
        queen1[1] = 0
        // 更新第2行棋格狀態(此時已放置1個皇后)
        rank2 := render(queen1)
        // 放置第2個皇后
        for i := 0; i < 4; i++ {
            if !rank2[i] {
                queen2[0] = i
                queen2[1] = 1
                // 更新第3行棋格狀態(此時已放置2個皇后)
                rank3 := render(queen1, queen2)
                // 放置第3個皇后
                for i := 0; i < 4; i++ {
                    if !rank3[i] {
                        queen3[0] = i
                        queen3[1] = 2
                        // 更新第4行棋格狀態(此時已放置3個皇后)
                        rank4 := render(queen1, queen2, queen3)
                        // 放置第4個皇后
                        for i := 0; i < 4; i++ {
                            if !rank4[i] {
                                queen4[0] = i
                                queen4[1] = 3
                                // 到此,4個皇后均成功置於棋盤中
                                fmt.Println("solution:", queen1, queen2, queen3, queen4)
                            }
                        }
                    }
                }
            }
        }
    }
}

// 根據已放置的皇后,更新下一行棋格的狀態
// 返回一個含4個bool類型元素的數組,true表示受攻擊的,false表示未受攻擊。
func render(queens ...[2]int) [4]bool {
    // 國際象棋棋盤中的一行,在英文中叫作:rank
    var rank [4]bool
    // 獲取已放置的皇后的數量,能夠獲得下一行的索引
    y := len(queens)
    // 遍歷下一行的棋格
    for x := 0; x < 4; x++ {
        for _, queen := range queens {
            // 經過已放置的皇后的棋格座標來判斷攻擊範圍
            if x-queen[0] == y-queen[1] ||  // 正斜攻擊
                x == queen[0] ||    // 縱向攻擊
                x+y == queen[0]+queen[1] {  // 反斜攻擊
                rank[x] = true
                // 一旦判斷出該棋格受到攻擊,則不用再計算後面的皇后對其影響
                break
            }
        }
    }
    return rank
}

  運行後結果以下:

   2種解,並分別給出了每種解法的4皇后的座標分佈。

  說明一下:這裏我用到一個包含兩整型元素的數組來表示皇后的座標,每個中括號裏面的第1個數字表示 x軸 座標(對應棋盤上的列),第2個數字表示 y軸 座標(對應棋盤上的行)。

 

  如今,4皇后已經解決了,那8皇后呢?

  很簡單,我只須要將 main 函數裏面的 for 循環再寫4套,就搞定了,複製粘貼但是基本功啊。

  (開個玩笑~)

 

  雖然照着上面的代碼,寫8套循環也確實能夠獲得正確的結果,但應該沒有人有勇氣公開地這麼幹吧。

  因此,上面的代碼充其量只能算是個草稿,接下來須要把它改爲像樣的程序。 

  經過前面的分析以及上面的代碼,能夠很明顯地看出4層循環體裏的代碼邏輯是同樣的。

 

  當循環趕上重複時…… 遞歸,就要來了。

 

  但在遞歸以前,先作點小調整。

  增長一個const  n,用於定義棋盤的規格,避免直接使用字面量「4」;

  將用於存儲皇后座標的4個 array 合成1個 slice,這樣就不用作固定次數的初始化了,並且對 slice 的操做也使得代碼看上去更討巧一點。

 

  而後,將以前代碼中,main 函數裏的多重循環部分,精簡成一個遞歸的形式函數調用:

// 放置下一個皇后
// 函數的參數爲已放置的皇后的座標集
func place(queens [][2]int) {
    // 獲取已放置的皇后數量
    y := len(queens)
    // 當已放置的皇后數量未達到n個以前,繼續求解動做
    if y < n {
        // 計算下一行的棋格狀態
        nextRank := render(queens)
        for x := 0; x < n; x++ {
            // 當遍歷到下一行的可用棋格時
            if !nextRank[x] {
                // 放置一個皇后
                queens = append(queens, [2]int{x, y})
                // 而後繼續嘗試下一個皇后的放置
                place(queens)
                // 當上一句的遞歸調用結束時,表示本次求解過程的結束
                // 此時,不管求解是否成功,均須要還本來次的狀態(即拿起皇后,準備嘗試下一次放置)
                queens = queens[:y]
            }
        }
    } else {    
        // 當n個皇后均已放置時,表示一次求解的完成
        // TODO
    }
}

  以前代碼中,用於「根據已放置的皇后計算下一行棋格狀態」的 render 函數,無須調整。

 

  最後,我以爲應該增長一點可視化的工做,將結果直觀的打印出來,雖然這不是解題的必要,但數據可視化絕對是一種人文關懷。

  加個打印結果的函數:

// 打印結果
// 參數說明 - index:當前解法的序號;solution:皇后分佈的座標
func visualize(index int, solution [][2]int) {
    fmt.Println("Solution ", index)
    fmt.Println(strings.Repeat("-", 2*n-1))
    for y := 0; y < n; y++ {
        for x := 0; x < n; x++ {
            if x == solution[y][0] && y == solution[y][1] {
                fmt.Print("Q ")
            } else {
                fmt.Print("* ")
            }
        }
        println()
    }
    fmt.Println(strings.Repeat("-", 2*n-1))
}

  函數 visualize 的調用,天然應該發生在 place 函數體的 else 部分,而且順便記錄一下解法的數量(加一個統計變量 total,統計解法總數)

else {   
        // 當n個皇后均已放置時,表示一次求解的完成
        total++
        visualize(total, queens)
    }

 

  最終的代碼長這樣:

// 8 QUEENS PUZZLE
package main

import (
    "fmt"
    "strings"
)

// 棋盤規格
const n int = 4

// 統計解法總數
var total int

func main() {
    // 用於記錄已放置的皇后
    var queens [][2]int
    // 遞歸求解
    place(queens)
}

// 放置下一個皇后
// 函數的參數爲已放置的皇后的座標集
func place(queens [][2]int) {
    // 獲取已放置的皇后數量
    y := len(queens)
    // 當已放置的皇后數量未達到n個以前,繼續求解動做
    if y < n {
        // 計算下一行的棋格狀態
        nextRank := render(queens)
        for x := 0; x < n; x++ {
            // 當遍歷到下一行的可用棋格時
            if !nextRank[x] {
                // 放置一個皇后
                queens = append(queens, [2]int{x, y})
                // 而後繼續嘗試下一個皇后的放置
                place(queens)
                // 當上一句的遞歸調用結束時,表示本次求解過程的結束
                // 此時,不管求解是否成功,均須要還本來次的狀態(即拿起皇后,準備嘗試下一次放置)
                queens = queens[:y]
            }
        }
    } else {    
        // 當n個皇后均已放置時,表示一次求解的完成
        total++
        visualize(total, queens)
    }
}

// 根據已放置的皇后,更新下一行棋格的狀態
// 返回一個含4個bool類型元素的數組,true表示受攻擊的,false表示未受攻擊。
func render(queens [][2]int) [n]bool {
    var rank [n]bool
    y := len(queens)
    for x := 0; x < n; x++ {
        for _, queen := range queens {
            if x-queen[0] == y-queen[1] ||
                x == queen[0] ||
                x+y == queen[0]+queen[1] {
                rank[x] = true
                break
            }
        }
    }
    return rank
}

// 打印結果
// 參數說明 - index:當前解法的序號;solution:皇后分佈的座標
func visualize(index int, solution [][2]int) {
    fmt.Println("Solution ", index)
    fmt.Println(strings.Repeat("-", 2*n-1))
    for y := 0; y < n; y++ {
        for x := 0; x < n; x++ {
            if x == solution[y][0] && y == solution[y][1] {
                fmt.Print("Q ")
            } else {
                fmt.Print("* ")
            }
        }
        println()
    }
    fmt.Println(strings.Repeat("-", 2*n-1))
}

  運行結果以下:

  將 const n int = 4 改爲 const n int = 8 .

  終於,獲得八皇后的答案了。

  共92互不相同的解。

 

  拿到結果了,能夠再研究研究過程了。去掉可視化工做,只計算解法數量,而後看看程序的性能。

  註釋掉 visualize 函數的調用,並將 main 函數改造一下,統計程序運行的時間:

func main() {

    start := time.Now()

    // 用於記錄已放置的皇后
    var queens [][2]int
    // 遞歸求解
    place(queens)

    end := time.Now()
    elapsed := end.Sub(start)
    
    fmt.Println("Total:", total)
    fmt.Println("Elapsed:", elapsed)
}

 

  在我老舊的一款ThinkPad E系列筆記本上

  

  運行結果以下:

  

  998微秒,不到1毫秒,看上去不錯。

 

  至此,八皇后的問題完全完結。

  事實上,皇后的問題也順便完結了。

 

  將常量 改爲9,試試看:

  

  共352種互不相同的解,耗時1.99毫秒

 

  n = 10 時:

  

  8.99毫秒算出724種互不相同的解。

 

 

 

 

  就這樣吧……

 

 

 

 

後 記

 

原本覺得這個問題就算研究完了,直到有一天和老爸的另外一次通話——

「你上次找老子要國際象棋的那個問題,後來想出來沒有?」

「爸,那小兒科我當天掛完電話,分分鐘就解出來了。」

「滾遠點,怕不是買了4副象棋吧?」

「怎麼可能,我能夠心算8盤棋。」

「滾遠點,你那個問題我後來也想了的,很簡單的問題啊。」

「啊?」(What??? 老爸也解八皇后?)

「你題目只說了擺八個皇后,沒說不讓擺其它的棋子,對吧?你用其它的兵啊、馬啊等棋子把八個皇后隔開,就能夠作到互不攻擊了。」

「@#¥%&*……」

「還有事嗎?」

「沒沒,沒了,爸。」(持續暈眩中)

「早點休息,多喝水,天氣冷了注意加衣服,少熬夜。就這。」

——對方掛斷通話

 

 

 

附:老爸的解法

 

 

 

 

 

 本文已獨家受權給腳本之家(ID:jb51net)公衆號發佈

相關文章
相關標籤/搜索