1. 程式人生 > >演算法之八皇后問題詳解暨終極極限挑戰

演算法之八皇后問題詳解暨終極極限挑戰

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

八皇后問題最早是由國際西洋棋棋手馬克斯·貝瑟爾於1848年提出。之後陸續有數學家對其進行研究,其中包括高斯和康託,並且將其推廣為更一般的n皇后擺放問題。八皇后問題的第一個解是在1850年由弗朗茲·諾克給出的。諾克也是首先將問題推廣到更一般的n皇后擺放問題的人之一。1874年,S.岡德爾提出了一個通過行列式來求解的方法,這個方法後來又被J.W.L.格萊舍加以改進。

一、暴力求解

設八個皇后為xi,分別在第i行(i=1,2,3,4……,8);

問題的解狀態:可以用(1,x1),(2,x2),……,(8,x8)表示8個皇后的位置;

由於行號固定,可簡單記為:(x1,x2,x3,x4,x5,x6,x7,x8);

問題的解空間:(x1,x2,x3,x4,x5,x6,x7,x8),1≤xi≤8(i=1,2,3,4……,8),共8的8次方個狀態;

約束條件:八個(1,x1),(2,x2) ,(3,x3),(4,x4) ,(5,x5), (6,x6) , (7,x7), (8,x8)不在同一行、同一列和同一對角線上。

盲目的列舉演算法:通過8重迴圈模擬搜尋空間中的88個狀態,從中找出滿足約束條件的“答案狀態”。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
八皇后——盲目迭代法
"""
def check_1(a, n):
    for i in range(1, n):
        for j in range(0, i):
            if a[i] == a[j] or abs(a[i]-a[j]) == i - j:
                return False
    return True # 不衝突

def queens_1():
    a= [0 for i in range(8)]
    count = 0
    for a[0] in range(8):
        for a[1] in range(8):
            for a[2] in range(8):
                for a[3] in range(8):
                    for a[4] in range(8):
                        for a[5] in range(8):
                            for a[6] in range(8):
                                for a[7] in range(8):
                                    if check_1(a,8) == False:
                                        continue
                                    else:
                                        print(a)
                                        count +=1
    print('八皇后問題的全部解法:',count)

if __name__ == '__main__':
    queens_1()

以上的思路很簡單,但是效率不高,時間複雜度為N的N次方,八皇后會跑到45s左右,程式碼不好看,而且只能求八皇后問題,無法擴充套件到n皇后,所以接下來用其他思路

其實在上面窮舉的過程中有些情況是不需要再嘗試的,但是我們沒有及時收斂,下面的演算法均是對窮舉的優化。

二、回溯法

  回溯的基本思想:從問題的某一種狀態出發,搜尋可以到達的所有狀態。當某個狀態到達後,可向前回退,並繼續搜尋其他可達狀態。當所有狀態都到達後,回溯演算法結束!

這個演算法就是一個搜尋演算法,對一棵樹進行深度優先搜尋,但是在搜尋的過程中具有自動終止返回上一層繼續往下搜尋的能力,這個演算法其實就是一個搜尋樹,對部分節點進行了剪枝是一種可行的演算法,對八皇后這樣皇后數較少的問題還能夠解決,如果皇后數再大一點就無能為力了

如圖,說明八皇后問題中的回溯演算法:

      

注意:其實就是不斷的通過遞迴函式,去往棋盤中嘗試放皇后,成功就繼續遞迴(即繼續放皇后),失敗就跳出遞迴函式,回溯到上層遞迴函式中,上層遞迴函式中儲存著上一個皇后的位置!!!這就是八皇后中,回溯的概念!

演算法思考,初步思路:

首先如何決定下一個皇后能不能放這裡可以有兩種思路,

第一種是嘗試維護一個8*8的二維矩陣,

每次找到一個空位放下一個皇后就把對應行列對角線上的棋格做個標記,擺放後立即呼叫一個驗證函式(傳遞整個棋盤的資料),驗證合理性,安全則擺放下一個,不安全則嘗試擺放這一行的下一個位置,直至擺到棋盤邊界,當這一行所有位置都無法保證皇后安全時,需要回退到上一行,清除上一行的擺放記錄,並且在上一行嘗試擺放下一位置的皇后(回溯演算法的核心)當擺放到最後一行,並且呼叫驗證函式確定安全後,累積數自增1,表示有一個解成功算出.驗證函式中,需要掃描當前擺放皇后的左上,中上,右上方向是否有其他皇后,有的話存在危險,沒有則表示安全,並不需要考慮當前位置棋盤下方的安全性,因為下面的皇后還沒有擺放

回溯演算法的天然實現是使用編譯器的遞迴函式,但是其效能令人遺憾

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
維護8×8格的矩陣,遞迴求解
"""
import time
def QueenAtRow(chess, row):
    num = len(chess[0])
    if row == num:
        print("---"* num)
        for i in range(num):
            print(chess[i], ' ')
        return
    chessTmp = chess
    # 向這一行的每一個位置嘗試排放皇后
    # 然後檢測狀態,如果安全則繼續執行遞迴函式擺放下一行皇后
    for i in range(num):
        for j in range(num):
            # 擺放這一行的皇后,之前要清掉所有這一行擺放的記錄
            chessTmp[row][j] = 0
        chessTmp[row][i] = 1
        if is_conflict(chessTmp, row ,i):
            QueenAtRow(chessTmp,row +1)

