淺談N皇后問題解法及可視化實現


可以訪問我的知乎:https://zhuanlan.zhihu.com/p/478732443

問題提出

一般地,\({N}\)​皇后問題描述如下:

在大小為\({N×N}\)的棋盤上擺放\({N}\)個皇后,使其兩兩之間不能互相攻擊,即任意兩個皇后都不能處於棋盤的同一行、同一列或同一斜線上,求出滿足條件的所有棋局及局面總數。

特殊地,當\({N=8}\)​​時為著名的以國際象棋棋盤為背景的8皇后問題,易解得符合條件的局面總數為92。本實驗主要探究\({N×N}\)​​的棋盤上擺放\({N}\)​​​個皇后的一般問題。

問題分析

\({N}\)​皇后問題可以維護一個長度為\({N}\)​的一維數組q表示局面情況。\({q_i(i=0,…N-1)}\)​表示第\({i}\)​行皇后放置位置所在列數\({j(j=0,…N-1)}\)​,每個局面可看作由\({0}\)​到\({(N-1)}\)​序列組成的一個排列,原問題可以轉化成尋找所有不導致行/列/斜線沖突的排列。

直觀的想法是將\({N}\)​皇后問題看成組合問題,\({N×N}\)​的棋盤上每個格都有防放置皇后和空格兩種情況,總情況數達\({2^{N^2}}\)​,指數爆炸在時間和空間復雜度上都難以負擔。一種改進思路是考慮每列僅允許出現一個皇后,按照行先序順序放置皇后,第一行有\({N}\)​種可能,第二行有\({N-1}\)​種可能…第\({N}\)​行有1種可能,總情況數縮減為\({N!}\)​略有改善。為進一步提高算法效率,考慮棋盤上每條斜線上僅允許出現一個皇后,在此聯想到回溯剪枝的算法。

算法描述

引入N叉樹的數據結構,構建\({N}\)​​皇后問題的解空間樹。排列長度\({N}\)​​為樹的深度,據此判定遞歸的出口。初始狀態將一維數組q置空,以行先序從第0行開始遞歸求解。遍歷\({i}\)​​行每一列\({j}\)​​,若q中已有\({j}\)​​(即第\({j}\)​​列已經放置了皇后),則發生剪枝,繼續遍歷其他列;若q中沒有\({j}\)​​,則\({q[i]=j}\)​​,遞歸式進入\({i+1}\)​​行的遍歷,直到數組q中已經賦滿\({N}\)​​個列號,則進入遞歸出口進行判定局面的合理性(即每個行/列/斜線最多僅有1個皇后)。若局面合理,將該合理局面保存在二維數組result中,繼續回溯探索其他合理局面;若局面不合理,同樣回溯探索其他合理局面。以此類推,直到遍歷完解空間中所有情況,result中所保存的就是所有合理局面,result的長度就是\({N}\)​​皇后問題滿足條件的所有局面總數。

其中判斷局面合理性的方法具體如下:

由數組q可獲得第\({i}\)​個皇后的位置記為\({(x_i, y_i)}\)​,\(i=0,1,…N-1\)​,一個合理的局面需要滿足以下條件(\({\forall j\ne i,j=0,1,…N-1}\)​):

  1. \({x_j\ne x_i}\)​(橫行)
  2. \({y_j\ne y_i}\)​​(縱行)
  3. \({x_j+y_j\ne x_i+y_i}\)(主斜線)
  4. \({y_j-x_j\ne y_i-x_i}\)(反斜線)

以4皇后為例,下圖為剪枝與回溯思想的示意圖:(僅示意,不代表真實求解情況)

代碼實現

核心算法代碼

[編譯環境]

Windows 系統|PyCharm 編譯器|python 3.8.11

傳統解決\({N}\)​皇后問題的算法都是基於遞歸的回溯算法,但單線程遞歸程序不易可視化。因此,我分別設計了用於求解\({N}\)​皇后所有解法數的遞歸函數search()和用於窗口可視化的基於棧實現的非遞歸函數 next(),並將它們封裝在類NQueen中。其中,search()通過調用dfs()方法遞歸求解所有\({N}\)​皇后問題的局面;next()輸入一組\({N}\)​合理的\({N}\)​皇后排列返回下一組合理的\({N}\)​皇后排列,默認開始為空排列,默認最后一組合理的\({N}\)​皇后排列的下一組排列為空排列。

NQueen代碼如下:

