08 圖的數據結構和算法


圖的遍歷

  • 深度優先遍歷
    有些類似前序遍歷,從圖的某一頂點開始遍歷,被訪問過的頂點就做上已訪問的記號,接着遍歷此頂點所有相鄰且未訪問過的頂點中的任意一個頂點,並做上已訪問的記號,再以該點為新的起點繼續進行深度優先的搜索。
    這種遍歷方法結合了遞歸和堆棧兩種數據結構的技巧,由於此方法會造成無限循環,因此必須加入一個變量,判斷該點是否已經遍歷完畢。
  class Node:
      def __init__(self):
          self.val = 0
          self.next = None
  
  head = [Node()] * 9  # 聲明一個節點類型的鏈表數組
  run = [0] * 9
  
  def dfs(current):
      run[current] = 1
      print("[%d] " % current, end='')
      ptr = head[current].next
      while ptr != None:
          if run[ptr.val] == 0:  # 該頂點未遍歷
              dfs(ptr.val)
          ptr = ptr.next
  
  # 聲明圖的邊線數組
  data = [
      [1, 2], [2, 1], [1, 3], [3, 1],
      [2, 4], [4, 2], [2, 5], [5, 2],
      [3, 6], [6, 3], [3, 7], [7, 3],
      [4, 8], [8, 4], [5, 8], [8, 5],
      [6, 8], [8, 6], [7, 8], [8, 7]
  ]
  
  for i in range(1, 9):
      run[i] = 0
      head[i] = Node()
      head[i].val = i  # 給各個鏈表頭設置初值
      head[i].next = None
      ptr = head[i]
      for j in range(20):  # 20條邊線
          if data[j][0] == i:  # 如果起點和鏈表頭相等,就把頂點加入鏈表
              newnode = Node()
              newnode.val = data[j][1]
              newnode.next = None
              while True:  # 這樣寫有什么優勢?
                  ptr.next = newnode  # 加入新節點
                  ptr = ptr.next
                  if ptr.next == None:
                      break
  
  print("圖的鄰接表內容:")
  for i in range(1, 9):
      ptr = head[i]
      print("頂點 %d ==> " % i, end='')
      ptr = ptr.next
      while ptr != None:
          print(" [%d] " % ptr.val, end='')
          ptr = ptr.next
      print()
  
  print("深度優先遍歷的頂點:")
  dfs(1)
  print()
圖的鄰接表內容:
  頂點 1 ==>  [2]  [3] 
  頂點 2 ==>  [1]  [4]  [5] 
  頂點 3 ==>  [1]  [6]  [7] 
  頂點 4 ==>  [2]  [8] 
  頂點 5 ==>  [2]  [8] 
  頂點 6 ==>  [3]  [8] 
  頂點 7 ==>  [3]  [8] 
  頂點 8 ==>  [4]  [5]  [6]  [7] 
深度優先遍歷的頂點:
  [1] [2] [4] [8] [5] [6] [3] [7] 
  • 廣度優先遍歷
    利用隊列和遞歸技巧。從圖的某一頂點開始遍歷,被訪問過的頂點做上已訪問的記號,接着遍歷此頂點的所有相鄰且未訪問過的頂點中的任意一個頂點,並做上已訪問的記號,再以該點為起點繼續進行廣度優先遍歷。
  MAXSIZE = 10  # 定義隊列最大容量
  
  front = -1  # 指向隊列的前端
  rear = -1  # 執行隊列的后端
  
  
  class Node:
      def __init__(self, x):
          self.val = x
          self.next = None
  
  
  class GraphLink:
      def __init__(self):
          self.first = None
          self.last = None
  
      def my_print(self):
          current = self.first
          while current != None:
              print(" [%d] " % current.val, end='')
              current = current.next
          print()
  
      def insert(self, x):
          newnode = Node(x)
          if self.first == None:
              self.first = newnode
              self.last = newnode
          else:
              self.last.next = newnode
              self.last = newnode
  
  
  # 入隊
  def enqueue(value):
      global MAXSIZE
      global rear
      global queue
      if rear >= MAXSIZE:
          return
      rear += 1
      queue[rear] = value
  
  
  # 出隊
  def dequeue():
      global front
      global queue
      if front == rear:
          return -1
      front += 1
      return queue[front]
  
  
  # 廣度優先搜索
  def bfs(current):
      global front
      global rear
      global Head
      global run
      enqueue(current)
      run[current] = 1
      print(" [%d] " % current, end='')
      while front != rear:  # 判斷當前隊列是否為空
          current = dequeue()
          tempnode = Head[current].first  # 先記錄當前頂點的位置
          while tempnode != None:
              if run[tempnode.val] == 0:
                  enqueue(tempnode.val)
                  run[tempnode.val] = 1
                  print(" [%d] " % tempnode.val, end='')
              tempnode = tempnode.next
  
  
  # 聲明圖的邊線數組
  data = [[0] * 2 for row in range(20)]
  data = [
      [1, 2], [2, 1], [1, 3], [3, 1],
      [2, 4], [4, 2], [2, 5], [5, 2],
      [3, 6], [6, 3], [3, 7], [7, 3],
      [4, 8], [8, 4], [5, 8], [8, 5],
      [6, 8], [8, 6], [7, 8], [8, 7]
  ]
  run = [0] * 9  # 記錄個頂點是否遍歷過
  queue = [0] * MAXSIZE
  Head = [GraphLink] * 9
  
  print('圖的鄰接表內容:')
  for i in range(1, 9):  # 共有8個頂點
      run[i] = 0
      print("%d ==> " % i, end='')
      Head[i] = GraphLink()
      for j in range(20):
          if data[j][0] == i:
              datanum = data[j][1]
              Head[i].insert(datanum)
      Head[i].my_print()
  
  print("廣度優先遍歷的頂點:")
  bfs(1)
  print()
圖的鄰接表內容:
  1 ==>  [2]  [3] 
  2 ==>  [1]  [4]  [5] 
  3 ==>  [1]  [6]  [7] 
  4 ==>  [2]  [8] 
  5 ==>  [2]  [8] 
  6 ==>  [3]  [8] 
  7 ==>  [3]  [8] 
  8 ==>  [4]  [5]  [6]  [7] 
廣度優先遍歷的頂點:
  [1]  [2]  [3]  [4]  [5]  [6]  [7]  [8] 

最小生成樹(Minimum Cost Spanning Tree,MST)

一個圖的生成樹(spanning tree)就是以最少的邊來連通圖中所有的頂點,且不造成回路(cycle)的樹形結構。
weighted graph / network

  • 貪婪法則(Greedy Rule)
    1. Prim 算法
      P氏法。對一個加權圖形 G=(V, E),設 V = {1,2,...,n},假設 U = {1},也就是說,U 和 V 是兩個頂點的集合。
      然后從 U-V 的差集所產生的集合中找出一個頂點 x,該頂點 x 能與 U 集合中的某點形成最小成本的邊,且不會造成回路。然后將頂點 x 加入 U 集合中,反復執行同樣的步驟,一直到 U 集合等於 V 集合( U=V )為止。
    2. Kruskal 算法
      K氏法。將各邊按權值大小從小到大排列,接着從權值最低的邊開始建立最小成本的生成樹,如果加入的邊會造成回路則舍棄不用,直到加入了 n-1 個邊為止。

Kruskal 算法

  VERTS = 6
  
  
  class Edge:
      def __init__(self):
          self.start = 0
          self.to = 0
          self.find = 0
          self.val = 0
          self.next = None
  
  
  v = [0] * (VERTS + 1)
  
  
  def findmincost(head):
      minval = 100
      ptr = head
      while ptr != None:
          if ptr.val < minval and ptr.find == 0:
              minval = ptr.val
              retptr = ptr
          ptr = ptr.next
      retptr.find = 1
      return retptr
  
  
  def mintree(head):
      global VERTS
      result = 0
      ptr = head
      for i in range(VERTS):
          v[i] = 0
      while ptr != None:
          mceptr = findmincost(head)
          v[mceptr.start] = v[mceptr.start] + 1
          v[mceptr.to] = v[mceptr.to] + 1
          if v[mceptr.start] > 1 and v[mceptr.to] > 1:
              v[mceptr.start] -= 1
              v[mceptr.to] -= 1
              result = 1
          else:
              result = 0
          if result == 0:
              print("起始頂點 [%d] -> 終止頂點 [%d] -> 路徑長度 [%d]" % (mceptr.start, mceptr.to, mceptr.val))
          ptr = ptr.next
  
  
  data = [
      [1, 2, 6], [1, 6, 12], [1, 5, 10],
      [2, 3, 3], [2, 4, 5], [2, 6, 8],
      [3, 4, 7], [4, 6, 11], [4, 5, 9],
      [5, 6, 16]
  ]
  
  # 建立圖的鏈表
  head = None
  for i in range(10):
      for j in range(1, VERTS + 1):
          if data[i][0] == j:
              newnode = Edge()
              newnode.start = data[i][0]
              newnode.to = data[i][1]
              newnode.val = data[i][2]
              newnode.find = 0
              newnode.next = None
              if head == None:
                  head = newnode
                  head.next = None
                  ptr = head
              else:
                  ptr.next = newnode
                  ptr = ptr.next
  print('------------------------------------')
  print('建立最小成本生成樹:')
  print('------------------------------------')
  mintree(head)
  ----------------------------------------
  建立最小成本生成樹:
  ----------------------------------------
  起始頂點[2] -> 終止頂點[3] -> 路徑長度[3]
  起始頂點[2] -> 終止頂點[4] -> 路徑長度[5]
  起始頂點[1] -> 終止頂點[2] -> 路徑長度[6]
  起始頂點[2] -> 終止頂點[6] -> 路徑長度[8]
  起始頂點[4] -> 終止頂點[5] -> 路徑長度[9]

