You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

graph.py 8.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. from collections import deque
  2. from typing import List
  3. import re
  4. from enum import Enum
  5. class NodeColor(Enum):
  6. """Enumeration for node colors in a graph traversal."""
  7. WHITE = 1 # WHITE: not visited
  8. GRAY = 2 # GRAY: visited but not all neighbors visited
  9. BLACK = 3 # BLACK: visited and all neighbors visited
  10. class Vertex:
  11. """A vertex in a graph."""
  12. def __init__(self, value):
  13. self.value = value
  14. def __repr__(self):
  15. return str(self.value)
  16. class Graph:
  17. """A graph."""
  18. def insert_vertex(self, name: str):
  19. raise NotImplementedError("Please implement this method in subclass")
  20. def connect(self, name1: str, name2: str, weight: float = 1):
  21. raise NotImplementedError("Please implement this method in subclass")
  22. def all_vertices(self) -> List[Vertex]:
  23. raise NotImplementedError("Please implement this method in subclass")
  24. def get_vertex(self, name: str) -> Vertex:
  25. raise NotImplementedError("Please implement this method in subclass")
  26. def get_adjacent_vertices(self, name: str) -> List[Vertex]:
  27. raise NotImplementedError("Please implement this method in subclass")
  28. def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]:
  29. raise NotImplementedError("Please implement this method in subclass")
  30. def all_edges(self) -> List[tuple[str, str, float]]:
  31. raise NotImplementedError("Please implement this method in subclass")
  32. def bfs(self, start_name: str):
  33. """
  34. Perform a breadth-first search starting at the given vertex.
  35. :param start_name: the name of the vertex to start at
  36. :return: a tuple of two dictionaries, the first mapping vertices to distances from the start vertex,
  37. the second mapping vertices to their predecessors in the traversal tree
  38. """
  39. color_map = {} # maps vertices to their color
  40. distance_map = {} # maps vertices to their distance from the start vertex
  41. predecessor_map = {} # maps vertices to their predecessor in the traversal tree
  42. # Initialize the maps
  43. for vertex in self.all_vertices():
  44. color_map[vertex] = NodeColor.WHITE
  45. distance_map[vertex] = None
  46. predecessor_map[vertex] = None
  47. # Start at the given vertex
  48. start_node = self.get_vertex(start_name)
  49. color_map[start_node] = NodeColor.GRAY
  50. distance_map[start_node] = 0
  51. # Initialize the queue with the start vertex
  52. queue = deque()
  53. queue.append(start_node)
  54. # Process the queue
  55. while len(queue) > 0:
  56. vertex = queue.popleft()
  57. for dest in self.get_adjacent_vertices(vertex.value):
  58. if color_map[dest] == NodeColor.WHITE:
  59. color_map[dest] = NodeColor.GRAY
  60. distance_map[dest] = distance_map[vertex] + 1
  61. predecessor_map[dest] = vertex
  62. queue.append(dest)
  63. color_map[vertex] = NodeColor.BLACK
  64. # Return the distance and predecessor maps
  65. return distance_map, predecessor_map
  66. def path(self, destination, map):
  67. """
  68. Compute the path from the start vertex to the given destination vertex.
  69. The map parameter is the predecessor map
  70. """
  71. path = []
  72. destination_node = self.get_vertex(destination)
  73. while destination_node is not None:
  74. path.insert(0, destination_node.value)
  75. destination_node = map[destination_node]
  76. return path
  77. class AdjacencyListGraph(Graph):
  78. """A graph implemented as an adjacency list."""
  79. def __init__(self):
  80. self.adjacency_map = {} # maps vertex names to lists of adjacent vertices
  81. self.vertex_map = {} # maps vertex names to vertices
  82. def insert_vertex(self, name: str):
  83. if name not in self.vertex_map:
  84. self.vertex_map[name] = Vertex(name)
  85. if name not in self.adjacency_map:
  86. self.adjacency_map[name] = []
  87. def connect(self, name1: str, name2: str, weight: float = 1):
  88. adjacency_list = self.adjacency_map[name1]
  89. dest = self.vertex_map[name2]
  90. adjacency_list.append((dest, weight))
  91. def all_vertices(self) -> List[Vertex]:
  92. return list(self.vertex_map.values())
  93. def get_vertex(self, name: str) -> Vertex:
  94. return self.vertex_map[name]
  95. def get_adjacent_vertices(self, name: str) -> List[Vertex]:
  96. return list(map(lambda x: x[0], self.adjacency_map[name]))
  97. def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]:
  98. return self.adjacency_map[name]
  99. def all_edges(self) -> List[tuple[str, str, float]]:
  100. result = []
  101. for name in self.adjacency_map:
  102. for (dest, weight) in self.adjacency_map[name]:
  103. result.append((name, dest.value, weight))
  104. return result
  105. class AdjacencyMatrixGraph(Graph):
  106. """A graph implemented as an adjacency matrix."""
  107. def __init__(self):
  108. self.index_map = {} # maps vertex names to indices
  109. self.vertex_list = [] # list of vertices
  110. self.adjacency_matrix = [] # adjacency matrix
  111. def insert_vertex(self, name: str):
  112. if name not in self.index_map:
  113. self.index_map[name] = len(self.vertex_list)
  114. self.vertex_list.append(Vertex(name))
  115. for row in self.adjacency_matrix: # add a new column to each row
  116. row.append(None)
  117. self.adjacency_matrix.append([None] * len(self.vertex_list)) # add a new row
  118. def connect(self, name1: str, name2: str, weight: float = 1):
  119. index1 = self.index_map[name1]
  120. index2 = self.index_map[name2]
  121. self.adjacency_matrix[index1][index2] = weight
  122. def all_vertices(self) -> List[Vertex]:
  123. return self.vertex_list
  124. def get_vertex(self, name: str) -> Vertex:
  125. index = self.index_map[name]
  126. return self.vertex_list[index]
  127. def get_adjacent_vertices(self, name: str) -> List[Vertex]:
  128. index = self.index_map[name]
  129. result = []
  130. for i in range(len(self.vertex_list)):
  131. if self.adjacency_matrix[index][i] is not None:
  132. name = self.vertex_list[i].value
  133. result.append(self.get_vertex(name))
  134. return result
  135. def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]:
  136. index = self.index_map[name]
  137. result = []
  138. for i in range(len(self.vertex_list)):
  139. if self.adjacency_matrix[index][i] is not None:
  140. name = self.vertex_list[i].value
  141. result.append((self.get_vertex(name), self.adjacency_matrix[index][i]))
  142. return result
  143. def all_edges(self) -> List[tuple[str, str, float]]:
  144. result = []
  145. for i in range(len(self.vertex_list)):
  146. for j in range(len(self.vertex_list)):
  147. if self.adjacency_matrix[i][j] is not None:
  148. result.append((self.vertex_list[i].value, self.vertex_list[j].value, self.adjacency_matrix[i][j]))
  149. return result
  150. if __name__ == "__main__":
  151. def read_cave_into_graph(graph: Graph, filename: str):
  152. """Read a cave description from a file and insert it into the given graph."""
  153. with open(filename, "r") as file:
  154. lines = file.readlines()
  155. for line in lines:
  156. # match a line with two node names and an optional direction
  157. m = re.match(r"(^\s*\"(.*)\"\s*([<>]*)\s*\"(.*)\"\s*)", line)
  158. if m:
  159. startnode = m.group(2)
  160. endnode = m.group(4)
  161. opcode = m.group(3)
  162. graph.insert_vertex(startnode)
  163. graph.insert_vertex(endnode)
  164. if '>' in opcode:
  165. graph.connect(startnode, endnode)
  166. if '<' in opcode:
  167. graph.connect(endnode, startnode)
  168. graph = AdjacencyListGraph()
  169. #graph = AdjacencyMatrixGraph()
  170. read_cave_into_graph(graph, "../../hoehle.txt")
  171. _, predecessor_map = graph.bfs('Höhleneingang')
  172. path = graph.path('Schatzkammer', predecessor_map)
  173. print(path)
  174. _, predecessor_map = graph.bfs('Schatzkammer')
  175. path = graph.path('Höhleneingang', predecessor_map)
  176. print(path)