class NQueen:
    def __init__(self, N):
        self.N = N  # 方形棋盤邊長=皇后數量
        self.result = []  # 所有皇后合理安放局面

    def check(self, cur):
        boolMatrix = np.zeros((self.N, self.N))  # 轉化成布爾矩陣
        for i in range(len(cur)):
            boolMatrix[i, cur[i]] = 1
        flag = True  # 合格局面
        # 判列
        flag &= all(np.sum(boolMatrix, axis=0) == np.ones(self.N))
        if not flag:
            return flag
        # 判行
        flag &= all(np.sum(boolMatrix, axis=0) == np.ones(self.N))
        if not flag:
            return flag
        # 判左下右上斜線:i+j∈[0,2*(N1-1)]
        for tmpadd in range(0, 2 * (self.N - 1) + 1):
            tmp = [boolMatrix[i, tmpadd - i] for i in range(self.N) if 0 <= tmpadd - i < self.N]
            flag &= (sum(tmp) <= 1)
            if not flag:
                return flag
        # 判左上右下斜線:i-j∈[-(N1-1),(N1-1)]
        for tmpminus in range(1 - self.N, self.N):
            tmp = [boolMatrix[i, i - tmpminus] for i in range(self.N) if 0 <= i - tmpminus < self.N]
            flag &= (sum(tmp) <= 1)
            if not flag:
                return flag
        return flag  # 運行到這里一定是經過四大檢驗的合格局面

    def outputresult(self, tofile):
        count = len(self.result)
        print(f'{self.N}×{self.N}棋盤上放置{self.N}個皇后的可行局面總數:{count}')
        if tofile:
            with open(f'../../result/N={self.N}.txt', 'w') as f:
                f.write(f'{count}\n')
                for each in self.result:
                    for i in range(len(each)):
                        f.write(str(each[i] + 1))
                        if i != len(each) - 1:
                            f.write(',')
                        else:
                            f.write('\n')

    def dfs(self, cur):
        # cur:現在搜索階段,行先序,0~N-1表示每行具體放的位置
        if len(cur) >= self.N:  # 遞歸出口
            if self.check(cur):
                self.result.append(cur.copy())  
            return
        for i in range(0, self.N):
            if i not in cur:  # 剪枝排除掉一些列重復的情況
                cur.append(i)
                self.dfs(cur)
                cur.pop()

    def search(self, tofile):
        self.dfs([])
        self.outputresult(tofile=tofile)

    def next(self,cur):# 輸入一個狀態返回下一個符合的狀態,開始默認空狀態,輸入最后一個狀態輸出[]
        # 首位添加-1
        cur = [-1,*cur]
        # N進制加1
        if len(cur)>1:
            cur[-1]+=1
            for i in range(-1,-self.N,-1):
                if cur[i]==self.N:
                    cur[i]=0
                    cur[i-1]+=1
                else:
                    break
        # 用+1狀態獲得下一合理局面
        pop = -1
        flag = 0
        while len(cur):
            if len(cur)-1 == self.N:  # 遞歸出口
                tmp = cur[1:]
                if self.check(tmp):
                    return tmp
                else:# 長度滿了但不符
                    pop = cur.pop()
                    flag = 1
            # 長度未滿,直接加
            for i in range(flag*(pop+1), self.N):
                if i in cur:  # 剪枝排除掉一些列重復的情況
                    continue
                cur.append(i)
                flag = 0
                break
            else:
                # 子樹遍歷完了彈棧
                pop = cur.pop()
                flag=1
        return []

可視化

調用python中的tkinter庫完成\({N}\)​​​皇后問題求解可視化呈現,代碼詳見附件。由於\({N}\)​​​較大時展示不便,在此取\({N}\)​​的調節范圍為\([4,11]\)​​​,部分界面可視化如下:

可視化展示 (2)
可視化展示 (4)

具體界面操作見視頻演示(連續執行是循環播放所有可能的局面)。

結果呈現

\({N\ge15}\)​​時合理局面數已經超過兩百萬,程序運行時間較長,在此展示\({N\le14}\)​​​的N皇后問題合理局面總數。

N皇后總情況數

觀察結果,僅當\({N=1}\)​或\({N\ge4}\)​時\({N}\)​​皇后問題才有非零解。

評價總結

優點

  • 本實驗將傳統的八皇后問題推廣至\({N}\)​皇后問題,更具普遍性。
  • 本實驗采用剪枝+回溯算法避免了組合爆炸問題,在合理的時間空間復雜度內得到了准確的\({N}\)​​皇后問題的合理局面數。
  • 本實驗分別使用遞歸方法和非遞歸方法求解\({N}\)皇后問題,適用性強,靈活度高,便於可視化呈現

拓展

一種改進的思路是對於不同的皇后問題,使用不同的方法計算合理局面數。

對於\({N\ne2/3/8/9/14/15/26/27/38/39}\)​​的任意N皇后問題,可以采取分治法確定部分解。例如可以通過5皇后問題的解確定25皇后問題的部分解進而確定125皇后問題的部分解,示意圖如下:

改進

以此類推,分治思路成倍向外擴展便能生成一個無窮皇后問題的解。

總結

本實驗對傳統的八皇后問題進行推廣,在求解\({N}\)​皇后問題所有滿足條件的棋局及局面總數的過程中掌握了回溯法避免組合爆炸的思想以及剪枝減少時間復雜度的優化算法,同時還設計非遞歸算法對\({N}\)​皇后問題的結果進行了清晰的可視化呈現,還進一步探究了分治法在\({N}\)​皇后問題中的應用,為生成無窮皇后問題的解提供了可行思路。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM