350 lines
16 KiB
Markdown
350 lines
16 KiB
Markdown
|
|
# Отчёт. Задание 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 мс.
|
|||
|
|
|
|||
|
|
### Графики
|
|||
|
|
|
|||
|
|

|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 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.
|