1. 程式人生 > >Python----遞歸------Eight Queens 八皇後問題

Python----遞歸------Eight Queens 八皇後問題

們的 說明 為什麽 遍歷 狀況 初始 方法 ttl text

遞歸思想是算法編程中的重要思想。

作為初學者,對遞歸編程表示很蒙逼,每次遇到需要遞歸的問題,心裏就有一萬頭草泥馬飛過~~~~~~(此處略去一萬頭草泥馬)

在B站看數據結構與算法的視頻時,視頻中給了兩個非常典型的例子——《漢諾塔》和《八皇後問題》,就希望自己用Python實現一下這兩個遞歸程序,其中漢諾塔問題比較簡單,還是能夠理解,這裏就不講了。

《八皇後問題》:說要在一個棋盤上放置8個皇後,但是不能發生戰爭,皇後們都小心眼,都愛爭風吃醋,如果有人和自己在一條線上(水平、垂直、對角線)就會引發撕13大戰,所以我們就是要妥當的安排8位娘娘,以保後宮太平。[1]

與網上普遍搜到的利用yield函數

完成遞歸方法不同,註意時因為對於初學Python的我來說,完全搞不懂yield怎麽用,更不要說將其放在遞歸程序裏了, [捂臉]

下面我會對我的代碼進行逐一解釋,希望更多的像我這樣的初學者能夠看懂。!!!敲黑板!!!------------不僅要看懂,還要自己會寫代碼-----------------


首先聲明:

代碼中位置狀況采用list列表的形式, 如,chess[0] = 4代表在棋盤第1行的第5個位置放置皇後。

count = 0                   # 定義一個全局變量count, 用於八皇後方案的統計

# conflict 函數為沖突函數,主要用於判斷新添加的位置是否與前面的位置沖突
# pos 為新添加的位置, chess為添加pos位置前的皇後位置分布 def conflict(pos, chess): len_chess = len(chess)       # 現在已經分配了幾行,幾有幾個皇後在棋盤上,因為初始chess為空,每次判斷一個符合規則的位置後再加入到棋盤中 for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 這一行為關鍵,太多了就放後面了 return True return False # Queen函數為遞歸函數
def Queens(num,chess): global count         # python 中要再函數中使用之前定義的全局變量,必須再函數中用 global 聲明一下 if len(chess) == 8:          # 判斷chess的長度,當chess長度為8的時候,說明8個皇後都已經全部放好位置了,就可以直接輸出相應的方案了 count += 1 print(chess) else:                 # chess長度小於8,說明還有沒安排好的皇後,所以繼續安排剩下的位置 for pos in range(num):      # 對接下來的一行的8個位置進行遍歷 if not conflict(pos, chess): # 對每個位置進行沖突判斷 chess1 = []   #關鍵是這三行,用於構造棋盤的副本,這三句非常重要,具體原因放在後面解釋 for i in chess: chess1.append(i) chess1.append(pos)   # 符合規則,則將其附在chess副本的後面 Queens(num,chess1)   # 遞歸調用Queens函數 board = [] # 初始化空棋盤 Queens(8,board) # 調用函數 print(count) # 打印總共有多少中方案

沖突判斷

def conflict(pos, chess):
    len_chess = len(chess)   # 現在已經分配了幾行,幾有幾個皇後在棋盤上,因為初始chess為空,每次判斷一個符合規則的位置後再加入到棋盤中
    for i in range(len_chess):if abs(chess[i] - pos) in (0, len_chess - i): # 這一行為關鍵,太多了就放後面了
            return True
    return False

根據規則:有三種情況存在沖突,我i們一一來列舉分析一下:

  1. 在同一列的情況,若兩個皇後在同一列,那麽這兩行的縱坐標相同,反映到我們的棋盤定義上則未:chess[i] = chess[j] 即第i行與第j行的兩個皇後在同一列了
  2. 第二種情況則為在“\” 對角線上,那麽各位置的(橫坐標 - 縱坐標)的值時一樣的,假設chess[i] = a1,chess[j] = a2在對角線"\"上,i - a1 = j - a2。 如(0,1),(1,2),(2,3)橫縱坐標之差都為1
  3. 第三種情況則為在" / " 對角線上,那麽各位置的(橫坐標 + 縱坐標)的值時一樣的,假設chess[i] = a1,chess[j] = a2在對角線"\"上,i + a1 = j + a2。 如(0,3),(1,2),(2,1)橫縱坐標之和都為1

我們再觀察,就會發現(這個我自己想時想不出來的,也是對照著代碼思考出來的):

對於被判斷是否符合要求的元素,其索引恰好為現在chess的長度,因為最後一個元素的索引為len(chess)-1,那麽新的元素的索引恰為len(chess)

我們另 len_chess 為當前chess的長度,則三種情況對應的表達式為:

  1. chess[i] - pos = 0
  2. chess[i] - i = pos - len_chess ---------> chess[i] - pos = i-len_chess
  3. chess[i] + i = pos + len_chess ---------> chess[i] - pos = len_chess - i

由於 len_chess > i, 所以可以得到當abs(chess[i] - pos)==0 或len_chess - i時則不符合規則,返回True :

棋盤副本建立

        for pos in range(num):   #對接下來的一行的8個位置進行遍歷 
            if not conflict(pos, chess): #對每個位置進行沖突判斷
        chess1 = []          #關鍵是這三行,用於構造棋盤的副本,這三句非常重要,具體原因放在後面解釋
                for i in chess:
                    chess1.append(i)
                chess1.append(pos)    # 符合規則,則將其附在chess副本的後面
                Queens(num,chess1)    # 遞歸調用Queens函數

這裏首先提一點的是,python裏面所有如果要將一個list拷貝一份,不能直接像C語言裏面一樣用幅值語句,需要重新初化,

我個人的理解是:因為list的名稱為一個地址,當我們將一個地址幅值給一個新的變量時,新的變量指向的還是原來那個數組,所以並沒有起到拷貝的作用。 比較典型的例子就是規定大小的二維數組的建立

言歸正傳,我們為什麽要新建立一個副本呢?

  • 因為我們需要使用副本來進行深層次的遞歸,如果不使用副本chess1而直接使用原始數據chess,那麽隨著遞歸的深入,chess所對應的地址內的值在不斷變化,當返回到遞歸點的時候,原始數據已經不存在了,

只會從當前chess對應的數組繼續進行遞歸。

  • 比如,當 chess = [0,2,4,1,3]時,此時我們需要填充第6行皇後的位置,但是通過判斷,發現第6行所有位置都不符合要求,那麽此時程序應該退回chess = [0,2,4,1,4]再繼續判斷第6行是否有符合的位置,但是若不用副本

的話,程序會退回,但是chess所對應的值還是[0,2,4,1,3],這樣是錯誤的。

QAQ: 這一段我也沒有講的特別清楚,主要關鍵就是這個副本的建立使原始數據不受破壞,可以在遞歸從深層跳出的時候保持為原來的狀態繼續進行,可以在編譯器中單步運行看一看再理解會更好

[1]. 摘自http://www.cnblogs.com/littleseven/p/5362791.html, 斯認為該博主寫的還可以,

Python----遞歸------Eight Queens 八皇後問題