From 82a6d359422e1dfaaadc8c1068860b3b57814d1a Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Wed, 5 Jun 2024 16:16:33 +0200 Subject: [PATCH] Lecture 6 --- SoSe24/lec06_graph/astar.py | 136 ++++++++++++++++++++++++ SoSe24/lec06_graph/{bfs.py => graph.py} | 80 +++++++++----- 2 files changed, 191 insertions(+), 25 deletions(-) create mode 100644 SoSe24/lec06_graph/astar.py rename SoSe24/lec06_graph/{bfs.py => graph.py} (72%) diff --git a/SoSe24/lec06_graph/astar.py b/SoSe24/lec06_graph/astar.py new file mode 100644 index 0000000..d90a611 --- /dev/null +++ b/SoSe24/lec06_graph/astar.py @@ -0,0 +1,136 @@ +import math +from typing import Callable +import re +from graph import Graph, AdjacencyListGraph, AdjacencyMatrixGraph, NodeColor, Vertex + + +def a_star(self, start_name: str, end_name: str, heuristic: Callable[[Vertex], float]): + 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 + + def cost(vertex: Vertex) -> float: + """Compute the cost of the path to the given vertex.""" + if distance_map[vertex] is None: + return math.inf + return distance_map[vertex] + heuristic(vertex) + + # 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 = [start_node] + + # Process the queue + while len(queue) > 0: + queue.sort(key=cost) + vertex = queue.pop(0) + if vertex.value == end_name: + # Return the distance and predecessor maps + return distance_map, predecessor_map + for dest, weight in self.get_adjacent_vertices_with_weight(vertex.value): + if color_map[dest] == NodeColor.BLACK: + continue + f = distance_map[vertex] + weight + heuristic(dest) + if color_map[dest] == NodeColor.GRAY and f > cost(dest): + continue + predecessor_map[dest] = vertex + distance_map[dest] = distance_map[vertex] + weight + if color_map[dest] == NodeColor.WHITE: + queue.append(dest) + color_map[dest] = NodeColor.GRAY + color_map[vertex] = NodeColor.BLACK + + # Return the distance and predecessor maps if no path was found + return None, None + + +# Add the a_star method to the Graph classes +AdjacencyListGraph.a_star = a_star +AdjacencyMatrixGraph.a_star = a_star + + +if __name__ == "__main__": + + def read_labyrinth_into_graph(graph: Graph, filename: str): + """Read a labyrinth from a file into a graph. The file format is a grid of characters:""" + start = None + end = None + with open(filename, "r") as file: + nodes = [] + lines = file.readlines() + for y, line in enumerate(lines): + for x, char in enumerate(line): + if char in ' AS': + name = pos_to_nodename((x, y)) + graph.insert_vertex(name) + nodes.append((x, y)) + if char == 'A': + end = (x, y) + if char == 'S': + start = (x, y) + for x, y in nodes: + name1 = f"x{x}y{y}" + for neighbor in [(x - 1, y), (x, y - 1), (x, y + 1), (x + 1, y)]: + if neighbor in nodes: + name_neighbor = pos_to_nodename(neighbor) + graph.connect(name1, name_neighbor, 1) + return start, end, lines + + + def nodename_to_pos(nodename): + """Convert a node name to a position (x, y).""" + m = re.match(r"(^x(\d*)y(\d*))", nodename) + if m: + return (int(m.group(2)), int(m.group(3))) + return None + + + def pos_to_nodename(pos): + """Convert a position (x, y) to a node name.""" + x, y = pos + return f"x{x}y{y}" + + + def get_heuristic(end) -> Callable[[Vertex], float]: + """Return a heuristic function for the given end position.""" + def heuristic(v: Vertex) -> float: + x, y = nodename_to_pos(v.value) + x1, y1 = end + # Euclidean distance + return math.sqrt(abs(x - x1)**2 + abs(y - y1)**2) + + return heuristic + + def update_lines(lines, distance_map, path): + """Update the lines with the path found by the A* algorithm.""" + def replace_at(x, y, replacement): + if lines[y][x] not in " .": + return + lines[y] = lines[y][:x] + replacement + lines[y][x+1:] + + for node in distance_map.keys(): + if distance_map[node] is not None: + x, y = nodename_to_pos(node.value) + replace_at(x, y, ".") + for node in path: + x, y = nodename_to_pos(node) + replace_at(x, y, "*") + return lines + + graph = AdjacencyListGraph() + #graph = AdjacencyMatrixGraph() + start, end, lines = read_labyrinth_into_graph(graph, "../../labyrinth.txt") + distance_map, predecessor_map = graph.a_star(pos_to_nodename(start), pos_to_nodename(end), get_heuristic(end)) + endname = pos_to_nodename(end) + lines = update_lines(lines, distance_map, graph.path(endname, predecessor_map)) + for line in lines: + print(line, end="") \ No newline at end of file diff --git a/SoSe24/lec06_graph/bfs.py b/SoSe24/lec06_graph/graph.py similarity index 72% rename from SoSe24/lec06_graph/bfs.py rename to SoSe24/lec06_graph/graph.py index 90771c3..e652286 100644 --- a/SoSe24/lec06_graph/bfs.py +++ b/SoSe24/lec06_graph/graph.py @@ -16,12 +16,15 @@ class Vertex: 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): + def connect(self, name1: str, name2: str, weight: float = 1): raise NotImplementedError("Please implement this method in subclass") def all_vertices(self) -> List[Vertex]: @@ -33,6 +36,10 @@ class Graph: 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 bfs(self, start_name: str): """ Perform a breadth-first search starting at the given vertex. @@ -74,6 +81,7 @@ class Graph: # 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. @@ -100,10 +108,10 @@ class AdjacencyListGraph(Graph): if name not in self.adjacency_map: self.adjacency_map[name] = [] - def connect(self, name1: str, name2: str): + 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) + adjacency_list.append((dest, weight)) def all_vertices(self) -> List[Vertex]: return list(self.vertex_map.values()) @@ -112,8 +120,14 @@ class AdjacencyListGraph(Graph): 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] + + + class AdjacencyMatrixGraph(Graph): """A graph implemented as an adjacency matrix.""" def __init__(self): @@ -126,13 +140,13 @@ class AdjacencyMatrixGraph(Graph): 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(0) - self.adjacency_matrix.append([0] * len(self.vertex_list)) # add a new row + row.append(None) + self.adjacency_matrix.append([None] * len(self.vertex_list)) # add a new row - def connect(self, name1: str, name2: str): + 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] = 1 + self.adjacency_matrix[index1][index2] = weight def all_vertices(self) -> List[Vertex]: return self.vertex_list @@ -145,31 +159,46 @@ class AdjacencyMatrixGraph(Graph): index = self.index_map[name] result = [] for i in range(len(self.vertex_list)): - if self.adjacency_matrix[index][i] == 1: + if self.adjacency_matrix[index][i] is not None: name = self.vertex_list[i].value result.append(self.get_vertex(name)) return result -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) + 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 + + + + 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") @@ -179,3 +208,4 @@ if __name__ == "__main__": _, predecessor_map = graph.bfs('Schatzkammer') path = graph.path('Höhleneingang', predecessor_map) print(path) +