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

16 KiB
Raw Blame History

Отчёт. Задание 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)

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)

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)

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)

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)

# 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


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.