From 24f11880e1f5340dd946787078dc177c965aab23 Mon Sep 17 00:00:00 2001 From: SobolevNS Date: Fri, 22 May 2026 13:48:52 +0300 Subject: [PATCH] add maze report --- SobolevNS/docs/report_02.md | 364 ++++++++++++++++++++++++++++++++++++ 1 file changed, 364 insertions(+) create mode 100644 SobolevNS/docs/report_02.md diff --git a/SobolevNS/docs/report_02.md b/SobolevNS/docs/report_02.md new file mode 100644 index 0000000..9fd4f53 --- /dev/null +++ b/SobolevNS/docs/report_02.md @@ -0,0 +1,364 @@ +# Отчёт по заданию 2. Поиск выхода из лабиринта с применением паттернов проектирования + +## 1. Постановка задачи + +Реализовать гибкую программу для загрузки лабиринта из файла, поиска пути от +старта до выхода с возможностью выбора алгоритма, текстовой визуализации +и экспериментального сравнения алгоритмов. В работе нужно применить +**не менее трёх паттернов GoF**, обосновать их выбор и продемонстрировать +преимущества такой архитектуры. + +В проекте применено **четыре паттерна**: **Builder**, **Strategy**, +**Observer** и **Command**. + +## 2. Диаграмма классов (упрощённая) + +```mermaid +classDiagram + class Cell { + +int x, y + +bool is_wall, is_start, is_exit + +int weight + +is_passable() bool + } + class Maze { + +int width, height + +Cell start, exit_ + +get_cell(x,y) Cell + +get_neighbors(cell) List~Cell~ + +render_text(path, player) str + } + + class MazeBuilder { + <> + +build_from_file(filename) Maze + } + class TextFileMazeBuilder { + +build_from_file(filename) Maze + } + + class PathFindingStrategy { + <> + +name : str + +find_path(maze, start, exit_) dict + } + class BFSStrategy + class DFSStrategy + class AStarStrategy + class DijkstraStrategy + + class MazeSolver { + -Maze maze + -PathFindingStrategy strategy + -List~Observer~ observers + +set_strategy(s) + +attach(o) + +solve() SearchStats + } + + class Observer { + <> + +update(event) + } + class ConsoleView + + class Command { + <> + +execute() + +undo() + } + class MoveCommand + class CommandHistory + class Player + + Maze "1" o-- "*" Cell + MazeBuilder <|-- TextFileMazeBuilder + TextFileMazeBuilder ..> Maze : creates + PathFindingStrategy <|-- BFSStrategy + PathFindingStrategy <|-- DFSStrategy + PathFindingStrategy <|-- AStarStrategy + PathFindingStrategy <|-- DijkstraStrategy + MazeSolver --> Maze + MazeSolver --> PathFindingStrategy + MazeSolver --> Observer + Observer <|-- ConsoleView + Command <|-- MoveCommand + CommandHistory o-- Command + MoveCommand --> Player + MoveCommand --> Maze +``` + +## 3. Паттерны и их обоснование + +### 3.1. Builder - `TextFileMazeBuilder` + +**Что делает.** Принимает имя файла, читает его, проверяет символы, ставит +координаты, создаёт `Cell`-объекты, находит `S` и `E`, валидирует +(ровно один старт и один выход) и возвращает готовый `Maze`. + +**Зачем нужен.** Конструирование лабиринта - это многошаговый процесс: +парсинг + валидация + расстановка флагов + поддержка взвешенных клеток +(`,` песок, `~` болото, `.` асфальт). Если положить всё это в конструктор +`Maze`, класс получится «толстым» и неудобным для расширения. + +**Что даёт.** Чтобы добавить новый формат (например, JSON или бинарный), +достаточно реализовать ещё один класс с тем же интерфейсом +`MazeBuilder.build_from_file`. Остальной код не меняется. + +### 3.2. Strategy - `PathFindingStrategy` + +**Что делает.** Объявляет единый интерфейс `find_path(maze, start, exit_)`. +Имеет четыре реализации: `BFSStrategy`, `DFSStrategy`, `AStarStrategy`, +`DijkstraStrategy`. Возвращают одинаковую структуру: +`{'path': [Cell, ...], 'visited': int}`. + +**Зачем нужен.** Все четыре алгоритма решают одну задачу, но с разными +компромиссами (скорость vs оптимальность vs учёт весов). Strategy позволяет +переключать их в рантайме одной строкой: + +```python +solver.set_strategy(AStarStrategy()) +``` + +без вмешательства в код решателя или модели лабиринта. + +**Что даёт.** Чтобы добавить, скажем, **двунаправленный BFS**, нужно лишь +написать новый класс - ни `MazeSolver`, ни `Maze` ничего не узнают +о нововведении. + +### 3.3. Observer - `MazeSolver` уведомляет `ConsoleView` + +**Что делает.** `MazeSolver` хранит список наблюдателей и шлёт им события: +`maze_loaded`, `search_start`, `search_end`, `path_found`, `no_path`. +`ConsoleView` подписывается и пишет в консоль. + +**Зачем нужен.** Решатель не должен знать, _кто_ и _как_ показывает +лабиринт пользователю. Можно подключить (или отключить) сразу несколько +наблюдателей - например, `ConsoleView` для отладки и `CSVLogger` +для эксперимента - не меняя `MazeSolver`. + +### 3.4. Command - `MoveCommand` с `undo` через `CommandHistory` + +**Что делает.** `MoveCommand` инкапсулирует один шаг игрока: сохраняет +предыдущую позицию, перемещает игрока в новое место. Метод `undo` +возвращает игрока обратно. `CommandHistory` ведёт стек выполненных команд +(общий undo). + +**Зачем нужен.** Ручное прохождение лабиринта = последовательность шагов, +каждый из которых должен быть откатываемым. Pattern Command даёт это +естественно и расширяемо: завтра можно добавить `MacroCommand` +(серия ходов) и `redo` - стек повторов. + +## 4. Этап 1-5: реализация + +### 4.1. Алгоритмы + +| Алгоритм | Структура данных | Учитывает веса? | Гарантирует кратчайший путь? | +| --- | --- | --- | --- | +| **BFS** | очередь (`deque`) | нет | да, по числу шагов | +| **DFS** | стек (`list`) | нет | нет | +| **A\*** | приоритетная очередь (`heapq`), эвристика - манхэттенское расстояние | **да** | да (если эвристика допустимая) | +| **Dijkstra** | приоритетная очередь | **да** | да | + +Все четыре пишут предшественников в словарь `parents`, и в конце путь +восстанавливается общей функцией `_reconstruct(...)`. + +### 4.2. Демонстрация (фрагмент вывода `demo.py`) + +``` +=== Builder: загружаем small_10x10.txt === +[ConsoleView] лабиринт 10x10 загружен +... +=== Strategy: пробуем все 4 алгоритма === +[ConsoleView] старт поиска: BFS +[ConsoleView] поиск окончен: путь=16, посещено=34, время=0.046 мс +--- BFS путь длиной 16 --- +########## +#S.......# +# ######.# +# #.# +###### #.# +# # #.# +# ## # #.# +# # #.# +# ##### .E +########## + +=== Command: пройдёмся вручную и сделаем undo === +стартовая позиция: (1,1) + move D: ok -> (2,1) + move D: ok -> (3,1) + move D: ok -> (4,1) + move D: ok -> (5,1) +Откатываем 2 хода (undo, undo): + теперь игрок в (3,1) +``` + +## 5. Этап 6. Экспериментальная часть + +### 5.1. Подготовка лабиринтов + +| Файл | Размер | Описание | +| --- | --- | --- | +| `small_10x10.txt` | 10×10 | ручной с простым путём | +| `medium_51x51.txt` | 51×51 | сгенерированный (DFS-карвер), тупики | +| `large_101x101.txt` | 101×101 | сгенерированный (DFS-карвер), запутанный | +| `empty_30x30.txt` | 30×30 | пустая комната - нет внутренних стен | +| `nopath_15x15.txt` | 15×15 | выход замурован - пути нет | +| `weighted_31x31.txt` | 31×31 | перфектный лабиринт + взвешенные клетки | +| `weighted_choice.txt` | 21×13 | **есть выбор** маршрута: через болото (короче) или вокруг (дешевле) | + +Все лабиринты генерирует `generate_mazes.py` (+ ручной `generate_weighted_choice.py`). +DFS-карвер реализован итеративно - для 101×101 рекурсивный вариант ловит +`RecursionError`. + +### 5.2. Замеры + +Для каждой пары (лабиринт × стратегия) запускали `solve()` **7 раз**, +усредняли время. Для пути и числа посещённых клеток между запусками +изменений нет (алгоритмы детерминированы) - фиксируем одно значение. + +Полные результаты - в `data/results.csv`. + +#### Сводная таблица (средние значения) + +| Лабиринт | Стратегия | t, мс | посещено | длина пути | стоимость | +| --- | --- | ---: | ---: | ---: | ---: | +| small_10x10 | BFS | 0.043 | 34 | 16 | 16 | +| | DFS | 0.015 | 18 | 16 | 16 | +| | A* | 0.043 | 27 | 16 | 16 | +| | Dijkstra | 0.044 | 33 | 16 | 16 | +| medium_51x51 | BFS | 0.50 | 524 | 353 | 353 | +| | DFS | 0.34 | 379 | 353 | 353 | +| | A* | 0.69 | 421 | 353 | 353 | +| | Dijkstra | 0.74 | 523 | 353 | 353 | +| large_101x101 | BFS | 2.08 | 2143 | 1265 | 1265 | +| | DFS | 1.35 | 1443 | 1265 | 1265 | +| | A* | 3.61 | 1831 | 1265 | 1265 | +| | Dijkstra | 3.36 | 2139 | 1265 | 1265 | +| empty_30x30 | BFS | 0.79 | 784 | **55** | 55 | +| | DFS | 0.47 | 784 | **379** | 379 | +| | A* | 1.53 | 784 | **55** | 55 | +| | Dijkstra | 1.34 | 784 | **55** | 55 | +| nopath_15x15 | все | ≈0.2 | 165 | 0 | 0 | +| weighted_31x31 | все | 0.3–0.7 | 318–433 | 265 | 391 | +| **weighted_choice** | BFS | 0.18 | 189 | **19** | **29** | +| | DFS | 0.03 | 55 | **19** | **29** | +| | A* | 0.26 | 117 | 25 | **25** | +| | Dijkstra | 0.36 | 209 | 25 | **25** | + +### 5.3. Графики + +![Время поиска](data/task2_maze/docs/data/plots/time_compare.png) + +![Сколько клеток посетил алгоритм](data/task2_maze/docs/data/plots/visited_compare.png) + +![Длина найденного пути](data/task2_maze/docs/data/plots/path_compare.png) + +![weighted_choice: длина vs стоимость](data/task2_maze/docs/data/plots/weighted_choice_compare.png) + +## 6. Анализ результатов + +### 6.1. На «обычных» перфектных лабиринтах путь единственный + +В лабиринтах, построенных DFS-карвером (`medium_51x51`, `large_101x101`, +`weighted_31x31`), между любыми двумя клетками существует **ровно один путь**. +Поэтому все четыре алгоритма находят его одинаковой длины (353, 1265, 265). +Различаются только время и **число посещённых клеток** - это и есть мера +«работы» алгоритма. + +* **DFS** - самый быстрый и обходит меньше всего клеток. Ему «везёт»: на + перфектном лабиринте он не возвращается, пока не упрётся в тупик. +* **BFS** - обходит чуть больше, потому что развивает фронт во всех направлениях. +* **A\*** и **Dijkstra** дороже по времени из-за `heapq`, но A\* экономит + посещения благодаря эвристике (на large_101x101: 1831 у A\* vs 2143 у BFS). + +### 6.2. На пустом лабиринте - главная разница между BFS/A\*/Dijkstra и DFS + +`empty_30x30` - это комната 28×28 проходимых клеток. Кратчайший путь между +противоположными углами - ровно 55 шагов. + +* BFS, A\*, Dijkstra находят его (длина = 55). +* **DFS находит путь длиной 379** - он петляет по краям комнаты, потому что + «жадно» идёт в первое попавшееся направление и никогда не возвращается, + пока не упрётся. + +Этот результат хорошо иллюстрирует: **DFS быстр, но даёт плохой путь +на открытых пространствах**. Если важна оптимальность - DFS не подходит. + +### 6.3. На взвешенном лабиринте с альтернативами - победа Dijkstra и A\* + +Лабиринт `weighted_choice` (21×13): открытая комната, в центре - болото 5×5 +(вес 3 за каждую клетку). Между стартом слева и выходом справа есть два +маршрута: +* «прямо через болото» - короче в клетках, но каждая болотная клетка стоит 3; +* «вокруг болота» - длиннее в клетках, но каждая стоит 1. + +Результаты: + +* BFS и DFS: путь **19 клеток**, **стоимость 29** (3 болотные × 3 = 9 «лишних» + единиц). +* A\* и Dijkstra: путь **25 клеток**, но **стоимость 25** - на 4 единицы + дешевле, потому что они учитывают вес клетки. + +Это и есть классическое преимущество взвешенных алгоритмов: +если шаги стоят по-разному (болото, песок, бездорожье), Dijkstra/A\* находят +оптимальный путь, а BFS/DFS - нет. + +### 6.4. На лабиринте без выхода + +`nopath_15x15`: все алгоритмы обходят все 165 проходимых клеток и возвращают +пустой путь. Время одинаковое - это, по сути, полный обход. Этот тест +показывает, что **все четыре стратегии корректно обрабатывают случай +отсутствия пути** (важная проверка). + +### 6.5. Время поиска ≠ качество пути + +Иерархия по скорости стабильна: **DFS < BFS < Dijkstra ≲ A\***. Но «быстрее» +не значит «лучше»: на `empty_30x30` DFS быстрее всех в 2 раза, но его путь +в 7 раз длиннее оптимального. На взвешенном лабиринте - BFS быстрее A\*, +но даёт более дорогой путь. + +**Вывод по алгоритмам:** + +| Когда подходит | Что выбрать | +| --- | --- | +| Минимум числа шагов на одинаковых клетках | **BFS** | +| Нужно быстро найти **хоть какой-то** путь | **DFS** | +| Взвешенный граф, есть хорошая эвристика | **A\*** (быстрее Dijkstra) | +| Взвешенный граф, эвристики нет | **Dijkstra** | + +## 7. Чем помогли паттерны + +Без паттернов код бы выглядел как один большой скрипт с `if maze_format == 'txt'` +и `if algorithm == 'bfs'`. Что я получил с паттернами: + +1. **Builder** - добавить новый формат лабиринта (JSON, графический PNG, генератор) + = новый класс, всё остальное не трогаем. +2. **Strategy** - добавить новый алгоритм (двунаправленный BFS, IDA\*) = новый + класс. `MazeSolver` не меняется. +3. **Observer** - `MazeSolver` ничего не знает про вывод. Я могу подключить + `ConsoleView` для интерактива и `CSVLogger` для эксперимента одновременно. + Эксперимент это и делает: подключает «тихого» наблюдателя. +4. **Command** - ручное прохождение и `undo` получаются естественно. + Расширить до `redo` - добавить второй стек. + +Что было бы сложно изменить без паттернов: + +* Сменить алгоритм поиска в рантайме без `if/elif`-простыни. +* Добавить второй вид визуализации (например, GUI) без затрагивания решателя. +* Поддержать сразу два формата лабиринта. + +## 8. Выводы + +* Реализованы четыре алгоритма поиска пути и четыре паттерна проектирования. +* Эксперимент подтвердил классические свойства алгоритмов: DFS быстрый, но + не оптимальный; BFS оптимален по шагам; A\*/Dijkstra оптимальны по + стоимости; A\* быстрее Dijkstra при наличии хорошей эвристики. +* Особенно выпукло разница видна на двух «диагностических» лабиринтах: + `empty_30x30` (DFS даёт «уродский» путь в 7 раз длиннее) и `weighted_choice` + (BFS/DFS режут через болото, Dijkstra/A\* обходят). +* Паттерны Builder/Strategy/Observer/Command превратили проект из «скрипта» + в расширяемое приложение. Новый формат, новый алгоритм или новый вид + визуализации добавляется без правки существующего кода + (принцип Open/Closed).