[算法系列]搞懂DFS(2)——模式套路+經典例題詳解(n皇后問題,素數環問題)

本文是算法系列遞歸講解中講述dfs的第二篇, 在上一篇: [算法系列]搞懂DFS(1)——經典例題(數獨遊戲, 部分和, 水窪數目)圖文詳解中, 已經通過三個例題講述了dfs的思路以及設計方法, 本文先歸納常見dfs套路, 總結一般思路, 之後再通過兩個經典例題(n皇后, 素數環) 進行鞏固加深.

1. dfs常見模式

dfs本質上來說是對解空間所有解的一種枚舉(遍歷) , 但與暴力搜索所不同的是, 在進行求解時, 會優先考慮**“一條道走到黑, 不撞男牆不回頭”(深度優先)** 進行遍歷, 參考下面這幅圖.

在這裏插入圖片描述

上圖是一顆解答樹, 你可以想成每一個結點都是我們在求解過程中的每一個 狀態 ,比如在上篇的數獨遊戲中:

  • a 結點表示初始狀態,
  • b 結點表示在第一個位置選了一個數字(例如1)的狀態,
  • c結點表示在第一個位置選了另一個數字(例如2)
  • e 結點表示在第一個位置上選了1之後, 再進一步於第二個位置選上一個數字(例如2)

從a -> b, b->e, g->f 這樣在結點只見轉移的我們稱之爲: 狀態轉移 其中,a->b相當於試探,e->b相當於回溯

現在我們來好好談談回溯:

回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇並不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術爲回溯法,而滿足回溯條件的某個狀態的點稱爲“回溯點”。

圖中的e點就是一個走不下去的點,此時就只好回到b狀態(如紅色數字3),注意,此時這個位置理應也應當置爲0,選上下一個選優條件,進行下一次試探.(在數獨遊戲中, 從1~9逐次選擇數字就是一種選優條件,1不行咱選2,…)

道理就那麼多,接下來看看dfs的思路:

  • 定義狀態: 通常狀態就是我們所謂的函數及其參數, 每一個狀態的區分關鍵在於參數內容不同
  • 狀態轉移: 在於遞歸調用的過程,如上圖的那些箭頭
  • 優選條件:按照怎樣的一個規則去進行狀態轉移, 比如數獨遊戲中我們是從1~9由小到大逐個試探

那麼再從遞歸問題的設計思路來看看dfs應該怎麼解答:

  • 找出口: 即通常所說的最小條件, 有時也是邊界條件
  • 找重複:對於當前問題的解法,應該是和子問題解法相同的,這一點不再贅述
  • 找變化:在遞歸中有狀態轉移,往往就有變化的量(否則怎麼能叫求解呢?)這一點通常將其作爲參數

好了: 現在可以發揮想象力,大概寫出dfs的僞代碼:

dfs(參數1,參數2,參數3,...):
	if 邊界條件:
		//副作用,比如添加到解集中
		return
	從候選集中按照選優條件取出一個 e:
		if e 符合條件:
			這裏可能改變狀態,比如填上一個數字
			dfs(參數1,參數2,參數3,...) //這裏某些參數可能會變,以表示到達下一個狀態,比如dfs(arr,x+1,y+1)
			回溯,將剛剛的改變復位
		

這是大概模式,具體實現是要根據問題進行修改.下面來看看兩道經典的題

2. n皇后問題

'''
n 皇后問題

在n×n格的棋盤上放置彼此不受攻擊的n個皇后。
按照國際象棋的規則,皇后可以攻擊與之處在同一行或同一列或同一斜線上的棋子。
即n後問題等價於再n×n的棋盤上放置n個皇后,任何2個皇后不妨在同一行或同一列或同一斜線上。

輸入 n, 返回解法的個數
'''

如圖爲一個皇后的攻擊範圍:

在這裏插入圖片描述

如圖爲一個8皇后的解的狀態:

在這裏插入圖片描述

任何兩個皇后不在同一行同一列同一斜線上, 那麼這樣的解的狀態有幾種呢?

思路:

dfs唄,思路暴力而簡單, 先在第一行放一個queue,再在下一排放下一個滿足條件的q…以此類推,如果發現實在放不動了則回溯,尋找上一次的狀態的下一個合法位置, 所有位置都不行則再次回溯…

狀態確立很簡單,即當前哪些位置上擺了queue, 現在需要考慮的是如何存儲這一狀態. 顯然,二維數組是可以的,但是這有一個更爲巧妙的存儲方法: 用一個長度爲n的一維數組rec,對應下標爲對應行數,其相應值爲放上queue的縱座標位置. 例如:

  • rec[-100,-100,-100,-100,-100,-100,-100,-100,] 表示所有位置都沒有queue
  • rec[0,-100,-100,-100,-100,-100,-100,-100,] 表示第一行第一列行一個queue
  • rec[0,6,3,5,7,1,4,2,] 表示即爲上圖這個合法結果

有了前面的基礎,僞代碼就不上了:


res = []#存放最終結果的list, 裏面存放的每個正確結果的rec
def n_queue(n):
    rec = [-100] *n    #存放解, rec[i]表示第i行的皇后放在第rec[i]上
    dfs_n_queue(rec,0)    #從第0行開始

    print(res)

