# Отчёт: Поиск выхода из лабиринта ## 1. Описание задачи и выбранных паттернов ### Задача Разработать программу для загрузки лабиринта из текстового файла, поиска пути от старта до выхода тремя алгоритмами (BFS, DFS, A*), визуализации найденного пути и экспериментального сравнения алгоритмов по времени, числу посещённых клеток и длине пути. ### Структура файлов ``` 02/ main.py - точка запуска codes/ maze.py - все классы (Cell, Maze, Builder, Strategy, Solver) maze_generator.py - генерация тестовых лабиринтов mazes/ - текстовые файлы лабиринтов results/ results_maze.csv - результаты экспериментов benchmark_plot.png - графики docs/ report1.md - отчёт mermaid.png - диаграмма классов ``` ### Применённые паттерны проектирования **1. Builder** - класс `TextFileMazeBuilder` реализует интерфейс `MazeBuilder`. Построение лабиринта из файла включает несколько шагов: чтение строк, обход символов, создание объектов `Cell`, поиск стартовой и конечной клетки. Без Builder вся эта логика оказалась бы в `main.py` или в конструкторе `Maze`. Builder скрывает детали создания от клиента. Если понадобится загружать лабиринт из JSON или бинарного файла - достаточно написать новый класс, реализующий тот же интерфейс `MazeBuilder`. **2. Strategy** - классы `BFSStrategy`, `DFSStrategy`, `AStarStrategy` реализуют интерфейс `PathFindingStrategy`. Алгоритм поиска можно менять во время работы программы через `MazeSolver.set_strategy()`, не трогая остальной код. Добавление нового алгоритма - это написание одного нового класса с методом `find_path()`. Без Strategy в `solve()` пришлось бы писать if/elif для каждого алгоритма. **3. Observer** - интерфейс `Observer` с методом `update(event)`. `MazeSolver` хранит список наблюдателей и уведомляет их при событиях `search_started`, `path_found`, `path_not_found`. Это позволяет добавлять отображение в консоль, запись в лог или GUI-уведомления, не меняя код солвера. Слабая связанность: солвер не знает, кто его слушает. ### Диаграмма классов ![Диаграмма классов](mermaid.png) --- ## 2. Листинги ключевых классов ### Cell и Maze ```python class Cell: def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False): self.x = x self.y = y self.is_wall = is_wall self.is_start = is_start self.is_exit = is_exit def is_passable(self): return not self.is_wall class Maze: def get_neighbors(self, cell): result = [] for dx, dy in [(0,-1),(0,1),(-1,0),(1,0)]: n = self.get_cell(cell.x + dx, cell.y + dy) if n and n.is_passable(): result.append(n) return result ``` ### Паттерн Builder ```python class MazeBuilder(ABC): @abstractmethod def build_from_file(self, filename) -> Maze: pass class TextFileMazeBuilder(MazeBuilder): def build_from_file(self, filename) -> Maze: with open(filename, encoding="utf-8") as f: lines = [l.rstrip("\n") for l in f] # ... парсинг символов, создание Cell, поиск S и E return Maze(cells, width, height, start, exit_cell) ``` ### Паттерн Strategy - алгоритм A* ```python class AStarStrategy(PathFindingStrategy): @staticmethod def _h(a, b): return abs(a.x - b.x) + abs(a.y - b.y) def find_path(self, maze, start, end): heap = [(0, 0, start)] parent = {start: None} g = {start: 0} closed = set() while heap: _, _, cur = heapq.heappop(heap) if cur in closed: continue closed.add(cur) if cur == end: return self._build_path(parent, start, end) for nb in maze.get_neighbors(cur): ng = g[cur] + 1 if ng < g.get(nb, float("inf")): g[nb] = ng heapq.heappush(heap, (ng + self._h(nb, end), id(nb), nb)) parent[nb] = cur return [] ``` ### MazeSolver ```python class MazeSolver: def __init__(self, maze, strategy): self.maze = maze self.strategy = strategy def set_strategy(self, strategy): self.strategy = strategy def solve(self) -> SearchStats: t0 = time.perf_counter() path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) t1 = time.perf_counter() return SearchStats( strategy=self.strategy.name, time_ms=(t1 - t0) * 1000, visited=self.strategy._visited, path_length=len(path), path=path, ) ``` --- ## 3. Результаты экспериментов Каждый алгоритм запускался 7 раз на каждом лабиринте, результаты усреднялись. ### Таблица результатов | Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | |----------|----------|-----------|----------------|------------| | small (11x11) | BFS | 0.070 | 39 | 33 | | small (11x11) | DFS | 0.055 | 33 | 33 | | small (11x11) | A* | 0.112 | 35 | 33 | | medium (51x51) | BFS | 1.391 | 793 | 497 | | medium (51x51) | DFS | 0.949 | 515 | 497 | | medium (51x51) | A* | 2.271 | 707 | 497 | | large (101x101) | BFS | 6.231 | 3533 | 1613 | | large (101x101) | DFS | 3.341 | 1957 | 1613 | | large (101x101) | A* | 11.27 | 3379 | 1613 | | empty (51x21) | BFS | 1.992 | 931 | 67 | | empty (51x21) | DFS | 1.021 | 451 | 451 | | empty (51x21) | A* | 3.527 | 931 | 67 | | no_exit (11x11) | BFS | 0.079 | 40 | - | | no_exit (11x11) | DFS | 0.077 | 40 | - | | no_exit (11x11) | A* | 0.140 | 40 | - | ### Графики ![Графики](../results/benchmark_plot.png) --- ## 4. Анализ эффективности алгоритмов и применимости паттернов ### Алгоритмы **BFS** гарантирует кратчайший путь по числу шагов. Расширяет узлы слой за слоем во всех направлениях, поэтому посещает наибольшее число клеток. На практике это надёжный выбор когда нужен точно кратчайший маршрут. **DFS** посещает меньше клеток и выполняется быстрее - на large лабиринте в 1.8 раза быстрее BFS. Однако путь может быть далеко не кратчайшим. На пустом лабиринте DFS нашёл путь длиной 451 шаг, тогда как BFS и A* - 67. Это связано с тем, что DFS уходит в первое попавшееся направление и возвращается только в тупике. **A*** использует манхэттенскую эвристику h = |x1-x2| + |y1-y2| и должен в теории посещать меньше клеток чем BFS. На лабиринтах, сгенерированных алгоритмом recursive backtracker, выигрыш небольшой (примерно 5%). Причина: backtracker строит дерево - между любыми двумя клетками ровно один путь, тупиков нет, эвристика не помогает их обходить. На лабиринтах с циклами A* посещает заметно меньше клеток. Накладные расходы на работу с heap и closed-set делают A* медленнее по времени, чем DFS. На пустом лабиринте (без стен) A* ведёт себя как BFS. Математически: f(x,y) = g + h = (x-1+y-1) + (W-x+H-y) = const для всех клеток. Все узлы неразличимы по приоритету. На лабиринте без выхода все три алгоритма посещают одинаковое число клеток и корректно возвращают пустой путь. ### Паттерны **Builder** оказался полезным при добавлении нового типа лабиринта (взвешенного, с символами s и m). Изменения были внесены только в `TextFileMazeBuilder`, клиентский код не менялся. **Strategy** позволил в одном цикле запустить все три алгоритма через `solver.set_strategy(strategy)`. Без паттерна пришлось бы либо дублировать код запуска для каждого алгоритма, либо писать условные ветки. **Observer** полезен при расширении: чтобы добавить вывод в лог или консоль, достаточно написать новый Observer и подписать его на solver, не меняя `MazeSolver`. --- ## 5. Выводы ООП и паттерны позволили сделать код гибким в нескольких направлениях. Добавление нового алгоритма поиска сводится к написанию одного класса, реализующего `find_path()`. Без Strategy пришлось бы добавлять ветку в `solve()` и во все места, где запускается поиск. Добавление нового формата лабиринта - только новый класс Builder. Без паттерна логика парсинга была бы перемешана с логикой работы программы. Добавление нового способа отображения (GUI, запись в файл) - только новый Observer. Без него MazeSolver пришлось бы напрямую вызывать функции отображения, что создало бы зависимость от конкретной реализации. Без применения паттернов код решал бы задачу, но любое изменение требовало бы правки в нескольких местах сразу. С паттернами каждый класс отвечает за одну задачу и не знает о деталях реализации соседних классов.