2026-rff_mp/VaravinVV/docs/data/task2/main.py

429 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import abc
import heapq
import time
from collections import deque
from dataclasses import dataclass
from typing import List, Optional, Dict, Set, Tuple, Any
class Cell:
#тут что такое клетка
def __init__(self, x: int, y: int, is_wall: bool = False,
is_exit: bool = False, is_start: bool = False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_exit = is_exit
self.is_start = is_start
def is_passable(self) -> bool: #можно ли пройти через клетку
return not self.is_wall
def __repr__(self) -> str:
return f"Cell({self.x},{self.y})"
class Maze:
def __init__(self, width: int, height: int): #что содержит лабиринт, начало конец и тд
self.width = width
self.height = height
self.grid: List[List[Cell]] = []
self.start_cell: Optional[Cell] = None
self.exit_cell: Optional[Cell] = None
def set_cell(self, x: int, y: int, cell: Cell) -> None: #ставим клетку куда надо или не ставим если в границы не попала
if not (0 <= x < self.width and 0 <= y < self.height):
raise IndexError("координаты вне границ лабиринта")
self.grid[y][x] = cell
def get_cell(self, x: int, y: int) -> Optional[Cell]: #тут уже из коррдинат клетку вытаскиваем
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[y][x]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]: #если соседняя клетка проходима - добавляем
neighbors = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder(abc.ABC):
"""Абстрактный строитель лабиринта."""
@abc.abstractmethod
def build_from_file(self, filename: str) -> Maze:
"""Построить лабиринт из файла."""
pass
class TextFileMazeBuilder(MazeBuilder):
"""Строитель лабиринта из текстового файла.
# - стена, пробел - проход, S - старт, E - выход."""
def build_from_file(self, filename: str) -> Maze:
lines = []
with open(filename, 'r', encoding='utf-8') as f:
for line in f:
line = line.rstrip('\n')
if line: #игнорируем пустые строки
lines.append(line)
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
# Инициализируем сетку пустыми клетками (по умолчанию стенами)
maze.grid = [[Cell(x, y, is_wall=True) for x in range(width)] for y in range(height)]
start_cell = None
exit_cell = None
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if x >= width:
continue
if ch == '#':
# стена (уже создана по умолчанию)
continue
elif ch == ' ':
# проход
cell = Cell(x, y, is_wall=False)
elif ch == 'S':
cell = Cell(x, y, is_wall=False, is_start=True)
start_cell = cell
elif ch == 'E':
cell = Cell(x, y, is_wall=False, is_exit=True)
exit_cell = cell
else:
# любой другой символ считаем проходом
cell = Cell(x, y, is_wall=False)
maze.set_cell(x, y, cell)
# Проверяем наличие старта и выхода
if start_cell is None:
raise ValueError("В лабиринте отсутствует стартовая клетка (S)")
if exit_cell is None:
raise ValueError("В лабиринте отсутствует выход (E)")
maze.start_cell = start_cell
maze.exit_cell = exit_cell
return maze
# ============================== Паттерн Strategy ==============================
class PathFindingStrategy(abc.ABC):
"""Интерфейс стратегии поиска пути."""
@abc.abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
"""
Возвращает (путь в виде списка клеток от start до exit_, количество посещённых клеток).
Если пути нет, возвращает ([], visited_count).
"""
pass
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину — гарантирует кратчайший путь."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
if start is exit_:
return [start], 1
queue = deque([start])
visited: Set[Cell] = {start}
parent: Dict[Cell, Optional[Cell]] = {start: None}
while queue:
current = queue.popleft()
if current is exit_:
# восстановление пути
path = []
cur = current
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path, len(visited)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
return [], len(visited)
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину — не гарантирует кратчайший путь, но быстр."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
if start is exit_:
return [start], 1
stack = [start]
visited: Set[Cell] = {start}
parent: Dict[Cell, Optional[Cell]] = {start: None}
while stack:
current = stack.pop()
if current is exit_:
path = []
cur = current
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path, len(visited)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
return [], len(visited)
class AStarStrategy(PathFindingStrategy):
"""Алгоритм A* с манхэттенской эвристикой."""
@staticmethod
def _heuristic(cell: Cell, target: Cell) -> int:
"""Манхэттенское расстояние."""
return abs(cell.x - target.x) + abs(cell.y - target.y)
def find_path(self, maze: Maze, start: Cell, exit_: Cell) -> Tuple[List[Cell], int]:
if start is exit_:
return [start], 1
# Приоритетная очередь: (f, counter, cell)
counter = 0
open_set = [(0, counter, start)]
g_score: Dict[Cell, int] = {start: 0}
f_score: Dict[Cell, int] = {start: self._heuristic(start, exit_)}
parent: Dict[Cell, Optional[Cell]] = {start: None}
closed_set: Set[Cell] = set()
visited_count = 0
while open_set:
_, _, current = heapq.heappop(open_set)
if current in closed_set:
continue
closed_set.add(current)
visited_count = len(closed_set) # количество обработанных клеток
if current is exit_:
path = []
cur = current
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path, visited_count
for neighbor in maze.get_neighbors(current):
if neighbor in closed_set:
continue
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
parent[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_)
f_score[neighbor] = f
counter += 1
heapq.heappush(open_set, (f, counter, neighbor))
return [], visited_count
# ============================== Статистика ==============================
@dataclass
class SearchStats:
"""Статистика поиска."""
time_ms: float # время выполнения в миллисекундах
visited_cells: int # количество посещённых клеток
path_length: int # длина найденного пути (0 если пути нет)
path_found: bool # найден ли путь
# ============================== Паттерн Observer ==============================
class Observer(abc.ABC):
"""Интерфейс наблюдателя."""
@abc.abstractmethod
def update(self, event_type: str, data: Any = None) -> None:
"""Получить уведомление от субъекта."""
pass
class Subject:
"""Субъект, за которым наблюдают."""
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer) -> None:
if observer not in self._observers:
self._observers.append(observer)
def detach(self, observer: Observer) -> None:
if observer in self._observers:
self._observers.remove(observer)
def notify(self, event_type: str, data: Any = None) -> None:
for observer in self._observers:
observer.update(event_type, data)
# ============================== MazeSolver (оркестратор) ==============================
class MazeSolver(Subject):
"""Решатель лабиринта, использующий стратегию поиска."""
def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
super().__init__()
self.maze = maze
self._strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy) -> None:
"""Сменить алгоритм поиска."""
self._strategy = strategy
def solve(self) -> Optional[SearchStats]:
"""Выполнить поиск пути с текущей стратегией.
Возвращает статистику или None, если стратегия не установлена."""
if self._strategy is None:
print("Стратегия не установлена.")
return None
self.notify("solving_start", {"strategy": self._strategy.__class__.__name__})
start_time = time.perf_counter()
path, visited = self._strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000.0
path_found = len(path) > 0
stats = SearchStats(
time_ms=time_ms,
visited_cells=visited,
path_length=len(path) if path_found else 0,
path_found=path_found
)
if path_found:
self.notify("path_found", {"path": path, "stats": stats})
else:
self.notify("no_path", {"stats": stats})
self.notify("solving_end", {"stats": stats})
return stats
class ConsoleView(Observer):
"""Отображает лабиринт и найденный путь в консоли."""
def __init__(self, maze: Maze):
self.maze = maze
self.last_path: List[Cell] = []
def update(self, event_type: str, data: Any = None) -> None:
if event_type == "path_found":
self.last_path = data["path"]
self.render()
elif event_type == "no_path":
self.last_path = []
self.render(no_path=True)
elif event_type == "solving_start":
print(f"\n== Поиск пути (алгоритм: {data['strategy']}) ==\n")
def render(self, no_path: bool = False) -> None:
"""Отрисовать лабиринт с текущим найденным путём."""
# Создаём множество клеток пути для быстрой проверки
path_set = set(self.last_path) if self.last_path else set()
for y in range(self.maze.height):
row = []
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if cell is None:
row.append('?')
continue
if cell is self.maze.start_cell:
row.append('S')
elif cell is self.maze.exit_cell:
row.append('E')
elif cell in path_set:
row.append('*')
elif cell.is_wall:
row.append('#')
else:
row.append(' ')
print(''.join(row))
if no_path:
print("\nПуть не найден!")
elif self.last_path:
print(f"\nНайден путь длиной {len(self.last_path)} клеток.")
else:
print("\nОжидание решения...")
def main():
import sys
if len(sys.argv) < 2:
print("для запуска: python main.py <имя_лабиринта>.txt")
return
filename = sys.argv[1]
# Строим лабиринт из файла (Builder)
builder = TextFileMazeBuilder()
try:
maze = builder.build_from_file(filename)
except Exception as e:
print(f"Ошибка загрузки лабиринта: {e}")
return
# Создаём решатель и прикрепляем наблюдателя
solver = MazeSolver(maze)
view = ConsoleView(maze)
solver.attach(view)
# Меню выбора стратегии
strategies = {
"1": BFSStrategy(),
"2": DFSStrategy(),
"3": AStarStrategy()
}
print("\nвыберите алгоритм поиска:")
print("1. BFS")
print("2. DFS")
print("3. A*")
choice = input("введите (1/2/3): ").strip()
strategy = strategies.get(choice)
if not strategy:
print("неверный выбор, по умолчанию используется BFS.")
strategy = BFSStrategy()
solver.set_strategy(strategy)
stats = solver.solve()
if stats:
print("\nстатистика по поиску пути в данном лабиринте")
print(f"выбранный алгоритм: {strategy.__class__.__name__}")
print(f"время выполнения: {stats.time_ms:.3f} мс")
print(f"посещено клеток: {stats.visited_cells}")
print(f"путь найден?: {'да' if stats.path_found else 'нет'}")
if stats.path_found:
print(f"длина пути: {stats.path_length}")
if __name__ == "__main__":
main()