forked from UNN/2026-rff_mp
194 lines
12 KiB
Markdown
194 lines
12 KiB
Markdown
|
|
# Отчёт: Поиск выхода из лабиринта
|
|||
|
|
## 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-уведомления, не меняя код солвера. Слабая связанность: солвер не знает, кто его слушает.
|
|||
|
|
|
|||
|
|
### Диаграмма классов
|
|||
|
|

|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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 | - |
|
|||
|
|
|
|||
|
|
### Графики
|
|||
|
|

|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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 пришлось бы напрямую вызывать функции отображения, что создало бы зависимость от конкретной реализации.
|
|||
|
|
|
|||
|
|
Без применения паттернов код решал бы задачу, но любое изменение требовало бы правки в нескольких местах сразу. С паттернами каждый класс отвечает за одну задачу и не знает о деталях реализации соседних классов.
|