圖的最短路徑法

MST 計算連通網絡中的每一個頂點所需的最小花費,但是連通樹中任意兩頂點的路徑不一定是一條花費最少的路徑。

  • Dijkstra算法與A*算法

Dijkstra算法 —— 實際權重

  SIZE = 7
  NUMBER = 6
  INFINITE = 99999
  
  Graph_Matrix = [[0] * SIZE for row in range(SIZE)]  # 圖的數組
  distance = [0] * SIZE  # 路徑長度數組
  
  
  def build_graph_matrix(path_cost):
      for i in range(1, SIZE):
          for j in range(1, SIZE):
              if i == j:  # 自己到自己的距離
                  Graph_Matrix[i][j] = 0
              else:
                  Graph_Matrix[i][j] = INFINITE
      # 存入圖的邊
      i = 0
      while i < SIZE:
          start_point = path_cost[i][0]  # 起點
          end_point = path_cost[i][1]  # 終點
          Graph_Matrix[start_point][end_point] = path_cost[i][2]  # 權值
          i += 1
  
  
  # 單點對全部頂點的最短距離
  def shortest_path(vertex1, vertex_total):
      shortest_vertex = 1  # 記錄最短距離的頂點
      goal = [0] * SIZE  # 記錄該點是否被選取
      for i in range(1, vertex_total + 1):
          goal[i] = 0
          distance[i] = Graph_Matrix[vertex1][i]
      goal[vertex1] = 1
      distance[vertex1] = 0
  
      for i in range(1, vertex_total):
          shortest_distance = INFINITE
          for j in range(1, vertex_total + 1):
              if goal[j] == 0 and shortest_distance > distance[j]:
                  shortest_distance = distance[j]
                  shortest_vertex = j
  
          goal[shortest_vertex] = 1
          # 計算開始頂點到各頂點的最短距離
          for j in range(vertex_total + 1):
              if goal[j] == 0 and distance[shortest_vertex] + Graph_Matrix[shortest_vertex][j] < distance[j]:
                  distance[j] = distance[shortest_vertex] + Graph_Matrix[shortest_vertex][j]
  
  
  # global path_cost
  path_cost = [
      [1, 2, 29], [2, 3, 30], [2, 4, 35],
      [3, 5, 28], [3, 6, 87], [4, 5, 42],
      [4, 6, 75], [5, 6, 97],
  ]
  build_graph_matrix(path_cost)
  shortest_path(1, NUMBER)
  print('------------------------------')
  print('頂點 1 到各頂點最短距離的最終結果:')
  print('------------------------------')
  for i in range(1, SIZE):
      print('頂點 1 到頂點 %d 的最短距離為 %d ' % (i, distance[i]))
  ------------------------------
  頂點 1 到各頂點最短距離的最終結果:
  ------------------------------
  頂點 1 到頂點 1 的最短距離為 0 
  頂點 1 到頂點 2 的最短距離為 29 
  頂點 1 到頂點 3 的最短距離為 59 
  頂點 1 到頂點 4 的最短距離為 64 
  頂點 1 到頂點 5 的最短距離為 87 
  頂點 1 到頂點 6 的最短距離為 139 

效率不高,因為在尋找起點到各個頂點距離的過程中,無論哪一個點,都要實際去計算起點與各個頂點之間的距離,以便獲得最后的一個判斷:到底哪一個頂點距離與起點最近。也就是說該算法在帶有權重值的有向圖間的最短路徑的尋找方式,只是簡單的使用了廣度優先進行查找,完全忽略了許多有用的信息,這種查找算法會消耗很多系統資源,包括CPU的時間和內存空間。

