1. 程式人生 > >圖形結構:遍歷模型,分治法,動態規劃,回溯法,BFS,DFS

圖形結構:遍歷模型,分治法,動態規劃,回溯法,BFS,DFS

圖形結構,是樹形結構的擴充套件。

我們在回溯法裡面瞭解到幾種結構:二叉樹,排列樹,完全n叉樹,這幾種解空間型別,都可以直接使用回溯法的框架解決。

二叉樹,排列樹,完全n叉樹,都可以看成x叉樹的變形,而圖形結構就是x叉樹。

在此之前,我們先明白一點:一顆二叉樹是什麼,他是某一顆二叉樹的子樹,同樣的道理,一個圖是什麼,他是某個圖的子圖。

一般的二叉樹問題,我們先處理當前結點,再處理左子樹和右子樹,對應一般的圖的問題,我們先處理當前頂點,再依次處理各個子圖。

在回溯法裡,都是看成解空間的深入,對應這裡的就是子問題的逐步變小,回溯法使用條件是:解空間滿足多米諾性質,也就是部分解不滿足最終解,部分解後面的解也就不滿足最終解,子問題的逐步縮小也是滿足多米諾性質的。

圖的寬度優先遍歷,和限界分支法類似,應該可以使用基於佇列方式或者優先順序佇列的方式,因為圖是一般形式的x叉樹,每一層的叉不知道幾個,都是藉助於標記來判斷是否完全遍歷的,注意標記visited的用法。

又思考了一下,為什麼圖會使用標記visited,其本質是:一個圖分解成:當前頂點+相鄰頂點各個子圖,這種劃分是分治,但是劃分的子問題有重疊,子問題多且相互不獨立時,使用動態規劃程式設計時使用備忘模型,一般使用標記陣列避免重複計運算元問題。

# 圖的定義,鄰接表,使用字典的方式,用於演示,比較直觀

g = {'A':{'B':1,'C':2},
     'B':{'A':1,'C'
:3,'D':4}, 'C':{'A':2,'B':3,'D':5,'E':6}, 'D':{'B':4,'C':5,'E':7,'F':8}, 'E':{'C':6,'D':7,'G':9}, 'F':{'D':8}, 'G':{'E':9} } # 圖的遍歷,寬度優先,也就是從起點開始,一層一層掃描,需要使用佇列資料結構 # 同時圖的遍歷,不同於樹的遍歷,樹的結構確定了,而圖點和點的相連情況無法提前確定 # 搜尋時,需要標記已掃描的點,避免重複掃描 def BFS(graph,start_vertex): # 初始化佇列,把起點放入佇列
queue =[start_vertex,] # 我們還需要定義一個數組記錄已訪問的頂點,一般使用雜湊表,每次查詢時間複雜度O(1) visited = set() # 記錄遍歷的結果 result = [] while queue: # 彈出一個頂點 current_vertex = queue.pop(0) # 假如該節點沒有訪問過,就訪問它,並標記已訪問 if current_vertex not in visited: result.append(current_vertex) visited.add(current_vertex) # 下面就需要訪問這個結點的鄰接頂點 for neighbour_vertex in graph[current_vertex].keys(): # 把鄰接頂點放入佇列,放入之前可以判斷一下,是否已經訪問過 # if neighbour_vertex not in visited: queue.append(neighbour_vertex) return result # 這裡面判斷結點是否訪問過,在彈出的時候判斷,可以改進一下,也就是在入隊的時候 # 就標記成已訪問的頂點,也就是重複的結點不會入棧 def BFS2(graph,start_vertex): queue = [start_vertex,] # 入棧就標記已訪問 visited = set() visited.add(start_vertex) # 用於記錄訪問的結點順序 result =[] while queue: current_vertex = queue.pop(0) result.append(current_vertex) for neighbour_vertex in graph[current_vertex].keys(): if neighbour_vertex not in visited: queue.append(neighbour_vertex) visited.add(neighbour_vertex) return result

圖的深度優先遍歷:

# 這裡面也需要注意標記的用法,我們在什麼時候標記頂點已經被訪問了
# 第一次訪問頂點的時候,還是列印結點結束也就是最後一次訪問結點的時候
# 一般情況下我們習慣在在一個結點確定訪問完畢也就是最後訪問結點的時候,標記結點已訪問
# 實際操作的效率高的是,在我們第一次碰見頂點的時候就標記結點,因為第一次碰見他就代表
# 處理他已經開始了
# BFS在入佇列的時候,標記已訪問,這樣重複的結點就不會入佇列
# DFS在進入遞迴前,標記頂點已訪問,這樣重複結點就不會進入遞迴
# DFS的遞迴,把原問題看成:當前頂點+ 所有相鄰頂點為起點子圖的子問題
# 為什麼不是當前頂點+ 一個相鄰頂點為起點子圖的子問題?想一下你要處理的是當前頂點+
# 剩餘n-1個頂點構成的子圖,這個子圖可能是好幾個分離的圖。
def DFS(graph,start_vertex):    
    def DFSRecursion(graph,start_vertex,visited):        
        result.append(start_vertex) 
        # 遍歷訪問子圖集,訪問前先判斷是否已訪問,未訪問標記已訪問,然後進入子圖處理
        for neighbour_vertex in graph[start_vertex].keys():
            if neighbour_vertex not in visited:
                visited.add(neighbour_vertex)
                DFSRecursion(graph,neighbour_vertex,visited)
    result =[]           
    visited = set()
    # 在進入遞迴前,把頂點標記為已訪問
    visited.add(start_vertex)        
    DFSRecursion(graph,start_vertex,visited)
    return result
    
# 深度優先遍歷,明顯需要用到棧的資料結構
# 一個圖的處理,分為兩個大部分,當前頂點處理 + 所有子圖的處理,
# 所以這個問題的結構是n叉樹,
# 可以參考,回溯法處理(基於深度優先的回溯法,在那裡使用迭代的方法比較少),
# 或者參考二叉樹的處理,先序遍歷,先訪問頂點,再左子樹,後訪問右子樹,逆序放入棧中
# 那麼對應圖的先序遍歷,先訪問頂點,在訪問各個子圖,只要把他們逆序放入棧中就行了
# 同理放入棧中前,先標記頂點已訪問         
def DFS2(graph,start_vertex):
    
    visited = set()
    # 在入棧之前需要先看是否已標記,未標記標記之後放入棧中
    visited.add(start_vertex)
    stack =[start_vertex,]
    result = []
    
    while stack:
        current_vertex = stack.pop()
        # 處理當前結點
        result.append(current_vertex)
        
        # 處理子圖子問題
        for neighbour_vertex in graph[current_vertex].keys(): 
            # 子問題入棧前需要判斷是否已標記,未標記標記之後放入棧中
            if neighbour_vertex not in visited:
                visited.add(neighbour_vertex)
                stack.append(neighbour_vertex)
    
    return result

測試結果:

print(BFS(g,'A'))
print(BFS2(g,'A'))
print(DFS(g,'A'))
print(DFS2(g,'A'))

runfile('D:/share/test/graph.py', wdir='D:/share/test')
['A', 'B', 'C', 'D', 'E', 'F', 'G']
['A', 'B', 'C', 'D', 'E', 'F', 'G']
['A', 'B', 'C', 'D', 'E', 'G', 'F']
['A', 'C', 'E', 'G', 'D', 'F', 'B']

不要認為深度優先遍歷的遞迴方式和迭代方式結果不一樣,假如需要一樣,那迭代棧實現方式就需要逆序放入棧。