2026-rff_mp/lomakinae/docs/02_report.md
2026-05-25 02:48:06 +03:00

350 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Отчёт. Задание 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 {
<<interface>>
+build_from_file(filename): Maze
}
class TextFileMazeBuilder {
+build_from_file(filename): Maze
}
class PathFindingStrategy {
<<interface>>
+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.