# Отчёт. Задание 2 - Поиск выхода из лабиринта ## Цель Разработать расширяемую программу для загрузки лабиринта из файла и поиска пути от старта до выхода с возможностью выбора алгоритма. Выполнить экспериментальное сравнение алгоритмов BFS, DFS и A\* на картах различной сложности. Применить минимум 3 паттерна проектирования GoF. ## 1. Описание задачи и выбранные паттерны Программный комплекс построен на принципах объектно-ориентированного программирования. Применены три паттерна проектирования GoF: 1. **Builder (Строитель)** - изолирует логику парсинга текстовых файлов и валидации структуры лабиринта (ровно один старт `S` и один выход `E`). Добавление нового формата (JSON, бинарный) требует только нового наследника `MazeBuilder`. 2. **Strategy (Стратегия)** - позволяет динамически менять алгоритм поиска пути (BFS, DFS, A\*) без модификации кода. Новый алгоритм добавляется наследованием от `PathFindingStrategy`. 3. **Facade (Фасад)** - предоставляет единую точку входа `MazeTestingFacade.run_full_diagnostic()` для последовательного запуска подсистемы бенчмарков и генерации графиков. ### Диаграмма классов (Mermaid) ```mermaid classDiagram class Maze { +int width +int height +List~List~Cell~~ cells +Cell start +Cell exit +get_cell(x, y): Cell +set_cell(x, y, cell_type) +get_neighbors(cell): List~Cell~ } class Cell { +int x +int y +bool is_wall +bool is_start +bool is_exit +is_passable(): bool } class MazeBuilder { <> +build_from_file(filename): Maze } class TextFileMazeBuilder { +build_from_file(filename): Maze } class PathFindingStrategy { <> +int visited_count +find_path(maze, start, exit_cell): List~Cell~ +reconstruct_path(came_from, exit_cell): List~Cell~ } class BFSStrategy class DFSStrategy class AStarStrategy class SearchStats { +float time_ms +int visited_cells +int path_length } class MazeSolver { +Maze maze +PathFindingStrategy strategy +set_strategy(strategy) +solve(): SearchStats } class MazeTestingFacade { +run_full_diagnostic() } MazeBuilder <|.. TextFileMazeBuilder MazeBuilder --> Maze : создает PathFindingStrategy <|.. BFSStrategy PathFindingStrategy <|.. DFSStrategy PathFindingStrategy <|.. AStarStrategy MazeSolver --> PathFindingStrategy : использует MazeSolver --> Maze : содержит Maze *-- Cell : содержит MazeTestingFacade --> MazeSolver : оркестрирует ``` ## 2. Листинги ключевых классов ### Builder - загрузка лабиринта из файла (`src/builder.py`) ```python class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename: str) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename: str) -> Maze: with open(filename, "r", encoding="utf-8") as f: lines = [line.rstrip("\n") for line in f.readlines()] height = len(lines) width = max(len(line) for line in lines) if height > 0 else 0 start_count = 0 exit_count = 0 maze = Maze(width, height) for y, line in enumerate(lines): for x, ch in enumerate(line): if ch == "#": maze.set_cell(x, y, "wall") elif ch == "S": maze.set_cell(x, y, "start") start_count += 1 elif ch == "E": maze.set_cell(x, y, "exit") exit_count += 1 else: maze.set_cell(x, y, "path") if start_count != 1 or exit_count != 1: raise ValueError(f"S={start_count}, E={exit_count}") return maze ``` ### Strategy - алгоритмы поиска пути (`src/strategies.py`) ```python class PathFindingStrategy(ABC): def __init__(self): self.visited_count = 0 @abstractmethod def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: pass def reconstruct_path(self, came_from: dict, exit_cell: Cell) -> List[Cell]: path = [] current = exit_cell while current is not None: path.append(current) current = came_from.get(current) path.reverse() return path class BFSStrategy(PathFindingStrategy): def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: queue = deque([start]) came_from = {start: None} visited = {start} while queue: current = queue.popleft() if current == exit_cell: self.visited_count = len(visited) return self.reconstruct_path(came_from, exit_cell) for neighbor in maze.get_neighbors(current): if neighbor not in visited: visited.add(neighbor) came_from[neighbor] = current queue.append(neighbor) self.visited_count = len(visited) return [] class AStarStrategy(PathFindingStrategy): def heuristic(self, cell: Cell, exit_cell: Cell) -> int: return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y) def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: heap = [] counter = 0 start_f = self.heuristic(start, exit_cell) heapq.heappush(heap, (start_f, counter, start)) counter += 1 came_from = {} g_score = {start: 0} f_score = {start: start_f} visited = set() while heap: current_f, _, current = heapq.heappop(heap) visited.add(current) if current == exit_cell: self.visited_count = len(visited) return self.reconstruct_path(came_from, exit_cell) if current_f > f_score.get(current, float("inf")): continue for neighbor in maze.get_neighbors(current): tentative_g = g_score[current] + 1 if tentative_g < g_score.get(neighbor, float("inf")): came_from[neighbor] = current g_score[neighbor] = tentative_g new_f = tentative_g + self.heuristic(neighbor, exit_cell) f_score[neighbor] = new_f heapq.heappush(heap, (new_f, counter, neighbor)) counter += 1 self.visited_count = len(visited) return [] ``` ### Оркестратор (`src/solver.py`) ```python class SearchStats(NamedTuple): time_ms: float visited_cells: int path_length: int class MazeSolver: def __init__(self, maze: Maze): self.maze = maze self.strategy = None def set_strategy(self, strategy: PathFindingStrategy): self.strategy = strategy def solve(self) -> SearchStats: if self.strategy is None: raise ValueError("Strategy not set") start_time = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) end_time = time.perf_counter() time_ms = (end_time - start_time) * 1000 return SearchStats( time_ms=time_ms, visited_cells=self.strategy.visited_count, path_length=len(path), ) ``` ### Facade - точка входа (`src/facade.py` и `main.py`) ```python # src/facade.py class MazeTestingFacade: def run_full_diagnostic(self): run_benchmarks() generate_plots() # main.py from src.facade import MazeTestingFacade def main(): facade = MazeTestingFacade() facade.run_full_diagnostic() ``` ## 3. Результаты экспериментов ### Параметры эксперимента | Параметр | Значение | | --------- | -------------------------------------------------------------- | | Итераций | 10 запусков на каждый лабиринт | | Лабиринты | maze_no_exit, maze_empty, maze_10x10, maze_50x50, maze_100x100 | | Алгоритмы | BFS (ширина), DFS (глубина), A\* (манхэттенская эвристика) | | Метрики | Время выполнения (мс), посещенные клетки, длина пути | Тесты проводились на 5 разных лабиринтах. Так как основные лабиринты (10x10, 50x50, 100x100) имеют только один верный путь, итоговая длина маршрута у всех алгоритмов совпадает. Сравниваем время работы и количество проверенных клеток. **1. Маленький лабиринт (10x10)** Все алгоритмы отработали быстрее 0.05 мс. Алгоритм A\* оказался наиболее точным, так как посетил всего 18 клеток. BFS проверил 33 клетки, а DFS проверил 32 клетки. **2. Средний лабиринт (50x50)** Здесь BFS обошел больше всего пространства (1046 клеток) и работал дольше всех (1.31 мс). В данном тесте DFS удачно выбрал направление и проверил меньше клеток (231 клетка за 0.25 мс). Алгоритм A\* показал стабильный результат, посетив 414 клеток за 1.05 мс. **3. Большой лабиринт (100x100)** На большой карте заметно преимущество A\*. Он нашел выход за 0.98 мс, посетив всего 380 клеток. BFS пришлось проверить почти весь лабиринт (2319 клеток), на что ушло 3.15 мс. DFS справился за 0.77 мс и проверил 662 клетки. **4. Пустой лабиринт (без стен)** В пустом пространстве все алгоритмы посетили одинаковое количество клеток (90 шт.). Однако DFS повел себя неоптимально и построил длинный зигзагообразный маршрут в 54 шага. BFS и A\* нашли прямой и кратчайший путь всего за 18 шагов. **5. Лабиринт без выхода** Все алгоритмы корректно обработали тупик и завершили работу без ошибок. Так как выхода нет, им пришлось проверить абсолютно все доступные клетки лабиринта (по 16 клеток у каждого). Длина пути у всех составила 0. Дольше всех из-за расчета эвристики работал A\* (0.036 мс), а BFS и DFS справились за 0.02 мс. ### Графики ![plot](data/02/benchmark_charts.png) --- ## 4. Анализ эффективности алгоритмов и применимость паттернов ### Масштабирование по размеру лабиринта 1. **A\*** - самый эффективный на больших картах. Благодаря Манхэттенской эвристике он учитывает направление к выходу и минимизирует лишние шаги. На лабиринте 100х100 он проверил всего 380 клеток (против 2319 у BFS). Минус - тратит чуть больше времени на мелких картах из-за математических расчетов. 2. **BFS (Поиск в ширину)** - выполняет избыточное исследование графа во все стороны. Из-за этого на карте 100х100 он посетил 2319 клеток, что увеличило время выполнения до 3.15 мс (худший результат по скорости). 3. **DFS (Поиск в глубину)** - работает на простом стеке, поэтому у него минимальные накладные расходы по времени (всего 0.77 мс на большой карте). Однако он не ищет оптимальный путь: на пустом лабиринте `maze_empty` вместо прямой линии в 18 шагов он построил ломаный зигзаг в 54 шага. 4. **Обработка тупиков (`maze_no_exit`)** - все алгоритмы успешно прошли стресс-тест. При отсутствии выхода они просто обходят 100% доступных клеток и корректно возвращают пустой путь. ### Применимость паттернов Паттерн **Strategy** позволил реализовать систему бенчмарков итерацией по массиву стратегий: `solver.set_strategy(strat)`. Без него пришлось бы использовать `if-elif` внутри решателя, что нарушило бы принцип открытости-закрытости (OCP). Добавление нового алгоритма (например, Дейкстры) не требует изменений в существующем коде. Паттерн **Builder** полностью инкапсулировал работу с файловой системой. Переход на другой формат хранения (JSON, бинарный) требует только создания нового наследника `MazeBuilder` без изменения остальной системы. Паттерн **Facade** скрыл последовательность вызовов `run_benchmarks()` и `generate_plots()` за единственным методом `run_full_diagnostic()`. Код `main.py` сведен к двум строкам и не зависит от деталей оркестрации подсистем. --- ## 5. Выводы Для реальных задач: - Экспериментально подтверждено, что поиск ($A^*$) с использованием Манхэттенской эвристики превосходит слепые методы (BFS, DFS) на больших лабиринтах. - Поиск в глубину непригоден для пустых пространств (строит крайне неоптимальные зигзагообразные пути) и сильно зависит от порядка обхода соседей, но не требует хранения большого объёма данных в памяти. - Тестирование на изолированном лабиринте ("без выхода") доказало отказоустойчивость реализованных стратегий - алгоритмы корректно завершают работу после полного исчерпания пространства состояний. Применение паттернов GoF (Builder, Strategy, Facade) обеспечило модульность и расширяемость системы. Добавление нового алгоритма, формата файлов или этапа диагностики не затрагивает существующий код, что соответствует принципам SOLID.