def dfs_n_queue(rec, row):
    if row == len(rec):		# 如果達到邊界,即所有行都移填上
        rec_cp = rec.copy()
        res.append(rec_cp)	#res中加入rec的克隆
        return

    for y in range(0 , len(rec)):    #	對當前行的每一列進行試探
        if check(rec, row, y):		 #   判斷每行每列每斜線,若沒有第二個,則check返回true
            rec[row] = y			#   檢查成功,rec[row]=y 表示row+1行y+1列放入皇后
            dfs_n_queue(rec, row + 1)#狀態轉移到下一個位置
    rec[row ]= -100 # 均不合法,返回上一層前將該位置恢復
    
# 判斷合法的函數    
def check(rec , x, y):
    for i in range(0 ,  len(rec)):
        #判斷同行 如果行數相同,跳過
        if i == x :
            continue
        #判斷同列: 
        if rec[i] == y:
            return  False
        #判斷主對角線 比如 (0,0)和(1,1) =>橫縱座標差相等
        if rec[i] - i ==  y-x:
            return  False
        #判斷副對角線 比如 (1,1)和(0,2) =>橫縱座標和相等
        if rec[i] + i == y + x:
            return  False
    return  True
n_queue(4)
# print: [[1, 3, 0, 2], [2, 0, 3, 1]]

3. 素數環

'''
輸入正整數n,對1-n進行排列, 使得相
輸出時從整數1開始,逆時針排列,同一個環應該恰好輸出1次鄰兩個數之和均爲質數
n<=16

如輸入:6
輸出:
1 4 3 2 5 6 
1 6 5 2 3 4
'''

很顯然也是可以用dfs進行求解的. 從第二個數開始, 對其左右兩邊進行求和(右邊無數字則不求解)比如:

1,2 	... 			[滿足]
1,2,3	...				[滿足]
1,2,3	...				[滿足]
1,2,3,4	...				[滿足]
1,2,3,4,5 ...			[不滿足,回溯]
1,2,3,4,6,...			[不滿足,回溯]
1,2,3,4 ...				[沒有更多選擇,繼續回溯]
1,2,3, ...				[沒有更多選擇,繼續回溯]
1,2,5, ...
...

由上我們可以看到, 我們在求解過程中,實際上的思路的和暴力枚舉有類似之處的.bingo, 其實從另一種角度來說, dfs的本質就是暴力枚舉. 不過不同在於,我們一邊列舉,一邊進行驗證: 當發現1,2,3,4,5,已經不滿足了,就沒必要繼續把6寫上去,而進行回溯.

傳統的暴力枚舉是將所有串全部寫出, 再來判斷是否滿足情況. 即該題中: 6位長度的串有6!中寫法… 對空間要求過多.

也沒有必要把整個串寫完整再進行判斷,比如1,2,3… 無論後面無論怎麼填均不合法, 這可以直接pass了. 更爲省時.

而將不滿足的直接pass, 將整個解答樹直接pass, 就是我們常常說的 剪枝 了. 當進入一個不可能爲解的境地, 直接返回.

好了, 按照dfs模式和遞歸設計思路. 下面直接上代碼

def zhishu_circle(n):
    rec = [0] * n		#定義狀態數組, 即上述的[1,2,3...]
    rec[0] = 1
    dfs_zhishu_circle(n, rec, 1) # rec[0]固定爲1,從下標爲1開始


def dfs_zhishu_circle(n,rec,cur):
    if cur == n:	#當前行到達邊界,所有數字填完,得到結果
        print(rec)
        return

    for tem in range(2 , n +1):		#對2~n+1按順序進行選擇試探
        if check_zhishu(n,rec,cur,tem):	#如果滿足題目要求
            rec[cur] = tem
            dfs_zhishu_circle(n, rec ,cur+1)
    rec[cur] = 0			# 均不滿足, 返回時恢復

def check_zhishu(n , rec, cur,tem):		
    for i in range(0, cur):
        if rec[i] == tem :  #這個數字當然取兩遍
            return False
    res_l = check_zhishu_core(rec[cur - 1] + tem) #判斷左邊和質數
    if (rec[(cur+1) % n] == 0):     #如果右邊沒填, 不考慮右邊
        res_r = True
    else: #否則也要考慮右邊相鄰和
        res_r =check_zhishu_core(tem + rec[(cur + 1) % n])
    return res_l and res_r

import math
#判斷k是否爲質數
def check_zhishu_core(k):
    for i in range(2, k):
        if (k % i == 0):
            return False
    return True

關於dfs 還有其他的介紹,比如圖算法的一些內容, 樹裏面其他應用也是比較常見,不過在遞歸專題中,介紹到這裏差不多啦.

如果對具體代碼實現上(比如狀態轉移, 回溯)有一些問題的小夥伴可以倒回去看上一篇文章, 應該就能理解了.

接下來的文章將介紹 貪心, 動態規劃等問題. 這兩個和現在以及之前談論話題的關係也極爲緊密.

往期回顧:

  1. [算法系列] 搞懂遞歸, 看這篇就夠了 !! 遞歸設計思路 + 經典例題層層遞進
  2. [算法系列] 遞歸應用: 快速排序+歸併排序算法及其核心思想與拓展 … 附贈 堆排序算法
  3. [算法系列] 深入遞歸本質+經典例題解析——如何逐步生成, 以此類推,步步爲營
  4. [算法系列]搞懂DFS(1)——經典例題(數獨遊戲, 部分和, 水窪數目)圖文詳解
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章