def is_conflict(chess,row ,col):
    step = 1
    while row - step >= 0:
        if chess[row - step][col] == 1:
            return False
        if col - step >= 0 and chess[row - step][col - step] == 1:
            return False
        if col + step < len(chess[0]) and chess[row - step][col + step] ==1:
            return False
        step +=1
    return True

if __name__ == '__main__':
    queenNum = 8   #修改不同的皇后數
    chess = [[0 for i in range(queenNum)] for j in range(queenNum)] #初始化棋盤,全部置0
    time1 = time.time()
    QueenAtRow(chess, 0)
    print('消耗時間為:', time.time() - time1)

時間八皇后跑到0.05s,用上面的程式碼嘗試算9-16皇后問題,開始嘗試解決16皇后問題時,發現時間複雜度已經超出我的心裡預期,一部分是由於程式碼會輸出排列出的結果IO導致,另一方面確實耗時很長,跑了兩小時都沒跑完。

目前N皇后的國際記錄,已經有科研單位給出了25皇后的計算結果,耗時暫未可知。一些演算法高手能在100秒內跑16皇后,上面的演算法效率只能說一般,仍然可以優化:

第二種方法通過一維陣列維護記錄每個皇后的位置

放棄構造矩陣來模擬棋盤位置,我們把問題更加抽象化,八個皇后能放下一定是一行放一個,只需一個數組記錄每個皇后的列數(預設第N個放第N行),那麼問題就被抽象成了陣列的第N個數和前N-1個數不存在幾個和差關係即可(比如差不為零代表不在同一列)。

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
維護一維陣列,遞迴求解
"""
import time
def QueenAtRow2(chess, row):
    num = len(chess)
    if row == num:
        print("---" * num)
        print(chess, ' ')
        return
    chessTmp = chess
    # 向這一行的每一個位置嘗試排放皇后
    # 然後檢測狀態,如果安全則繼續執行遞迴函式擺放下一行皇后
    for i in range(num):
        # 擺放這一行的皇后,之前要清掉所有這一行擺放的記錄
        chessTmp[row] = i
        if is_conflict(chessTmp, row, i):
            QueenAtRow2(chessTmp, row +1)

def is_conflict(chess,row ,col):
    step = 1
    # 判斷中上、左上、右上是否衝突
    for i in range(row-1, -1, -1):
        if chess[i] == col:
            return False
        if chess[i] == col - step:
            return False
        if chess[i] == col + step:
            return False
        step +=1
    return True

if __name__ == '__main__':
    for queenNum in range(4,10):
        # queenNum = 8  # 修改不同的皇后數
        chess = [0 for j in range(queenNum)]  # 初始化棋盤,全部置0
        time1 = time.time()
        QueenAtRow2(chess, 0)
        print('消耗時間為:', time.time() - time1)

與原來相比這樣耗時減少了一半,有了一定的提升。

第三種思路手動維護一個棧

由於遞迴可以看做底層幫你維護的一個堆疊不斷地push、pop,我們也可以通過手動維護一個堆疊來模擬這個遞迴呼叫的過程,只要構造兩個函式backward(往後回溯)、refresh(向前重新整理)來模擬堆疊進出即可。

時間複雜度是一樣的,空間複雜度因為自定義了class,有所上升。很可惜經過測試其效能沒有得到提升。

此外這裡還有通過對稱剪枝”,“遞歸回溯”,“多執行緒”優化後的演算法實現

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
八皇后手動維護堆疊python解法
"""
import queue
def EightQueen(board):
    blen = len(board)
    stack = queue.LifoQueue()  # 用後進先出佇列來模擬一個棧
    stack.put((0,0))    # 為了自洽的初始化
    while not stack.empty():
        i,j = stack.get()
        if check(board,i,j):    # 當檢查通過
            board[i] = j    # 別忘了放Queen
            if i >= blen - 1:
                print(board)   # i到達最後一行表明已經有了結果
                printBoard(board)
                # break
            else:
                if j < blen - 1:    # 雖然說要把本位置右邊一格入棧,但是如果本位置已經是行末尾,那就沒必要了
                    stack.put((i,j+1))
                stack.put((i+1,0))    # 下一行第一個位置入棧,準備開始下一行的掃描
        elif j < blen - 1:
            stack.put((i,j+1))    # 對於未通過檢驗的情況,自然右移一格即可

