from collections import deque from typing import List import re from enum import Enum class NodeColor(Enum): """Enumeration for node colors in a graph traversal.""" WHITE = 1 # WHITE: not visited GRAY = 2 # GRAY: visited but not all neighbors visited BLACK = 3 # BLACK: visited and all neighbors visited class Vertex: """A vertex in a graph.""" def __init__(self, value): self.value = value def __repr__(self): return str(self.value) class Graph: """A graph.""" def insert_vertex(self, name: str): raise NotImplementedError("Please implement this method in subclass") def connect(self, name1: str, name2: str, weight: float = 1): raise NotImplementedError("Please implement this method in subclass") def all_vertices(self) -> List[Vertex]: raise NotImplementedError("Please implement this method in subclass") def get_vertex(self, name: str) -> Vertex: raise NotImplementedError("Please implement this method in subclass") def get_adjacent_vertices(self, name: str) -> List[Vertex]: raise NotImplementedError("Please implement this method in subclass") def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]: raise NotImplementedError("Please implement this method in subclass") def all_edges(self) -> List[tuple[str, str, float]]: raise NotImplementedError("Please implement this method in subclass") def bfs(self, start_name: str): """ Perform a breadth-first search starting at the given vertex. :param start_name: the name of the vertex to start at :return: a tuple of two dictionaries, the first mapping vertices to distances from the start vertex, the second mapping vertices to their predecessors in the traversal tree """ color_map = {} # maps vertices to their color distance_map = {} # maps vertices to their distance from the start vertex predecessor_map = {} # maps vertices to their predecessor in the traversal tree # Initialize the maps for vertex in self.all_vertices(): color_map[vertex] = NodeColor.WHITE distance_map[vertex] = None predecessor_map[vertex] = None # Start at the given vertex start_node = self.get_vertex(start_name) color_map[start_node] = NodeColor.GRAY distance_map[start_node] = 0 # Initialize the queue with the start vertex queue = deque() queue.append(start_node) # Process the queue while len(queue) > 0: vertex = queue.popleft() for dest in self.get_adjacent_vertices(vertex.value): if color_map[dest] == NodeColor.WHITE: color_map[dest] = NodeColor.GRAY distance_map[dest] = distance_map[vertex] + 1 predecessor_map[dest] = vertex queue.append(dest) color_map[vertex] = NodeColor.BLACK # Return the distance and predecessor maps return distance_map, predecessor_map def path(self, destination, map): """ Compute the path from the start vertex to the given destination vertex. The map parameter is the predecessor map """ path = [] destination_node = self.get_vertex(destination) while destination_node is not None: path.insert(0, destination_node.value) destination_node = map[destination_node] return path class AdjacencyListGraph(Graph): """A graph implemented as an adjacency list.""" def __init__(self): self.adjacency_map = {} # maps vertex names to lists of adjacent vertices self.vertex_map = {} # maps vertex names to vertices def insert_vertex(self, name: str): if name not in self.vertex_map: self.vertex_map[name] = Vertex(name) if name not in self.adjacency_map: self.adjacency_map[name] = [] def connect(self, name1: str, name2: str, weight: float = 1): adjacency_list = self.adjacency_map[name1] dest = self.vertex_map[name2] adjacency_list.append((dest, weight)) def all_vertices(self) -> List[Vertex]: return list(self.vertex_map.values()) def get_vertex(self, name: str) -> Vertex: return self.vertex_map[name] def get_adjacent_vertices(self, name: str) -> List[Vertex]: return list(map(lambda x: x[0], self.adjacency_map[name])) def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]: return self.adjacency_map[name] def all_edges(self) -> List[tuple[str, str, float]]: result = [] for name in self.adjacency_map: for (dest, weight) in self.adjacency_map[name]: result.append((name, dest.value, weight)) return result class AdjacencyMatrixGraph(Graph): """A graph implemented as an adjacency matrix.""" def __init__(self): self.index_map = {} # maps vertex names to indices self.vertex_list = [] # list of vertices self.adjacency_matrix = [] # adjacency matrix def insert_vertex(self, name: str): if name not in self.index_map: self.index_map[name] = len(self.vertex_list) self.vertex_list.append(Vertex(name)) for row in self.adjacency_matrix: # add a new column to each row row.append(None) self.adjacency_matrix.append([None] * len(self.vertex_list)) # add a new row def connect(self, name1: str, name2: str, weight: float = 1): index1 = self.index_map[name1] index2 = self.index_map[name2] self.adjacency_matrix[index1][index2] = weight def all_vertices(self) -> List[Vertex]: return self.vertex_list def get_vertex(self, name: str) -> Vertex: index = self.index_map[name] return self.vertex_list[index] def get_adjacent_vertices(self, name: str) -> List[Vertex]: index = self.index_map[name] result = [] for i in range(len(self.vertex_list)): if self.adjacency_matrix[index][i] is not None: name = self.vertex_list[i].value result.append(self.get_vertex(name)) return result def get_adjacent_vertices_with_weight(self, name: str) -> List[tuple[Vertex, float]]: index = self.index_map[name] result = [] for i in range(len(self.vertex_list)): if self.adjacency_matrix[index][i] is not None: name = self.vertex_list[i].value result.append((self.get_vertex(name), self.adjacency_matrix[index][i])) return result def all_edges(self) -> List[tuple[str, str, float]]: result = [] for i in range(len(self.vertex_list)): for j in range(len(self.vertex_list)): if self.adjacency_matrix[i][j] is not None: result.append((self.vertex_list[i].value, self.vertex_list[j].value, self.adjacency_matrix[i][j])) return result if __name__ == "__main__": def read_cave_into_graph(graph: Graph, filename: str): """Read a cave description from a file and insert it into the given graph.""" with open(filename, "r") as file: lines = file.readlines() for line in lines: # match a line with two node names and an optional direction m = re.match(r"(^\s*\"(.*)\"\s*([<>]*)\s*\"(.*)\"\s*)", line) if m: startnode = m.group(2) endnode = m.group(4) opcode = m.group(3) graph.insert_vertex(startnode) graph.insert_vertex(endnode) if '>' in opcode: graph.connect(startnode, endnode) if '<' in opcode: graph.connect(endnode, startnode) graph = AdjacencyListGraph() #graph = AdjacencyMatrixGraph() read_cave_into_graph(graph, "../../hoehle.txt") _, predecessor_map = graph.bfs('Höhleneingang') path = graph.path('Schatzkammer', predecessor_map) print(path) _, predecessor_map = graph.bfs('Schatzkammer') path = graph.path('Höhleneingang', predecessor_map) print(path)