180 lines
5.7 KiB
Python
180 lines
5.7 KiB
Python
"""
|
||
maze_solver/strategies.py - паттерн Strategy.
|
||
|
||
Каждая стратегия реализует один и тот же интерфейс PathFindingStrategy
|
||
с методом find_path(maze, start, exit_), возвращающим:
|
||
{'path': [Cell, ...], 'visited': int}
|
||
|
||
Стратегии не модифицируют сам лабиринт.
|
||
"""
|
||
|
||
from abc import ABC, abstractmethod
|
||
from collections import deque
|
||
import heapq
|
||
|
||
|
||
# ---------- интерфейс стратегии ----------
|
||
|
||
class PathFindingStrategy(ABC):
|
||
name = "Strategy"
|
||
|
||
@abstractmethod
|
||
def find_path(self, maze, start, exit_):
|
||
"""Возвращает dict с ключами 'path' (list[Cell]) и 'visited' (int).
|
||
Если пути нет - path = []."""
|
||
|
||
|
||
# ---------- общая утилита: восстановление пути ----------
|
||
|
||
def _reconstruct(parents, start, end):
|
||
"""Восстанавливает путь по словарю предшественников {(x,y): Cell|None}."""
|
||
path = []
|
||
cur = end
|
||
while cur is not None:
|
||
path.append(cur)
|
||
cur = parents.get((cur.x, cur.y))
|
||
path.reverse()
|
||
if path and path[0] is start:
|
||
return path
|
||
return []
|
||
|
||
|
||
# ---------- BFS ----------
|
||
|
||
class BFSStrategy(PathFindingStrategy):
|
||
"""Поиск в ширину. Гарантирует кратчайший путь по числу шагов
|
||
(когда веса всех клеток равны)."""
|
||
name = "BFS"
|
||
|
||
def find_path(self, maze, start, exit_):
|
||
queue = deque([start])
|
||
parents = {(start.x, start.y): None}
|
||
visited = 1
|
||
|
||
while queue:
|
||
cell = queue.popleft()
|
||
if cell is exit_:
|
||
return {"path": _reconstruct(parents, start, exit_),
|
||
"visited": visited}
|
||
for nb in maze.get_neighbors(cell):
|
||
key = (nb.x, nb.y)
|
||
if key not in parents:
|
||
parents[key] = cell
|
||
visited += 1
|
||
queue.append(nb)
|
||
return {"path": [], "visited": visited}
|
||
|
||
|
||
# ---------- DFS ----------
|
||
|
||
class DFSStrategy(PathFindingStrategy):
|
||
"""Поиск в глубину. Не гарантирует кратчайший путь, но прост и быстр."""
|
||
name = "DFS"
|
||
|
||
def find_path(self, maze, start, exit_):
|
||
stack = [start]
|
||
parents = {(start.x, start.y): None}
|
||
visited = 1
|
||
|
||
while stack:
|
||
cell = stack.pop()
|
||
if cell is exit_:
|
||
return {"path": _reconstruct(parents, start, exit_),
|
||
"visited": visited}
|
||
for nb in maze.get_neighbors(cell):
|
||
key = (nb.x, nb.y)
|
||
if key not in parents:
|
||
parents[key] = cell
|
||
visited += 1
|
||
stack.append(nb)
|
||
return {"path": [], "visited": visited}
|
||
|
||
|
||
# ---------- A* ----------
|
||
|
||
def _manhattan(a, b):
|
||
return abs(a.x - b.x) + abs(a.y - b.y)
|
||
|
||
|
||
class AStarStrategy(PathFindingStrategy):
|
||
"""A*-поиск с манхэттенской эвристикой. Учитывает вес клеток (weight)."""
|
||
name = "A*"
|
||
|
||
def find_path(self, maze, start, exit_):
|
||
# f = g + h; в куче храним (f, tie, cell)
|
||
g_score = {(start.x, start.y): 0}
|
||
parents = {(start.x, start.y): None}
|
||
tie = 0
|
||
heap = [(_manhattan(start, exit_), tie, start)]
|
||
visited = 0
|
||
closed = set()
|
||
|
||
while heap:
|
||
f, _, cell = heapq.heappop(heap)
|
||
key = (cell.x, cell.y)
|
||
if key in closed:
|
||
continue
|
||
closed.add(key)
|
||
visited += 1
|
||
|
||
if cell is exit_:
|
||
return {"path": _reconstruct(parents, start, exit_),
|
||
"visited": visited}
|
||
|
||
for nb in maze.get_neighbors(cell):
|
||
nb_key = (nb.x, nb.y)
|
||
tentative_g = g_score[key] + nb.weight
|
||
if tentative_g < g_score.get(nb_key, float("inf")):
|
||
g_score[nb_key] = tentative_g
|
||
parents[nb_key] = cell
|
||
tie += 1
|
||
heapq.heappush(heap,
|
||
(tentative_g + _manhattan(nb, exit_), tie, nb))
|
||
return {"path": [], "visited": visited}
|
||
|
||
|
||
# ---------- Дейкстра ----------
|
||
|
||
class DijkstraStrategy(PathFindingStrategy):
|
||
"""Дейкстра - оптимальный путь с учётом веса клеток.
|
||
На немодифицированном лабиринте (все веса = 1) совпадает с BFS."""
|
||
name = "Dijkstra"
|
||
|
||
def find_path(self, maze, start, exit_):
|
||
dist = {(start.x, start.y): 0}
|
||
parents = {(start.x, start.y): None}
|
||
tie = 0
|
||
heap = [(0, tie, start)]
|
||
visited = 0
|
||
closed = set()
|
||
|
||
while heap:
|
||
d, _, cell = heapq.heappop(heap)
|
||
key = (cell.x, cell.y)
|
||
if key in closed:
|
||
continue
|
||
closed.add(key)
|
||
visited += 1
|
||
|
||
if cell is exit_:
|
||
return {"path": _reconstruct(parents, start, exit_),
|
||
"visited": visited}
|
||
|
||
for nb in maze.get_neighbors(cell):
|
||
nb_key = (nb.x, nb.y)
|
||
nd = d + nb.weight
|
||
if nd < dist.get(nb_key, float("inf")):
|
||
dist[nb_key] = nd
|
||
parents[nb_key] = cell
|
||
tie += 1
|
||
heapq.heappush(heap, (nd, tie, nb))
|
||
return {"path": [], "visited": visited}
|
||
|
||
|
||
STRATEGIES = {
|
||
"BFS": BFSStrategy,
|
||
"DFS": DFSStrategy,
|
||
"A*": AStarStrategy,
|
||
"Dijkstra": DijkstraStrategy,
|
||
}
|