Lecture 6
This commit is contained in:
parent
50cf0b9b52
commit
82a6d35942
136
SoSe24/lec06_graph/astar.py
Normal file
136
SoSe24/lec06_graph/astar.py
Normal file
@ -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="")
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user