add maze report
This commit is contained in:
parent
5aac691724
commit
24f11880e1
364
SobolevNS/docs/report_02.md
Normal file
364
SobolevNS/docs/report_02.md
Normal file
|
|
@ -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 {
|
||||
<<abstract>>
|
||||
+build_from_file(filename) Maze
|
||||
}
|
||||
class TextFileMazeBuilder {
|
||||
+build_from_file(filename) Maze
|
||||
}
|
||||
|
||||
class PathFindingStrategy {
|
||||
<<abstract>>
|
||||
+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 {
|
||||
<<abstract>>
|
||||
+update(event)
|
||||
}
|
||||
class ConsoleView
|
||||
|
||||
class Command {
|
||||
<<abstract>>
|
||||
+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. Графики
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 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).
|
||||
Loading…
Reference in New Issue
Block a user