A*算法 —— 實際權重+推測權重
結合了在路徑查找過程中從起點到各個頂點的“實際權重”及各個頂點預估到達終點的“推測權重”(heuristic cost)兩個因素,可以有效減少不必要的查找操作,提高效率。

  • 距離評估函數
    1. 曼哈頓距離(Manhattan distance):D = | x1 - x2 | + | y1 - y2 |
    2. 切比雪夫距離(Chebysev distance):D = max( | x1 - x2 |, | y1 - y2 | )
    3. 歐式幾何平面直線距離(Euclidean distance):D = ( ( x1 - x2)^2 + (y1 - y2 )^2 )^1/2
  • A* 算法主要步驟
    1. 首先確定各個頂點到終點的“推測權重”。“推測權重”可采取各個頂點和終點之間的直線距離(四舍五入后的值),直線距離的計算函數,可用上述其一。
    2. 分別計算從起點可抵達的各個頂點的權重,其計算方法是由起點到該頂點的“實際權重”,加上該頂點抵達終點的“推測權重”。計算完畢后,選出權重最小的點,並標示為查找完畢的點。
    3. 接着計算從查找完畢的頂點出發到各個頂點的權重,並在從其中選出一個權重最小的頂點,並再將其標示為查找完畢的頂點。以此類推。。。,反復同樣的計算過程,一直到抵達最后的終點。

A* 算法適用於可以事先獲得或預估各個頂點到終點距離的情況,萬一無法獲得各個頂點到達目的地終點的距離信息時,就無法使用A*算法。效率不是總優於 Dijkstra 算法,當“推測權重”的距離和實際兩個頂點間的距離相差很大時,A* 算法的查找效率可能比 Dijkstra 算法更差,甚至還會誤導方向,造成無法得到最短路徑的最終答案。
但是,如果推測權重所設置的距離和實際兩個頂點間的真實距離誤差不大時,A* 算法的查找效率遠大於 Dijkstra算法。
A* 算法常被用於游戲中角色追逐與快速突破關卡的設計。

Floyd算法
Dijkstra 算法只能求出某一點到其他頂點的距離,如果要求出圖中任意兩點甚至所有點間最短的距離,就必須使用 Floyd 算法。

  SIZE = 7
  NUMBER = 6
  INFINITE = 99999
  
  Graph_Matrix = [[0] * SIZE for row in range(SIZE)]  # 圖的數組
  distance = [[0] * SIZE for row in range(SIZE)]  # 路徑長度數組
  
  
  def build_graph_matrix(path_cost):
      for i in range(1, SIZE):
          for j in range(1, SIZE):
              if i == j:
                  Graph_Matrix[i][j] = 0
              else:
                  Graph_Matrix[i][j] = INFINITE
  
      # 圖的邊
      i = 0
      while i < SIZE:
          start_point = path_cost[i][0]
          end_point = path_cost[i][1]
          Graph_Matrix[start_point][end_point] = path_cost[i][2]
          i += 1
  
  
  def shortest_path(vertex_total):
      # 初始化圖的長度數組
      for i in range(1, vertex_total + 1):
          for j in range(i, vertex_total + 1):
              distance[i][j] = Graph_Matrix[i][j]
              distance[j][i] = Graph_Matrix[i][j]
  
      # 使用 Floyd 算法找出所有頂點兩兩之間的最短距離
      for k in range(1, vertex_total + 1):
          for i in range(1, vertex_total + 1):
              for j in range(1, vertex_total + 1):
                  if distance[i][k] + distance[k][j] < distance[i][j]:
                      distance[i][j] = distance[i][k] + distance[k][j]
  
  
  path_cost = [
      [1, 2, 20], [2, 3, 30], [2, 4, 25],
      [3, 5, 28], [4, 5, 32], [4, 6, 95],
      [5, 6, 67]
  ]
  
  build_graph_matrix(path_cost)
  print('=================================================')
  print('所有頂點兩兩之間的最短距離:')
  print('=================================================')
  shortest_path(NUMBER)
  print('        頂點1   頂點2  頂點3  頂點4   頂點5  頂點6')
  for i in range(1, NUMBER + 1):
      print('頂點 %d' % i, end='')
      for j in range(1, NUMBER + 1):
          print('%6d ' % distance[i][j], end='')
      print()
  =================================================
  所有頂點兩兩之間的最短距離:
  =================================================
          頂點1   頂點2  頂點3  頂點4   頂點5  頂點6
  頂點 1     0     20     50     45     77    140 
  頂點 2    20      0     30     25     57    120 
  頂點 3    50     30      0     55     28     95 
  頂點 4    45     25     55      0     32     95 
  頂點 5    77     57     28     32      0     67 
  頂點 6   140    120     95     95     67      0 

拓撲排序的步驟:

  1. 尋找圖中任何一個沒有先行者的起點
  2. 輸出此頂點,並將此頂點的所有邊刪除
  3. 重復上述步驟,處理所有頂點


免責聲明!

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



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