def check(board,row,col):
    i = 0
    while i < row:
        if abs(col-board[i]) in (0,abs(row-i)):
            return False
        i += 1
    return True

def printBoard(board):
    '''為了更友好地展示結果 方便觀察'''
    import sys
    for i,col in enumerate(board):
        sys.stdout.write('□ ' * col + '■ ' + '□ ' * (len(board) - 1 - col))
        print(' ')

if __name__ == '__main__':
    queenNum = 8
    board = [0 for i in range(queenNum)]
    EightQueen(board)

第四種全排列思路

由於八個皇后的任意兩個不能處在同一行,那麼這肯定是每一個皇后佔據一行。於是我們可以定義一個數組ColumnIndex[8],陣列中第i個數字表示位於第i行的皇后的列號。先把ColumnIndex的八個數字分別用0-7初始化,接下來我們要做的事情就是對陣列ColumnIndex做全排列。由於我們是用不同的數字初始化陣列中的數字,因此任意兩個皇后肯定不同列。我們只需要判斷得到的每一個排列對應的八個皇后是不是在同一對角斜線上,也就是陣列的兩個下標i和j,是不是i-j==ColumnIndex[i]-ColumnIndex[j]或者j-i==ColumnIndex[i]-ColumnIndex[j]。解空間降低到8 的階乘

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
全排列解八皇后問題'2018/9/12'
"""
from copy import deepcopy
def EightQueenByFP(src, slen):
    # 後找:字串中最後一個升序的位置i,即:S[i]<S[i+1]
    i = slen - 2
    while i >= 0 and src[i] >= src[i+1]:
        i -=1
    if i < 0:
        return False
    # 查詢(小大):S[i+1…N-1]中比S[i]大的最小值S[j]
    j = slen -1
    while src[j] <= src[i]:
        j -=1
    # 交換:S[i],S[j]
    src[j], src[i] = src[i], src[j]
    # 翻轉:S[i+1…N-1]
    Reverse(src, i+1, slen-1)
    return True

def swap(li, i, j):
    if i == j:
        return
    temp = li[j]
    li[j] = li[i]
    li[i] = temp
def is_conflict(src):
    slen = len(src)
    for i in range(slen):
        for j in range(i+1,slen):
            if src[i] - src[j] == i - j or src[j] - src[i] == i - j:
                return False
                break
    return True

def Reverse(li, i, j):
    if li is None or i < 0 or j < 0 or i >= j or len(li) < j + 1:
        return
    while i < j:
        swap(li, i, j)
        i += 1
        j -= 1

if __name__ == '__main__':
    queenNum = 8
    src = [i for i in range(queenNum)]
    result = [deepcopy(src)]
    count = 0
    while EightQueenByFP(src, len(src)):
        if is_conflict(src):  # 若對角線衝突則不滿足放置條件,沒有衝突為True
            result.append(deepcopy(src))
            count +=1
    for i in result:
        print(i)
    print(queenNum, '皇后問題的全部解法:',count)

第五種python花式求解實現

#!/usr/bin/env python
# _*_ coding:utf-8 _*_
'''
回溯法解N皇后問題
'''
import random
#衝突檢查,在定義state時,採用state來標誌每個皇后的位置,
# 其中索引用來表示橫座標,其對應的值表示縱座標,
# 例如: state[0]=3,表示該皇后位於第1行的第4列上
def is_conflict(state,nextX):
    nextY = len(state)
    for i in range(nextY):
        # 如果下一個皇后的位置與當前的皇后位置相鄰(包括上下,左右)或在同一對角線上,則說明有衝突,需要重新擺放
        if abs(state[i] - nextX) in (0, nextY - i):
            return True
    return False

#採用生成器的方式來產生每一個皇后的位置,並用遞迴來實現下一個皇后的位置。
def Queens(num, state= ()):
    # 這個state=()說明state一上來是一個空元組
    count = 0
    for pos in range(num):
        if not is_conflict(state, pos):
            # 產生當前皇后的位置資訊
            if len(state) == num - 1:
                yield (pos,)
            # 否則,把當前皇后的位置資訊,新增到狀態列表裡,並傳遞給下一皇后。
            else:
                for result in Queens(num, state + (pos,)):
                    yield (pos,) + result

#為了直觀表現棋盤,用X表示每個皇后的位置
def prettyprint(solution):
    def line(pos, length=len(solution)):
        return '. ' * (pos) + 'X ' + '. '*(length-pos-1)
    for pos in solution:
        print( line(pos))

if __name__ == '__main__':
    qNum = 8
    res = list(Queens(qNum))
    for i in res:
        print(i)
        # prettyprint(i)
    print(len(res))
    # prettyprint(random.choice(res))

除了上面介紹的這些,最主要的是遞歸回溯的思路,此外還有通過位運算的方法,以及網上還有寫的更簡練的程式碼,但是那種一行程式碼求解八皇后的程式碼,這種程式碼除了自己秀一下智商,你自己想想這種程式碼是給別人看的嗎?

擴充套件部分:

八皇后問題本質上是一個在解空間中搜索解的過程,除了一一遍歷以為,還有一些高效的搜尋方式:

1、概率演算法

     基本思想:首先應用隨機函式在一個8*8的矩陣中放入合適的4個皇后(即一半的皇后)然後再應用之前的回溯的方法進行搜尋,隨後迴圈這樣的一個過程,當時間容許的情況下,很快就可以找到滿足所有的解。當然這種方法對回溯法只是進行了一點點的改動,但時間複雜度上將有很大的提高。

2、A*演算法

      這種演算法原本是人工智慧裡求解搜尋問題的解法,但八皇后問題也同樣是一個搜尋問題,因此可以應用這種方法來進行求解。這裡關於A*演算法的定義以及一些概念就不提供了,大家可以上網進行搜尋,網上有很多關於A*演算法的詳細介紹。如果有興趣的朋友可以借閱一本人工智慧的書籍上面有關於A*演算法求解八皇后問題的詳細解答過程,同理例如遺傳演算法,蟻群演算法,粒子群演算法等都可以求解這類搜尋問題

3、廣度優先搜尋

     這個是和回溯法搜尋相反的一種方法,大家如果學過資料結構的應該知道,圖的搜尋演算法有兩種,一種是深度優先搜尋,二種是廣度優先搜尋。而前面的回溯演算法迴歸到圖搜尋的本質後,發現其實是深度優先搜尋,因此必定會有廣度優先搜尋解決八皇后問題,由於應用廣度優先搜尋解決八皇后問題比應用深度優先搜尋其空間複雜度要高很多,所以很少有人去使用這種方法,但是這裡我們是來學習演算法的,這種演算法在此不適合,不一定在其他的問題中就不適合了,有興趣的朋友可以參考任何一本資料結構的資料上面都有關於廣度優先搜尋的應用。