diff --git a/YaroslavtsevAS/docs/2-nd-lab/experiment_data.csv b/YaroslavtsevAS/docs/2-nd-lab/experiment_data.csv new file mode 100644 index 0000000..9be2e5e --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/experiment_data.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +Small 10x6,BFS,0.036793333189658974,27.0,10.0 +Small 10x6,DFS,0.027613000080843147,18.0,14.0 +Small 10x6,AStar,0.06921633333452822,22.0,10.0 +Medium 10x10,BFS,0.04238766662941392,40.0,15.0 +Medium 10x10,DFS,0.01747666677450373,21.0,15.0 +Medium 10x10,AStar,0.05802666661717618,28.0,15.0 +Large 20x20,BFS,0.09112733308332584,64.0,31.0 +Large 20x20,DFS,0.05382399983015299,64.0,41.0 +Large 20x20,AStar,0.21716699969450323,60.0,31.0 +Empty 15x15,BFS,0.38009000005937804,223.0,25.0 +Empty 15x15,DFS,0.17080266661650967,221.0,109.0 +Empty 15x15,AStar,0.6228723332242225,169.0,25.0 +No exit 10x10,BFS,0.014016666682437062,9.0,0.0 +No exit 10x10,DFS,0.013433666936180089,9.0,0.0 +No exit 10x10,AStar,0.024179666373432458,9.0,0.0 diff --git a/YaroslavtsevAS/docs/2-nd-lab/main.py b/YaroslavtsevAS/docs/2-nd-lab/main.py new file mode 100644 index 0000000..78e1afc --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/main.py @@ -0,0 +1,525 @@ +import sys +import os +import time +import csv +from collections import deque +import heapq +import matplotlib.pyplot as plt +import numpy as np + +# ========== Модель данных ========== +class Tile: + """Одна клетка лабиринта.""" + def __init__(self, x, y): + self._x = x + self._y = y + self._wall = False + self._entry = False + self._goal = False + + @property + def x(self): return self._x + @property + def y(self): return self._y + @property + def is_wall(self): return self._wall + @is_wall.setter + def is_wall(self, value): self._wall = value + @property + def is_entry(self): return self._entry + @is_entry.setter + def is_entry(self, value): self._entry = value + @property + def is_goal(self): return self._goal + @is_goal.setter + def is_goal(self, value): self._goal = value + + def can_walk(self): + """Можно ли встать на эту клетку.""" + return not self._wall + + +class Labyrinth: + """Прямоугольный лабиринт.""" + def __init__(self, width, height): + self._width = width + self._height = height + self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)] + self._start = None + self._exit = None + + @property + def width(self): return self._width + @property + def height(self): return self._height + @property + def start(self): return self._start + @property + def exit(self): return self._exit + + def tile_at(self, x, y): + if 0 <= x < self._width and 0 <= y < self._height: + return self._grid[y][x] + return None + + def configure_tile(self, x, y, kind): + tile = self.tile_at(x, y) + if tile is None: + return + if kind == 'wall': + tile.is_wall = True + elif kind == 'entry': + if self._start: + self._start.is_entry = False + tile.is_entry = True + tile.is_wall = False + self._start = tile + elif kind == 'goal': + if self._exit: + self._exit.is_goal = False + tile.is_goal = True + tile.is_wall = False + self._exit = tile + elif kind == 'floor': + tile.is_wall = False + + def neighbours(self, tile): + """Соседние проходимые клетки.""" + res = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nb = self.tile_at(tile.x + dx, tile.y + dy) + if nb and nb.can_walk(): + res.append(nb) + return res + + +# ========== Загрузка из файла ========== +class LabyrinthBuilder: + def build(self, filename): + raise NotImplementedError + +class TextLabyrinthBuilder(LabyrinthBuilder): + def build(self, filename): + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f] + h = len(lines) + w = max(len(l) for l in lines) if h else 0 + if h == 0 or w == 0: + raise ValueError("Файл лабиринта пуст.") + entries = exits = 0 + lab = Labyrinth(w, h) + for y, row in enumerate(lines): + for x, ch in enumerate(row): + if ch == '#': + lab.configure_tile(x, y, 'wall') + elif ch == 'S': + lab.configure_tile(x, y, 'entry') + entries += 1 + elif ch == 'E': + lab.configure_tile(x, y, 'goal') + exits += 1 + else: + lab.configure_tile(x, y, 'floor') + if entries != 1 or exits != 1: + raise ValueError(f"Некорректный лабиринт: найдено S={entries}, E={exits}") + return lab + + +# ========== Алгоритмы поиска ========== +class Pathfinder: + def find_path(self, lab, start, goal): + raise NotImplementedError + + def _build_path(self, preds, start, goal): + path = [] + cur = goal + while cur is not None: + path.append(cur) + cur = preds.get(cur) + path.reverse() + return path + + @property + def visited_count(self): + return getattr(self, '_visited', 0) + + +class BFS_Pathfinder(Pathfinder): + def find_path(self, lab, start, goal): + q = deque([start]) + preds = {start: None} + seen = {start} + while q: + cur = q.popleft() + if cur == goal: + self._visited = len(seen) + return self._build_path(preds, start, goal) + for nb in lab.neighbours(cur): + if nb not in seen: + seen.add(nb) + preds[nb] = cur + q.append(nb) + self._visited = len(seen) + return [] + + +class DFS_Pathfinder(Pathfinder): + def find_path(self, lab, start, goal): + stack = [start] + preds = {start: None} + seen = {start} + while stack: + cur = stack.pop() + if cur == goal: + self._visited = len(seen) + return self._build_path(preds, start, goal) + for nb in lab.neighbours(cur): + if nb not in seen: + seen.add(nb) + preds[nb] = cur + stack.append(nb) + self._visited = len(seen) + return [] + + +class AStar_Pathfinder(Pathfinder): + def _heuristic(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, lab, start, goal): + heap = [] + cnt = 0 + f_start = self._heuristic(start, goal) + heapq.heappush(heap, (f_start, cnt, start)) + cnt += 1 + preds = {} + g = {start: 0} + f = {start: f_start} + seen = set() + while heap: + cur_f, _, cur = heapq.heappop(heap) + seen.add(cur) + if cur == goal: + self._visited = len(seen) + return self._build_path(preds, start, goal) + if cur_f > f.get(cur, float('inf')): + continue + for nb in lab.neighbours(cur): + tent_g = g[cur] + 1 + if tent_g < g.get(nb, float('inf')): + preds[nb] = cur + g[nb] = tent_g + new_f = tent_g + self._heuristic(nb, goal) + f[nb] = new_f + heapq.heappush(heap, (new_f, cnt, nb)) + cnt += 1 + self._visited = len(seen) + return [] + + +# ========== Интерактивный игрок ========== +class Explorer: + def __init__(self, start_tile, labyrinth): + self._current = start_tile + self._previous = None + self._lab = labyrinth + + @property + def current(self): + return self._current + + def move(self, tile): + if tile and tile.can_walk(): + self._previous = self._current + self._current = tile + return True + return False + + def undo(self): + if self._previous: + self._current, self._previous = self._previous, None + return True + return False + + +class Action: + def execute(self): raise NotImplementedError + def undo(self): raise NotImplementedError + + +class MoveAction(Action): + def __init__(self, explorer, direction, lab): + self._explorer = explorer + self._dx, self._dy = direction + self._lab = lab + self._done = False + + def execute(self): + nx = self._explorer.current.x + self._dx + ny = self._explorer.current.y + self._dy + target = self._lab.tile_at(nx, ny) + if target and target.can_walk(): + self._explorer.move(target) + self._done = True + return True + return False + + def undo(self): + if self._done: + self._explorer.undo() + self._done = False + return True + return False + + +class GameObserver: + def update(self, event, data): raise NotImplementedError + + +class TerminalDisplay(GameObserver): + def __init__(self, explorer=None): + self._explorer = explorer + self._last_path = None + + def update(self, event, data): + if event == 'labyrinth_loaded': + self._draw_lab(data) + elif event == 'path_found': + self._last_path = data + self._show_path_info(data) + elif event == 'player_moved': + self._draw_with_player(data) + + def _draw_lab(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print('=' * (lab.width * 2 + 4)) + print(' ЛАБИРИНТ') + print('=' * (lab.width * 2 + 4)) + for y in range(lab.height): + print(' ', end='') + for x in range(lab.width): + t = lab.tile_at(x, y) + if t == lab.start: print('S', end=' ') + elif t == lab.exit: print('E', end=' ') + elif t.is_wall: print('#', end=' ') + else: print('.', end=' ') + print() + print('=' * (lab.width * 2 + 4)) + print(' S – вход E – выход # – стена . – пол') + + def _draw_with_player(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print('=' * (lab.width * 2 + 4)) + print(' ЛАБИРИНТ (P – игрок)') + print('=' * (lab.width * 2 + 4)) + for y in range(lab.height): + print(' ', end='') + for x in range(lab.width): + t = lab.tile_at(x, y) + if self._explorer and t == self._explorer.current: + print('P', end=' ') + elif t == lab.start: print('S', end=' ') + elif t == lab.exit: print('E', end=' ') + elif t.is_wall: print('#', end=' ') + else: print('.', end=' ') + print() + print('=' * (lab.width * 2 + 4)) + if self._explorer: + print(f' Позиция: ({self._explorer.current.x}, {self._explorer.current.y})') + + def _show_path_info(self, path): + if not path: + print('\n Путь не найден!') + else: + print(f'\n Длина найденного пути: {len(path)} клеток.') + + +class LabyrinthSolver: + def __init__(self, lab): + self._lab = lab + self._strategy = None + self._observers = [] + + def attach(self, obs): + self._observers.append(obs) + + def _notify(self, event, data): + for obs in self._observers: + obs.update(event, data) + + def set_strategy(self, strategy): + self._strategy = strategy + + def solve(self): + if self._strategy is None: + return None + start_t = time.perf_counter() + path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit) + elapsed = (time.perf_counter() - start_t) * 1000 + self._notify('path_found', path) + return { + 'time_ms': elapsed, + 'visited': self._strategy.visited_count, + 'length': len(path) + } + + +# ========== Эксперименты и визуализация ========== +def run_benchmark(maze_file, strategy, runs=5): + builder = TextLabyrinthBuilder() + lab = builder.build(maze_file) + total_t = total_v = total_l = 0 + for _ in range(runs): + solver = LabyrinthSolver(lab) + solver.set_strategy(strategy) + stats = solver.solve() + if stats: + total_t += stats['time_ms'] + total_v += stats['visited'] + total_l += stats['length'] + return { + 'time_ms': total_t / runs, + 'visited_cells': total_v / runs, + 'path_length': total_l / runs + } + + +def create_charts(results): + mazes = sorted({r['maze'] for r in results}) + strategies = ['BFS', 'DFS', 'AStar'] + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + x = np.arange(len(mazes)) + width = 0.25 + + for i, strat in enumerate(strategies): + times = [next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] + axes[0].bar(x + i*width, times, width, label=strat) + axes[0].set_title('Время выполнения (мс)') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=30, ha='right') + axes[0].legend() + axes[0].grid(alpha=0.3) + + for i, strat in enumerate(strategies): + visited = [next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] + axes[1].bar(x + i*width, visited, width, label=strat) + axes[1].set_title('Посещено клеток') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=30, ha='right') + axes[1].legend() + axes[1].grid(alpha=0.3) + + for i, strat in enumerate(strategies): + lengths = [next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == strat), 0) for m in mazes] + axes[2].bar(x + i*width, lengths, width, label=strat) + axes[2].set_title('Длина пути') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=30, ha='right') + axes[2].legend() + axes[2].grid(alpha=0.3) + + plt.tight_layout() + plt.savefig('maze_performance.png', dpi=150, bbox_inches='tight') + plt.show() + + +# ========== Главный вход ========== +if __name__ == '__main__': + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + print('Запуск экспериментов...') + maze_list = [ + ('maze/maze1.txt', 'Small 10x6'), + ('maze/maze10x10.txt', 'Medium 10x10'), + ('maze/maze20x20.txt', 'Large 20x20'), + ('maze/maze_empty.txt', 'Empty 15x15'), + ('maze/maze_no_exit.txt', 'No exit 10x10') + ] + strategies = [ + ('BFS', BFS_Pathfinder()), + ('DFS', DFS_Pathfinder()), + ('AStar', AStar_Pathfinder()) + ] + all_results = [] + for file, name in maze_list: + print(f'\nТестируем {name}...') + for sname, strat in strategies: + try: + stats = run_benchmark(file, strat, runs=3) + all_results.append({ + 'maze': name, + 'strategy': sname, + 'time_ms': stats['time_ms'], + 'visited_cells': stats['visited_cells'], + 'path_length': stats['path_length'] + }) + print(f' {sname}: время={stats["time_ms"]:.3f}мс, клеток={stats["visited_cells"]:.0f}, длина={stats["path_length"]:.0f}') + except Exception as e: + print(f' {sname}: ошибка – {e}') + with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze','strategy','time_ms','visited_cells','path_length']) + writer.writeheader() + writer.writerows(all_results) + if all_results: + create_charts(all_results) + print('\nРезультаты сохранены в experiment_data.csv и maze_performance.png') + else: + # Интерактивная игра + maze_file = 'maze/maze1.txt' + if len(sys.argv) > 1: + maze_file = sys.argv[1] + builder = TextLabyrinthBuilder() + labyrinth = builder.build(maze_file) + + player = Explorer(labyrinth.start, labyrinth) + display = TerminalDisplay(player) + display.update('labyrinth_loaded', labyrinth) + + solver = LabyrinthSolver(labyrinth) + solver.attach(display) + + print('\n УПРАВЛЕНИЕ:') + print(' H – влево J – вниз K – вверх L – вправо') + print(' U – отменить ход Q – выход') + print(' Автопоиск: B – BFS D – DFS A – A*') + print('=' * 50) + + history = [] + while True: + cmd = input('\n Команда > ').lower().strip() + if cmd == 'q': + print('\n До свидания!') + break + elif cmd == 'b': + solver.set_strategy(BFS_Pathfinder()) + stats = solver.solve() + print(f"\n BFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") + elif cmd == 'd': + solver.set_strategy(DFS_Pathfinder()) + stats = solver.solve() + print(f"\n DFS: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") + elif cmd == 'a': + solver.set_strategy(AStar_Pathfinder()) + stats = solver.solve() + print(f"\n A*: время={stats['time_ms']:.3f}мс, посещено={stats['visited']}, длина={stats['length']}") + elif cmd in ('h','j','k','l'): + dirs = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)} + action = MoveAction(player, dirs[cmd], labyrinth) + if action.execute(): + history.append(action) + display.update('player_moved', labyrinth) + if player.current == labyrinth.exit: + print('\n ПОЗДРАВЛЯЕМ! ВЫ ВЫБРАЛИСЬ ИЗ ЛАБИРИНТА!') + print(f' Всего ходов: {len(history)}') + break + else: + print('\n Там стена!') + elif cmd == 'u': + if history: + act = history.pop() + act.undo() + display.update('player_moved', labyrinth) + print('\n Ход отменён') + else: + print('\n Нечего отменять') + else: + print('\n Неизвестная команда. Используйте H,J,K,L,U,Q,B,D,A') \ No newline at end of file diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze/maze1.txt b/YaroslavtsevAS/docs/2-nd-lab/maze/maze1.txt new file mode 100644 index 0000000..dbe8284 --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/maze/maze1.txt @@ -0,0 +1,6 @@ +########## +#S.......# +#.###.#.## +#...#....# +#.#.#.##.# +#.....E..# diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze/maze10x10.txt b/YaroslavtsevAS/docs/2-nd-lab/maze/maze10x10.txt new file mode 100644 index 0000000..f06c970 --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/maze/maze10x10.txt @@ -0,0 +1,10 @@ +########## +#S..#...## +#.##.#.#.# +#....#...# +#.##.#.### +#..#.....# +##.#.###.# +#......#.# +#.####.#E# +########## diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze/maze20x20.txt b/YaroslavtsevAS/docs/2-nd-lab/maze/maze20x20.txt new file mode 100644 index 0000000..359613e --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/maze/maze20x20.txt @@ -0,0 +1,20 @@ +#################### +#S################## +#..#.############### +##...############### +####.############### +####. ############## +#.##..############## +#.......############ +#......############# +#.....############## +#....#####E.######## +#...######..######## +#...#####..######### +#...####..########## +#...###..########### +#........########### +#################### +#################### +#################### +#################### diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze/maze_empty.txt b/YaroslavtsevAS/docs/2-nd-lab/maze/maze_empty.txt new file mode 100644 index 0000000..5db6ebe --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/maze/maze_empty.txt @@ -0,0 +1,15 @@ +............... +.S............. +............... +............... +............... +............... +............... +............... +............... +............... +............... +............... +............... +.............E. +............... diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze/maze_no_exit.txt b/YaroslavtsevAS/docs/2-nd-lab/maze/maze_no_exit.txt new file mode 100644 index 0000000..9cc98ec --- /dev/null +++ b/YaroslavtsevAS/docs/2-nd-lab/maze/maze_no_exit.txt @@ -0,0 +1,10 @@ +########## +#S..#...## +#.##.#.#.# +#....#...# +########## +#....#...# +#.##.#.#.# +#..#.....# +##.#.###E# +########## diff --git a/YaroslavtsevAS/docs/2-nd-lab/maze_performance.png b/YaroslavtsevAS/docs/2-nd-lab/maze_performance.png new file mode 100644 index 0000000..7e4dce8 Binary files /dev/null and b/YaroslavtsevAS/docs/2-nd-lab/maze_performance.png differ diff --git a/YaroslavtsevAS/docs/ReportLab2.md b/YaroslavtsevAS/docs/ReportLab2.md new file mode 100644 index 0000000..8e45e6a --- /dev/null +++ b/YaroslavtsevAS/docs/ReportLab2.md @@ -0,0 +1,119 @@ +# Отчёт по лабораторной работе: Поиск выхода из лабиринта + +## 1. Постановка задачи + +Цель — разработать программу для загрузки лабиринта из текстового файла, поиска маршрута от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения эффективности трёх классических алгоритмов поиска пути. + +### Основные требования: +- Модель лабиринта: классы `Tile` (клетка) и `Labyrinth` (сетка). +- Загрузка карты из файла с символами `#` (стена), `S` (старт), `E` (выход). +- Реализация трёх стратегий поиска: BFS, DFS, A*. +- Оркестратор `LabyrinthSolver` с возможностью смены алгоритма во время выполнения. +- Сбор метрик: время работы (мс), количество посещённых клеток, длина найденного пути. +- Проведение экспериментов на пяти лабиринтах разного размера и структуры. + +### Использованные паттерны проектирования + +- **Builder** – `TextLabyrinthBuilder` инкапсулирует логику парсинга текстового файла и создания объекта `Labyrinth`. Это упрощает добавление новых форматов. +- **Strategy** – `BFS_Pathfinder`, `DFS_Pathfinder` и `AStar_Pathfinder` реализуют общий интерфейс `Pathfinder`. Класс `LabyrinthSolver` может динамически переключаться между ними. +- **Observer** – интерфейс `GameObserver` и его реализация `TerminalDisplay` позволяют отделить отображение лабиринта от бизнес-логики. +- **Command** – `MoveAction` оборачивает перемещение игрока, сохраняя историю и предоставляя возможность отмены (`undo`). + +## 2. Архитектура приложения + +- **Модель**: `Tile` (клетка) и `Labyrinth` (лабиринт). +- **Загрузка**: `LabyrinthBuilder` (абстрактный) и `TextLabyrinthBuilder`. +- **Алгоритмы**: `BFS_Pathfinder`, `DFS_Pathfinder`, `AStar_Pathfinder` — наследники `Pathfinder`. +- **Управление поиском**: `LabyrinthSolver` (смена стратегии, оповещение наблюдателей). +- **Визуализация**: `TerminalDisplay`, реализующий `GameObserver`. +- **Интерактив**: `Explorer` (игрок) и `MoveAction` (команда перемещения). + +## 3. Реализация алгоритмов поиска пути + +### BFS (поиск в ширину) +Использует очередь. Стартовая клетка помещается в очередь, затем на каждом шаге извлекается первый элемент. Если это выход — путь восстановлен. Иначе все непосещённые соседи добавляются в конец очереди. Гарантирует кратчайший путь по количеству шагов. + +### DFS (поиск в глубину) +Вместо очереди используется стек (LIFO). Начинает со старта, на каждом шаге извлекается последний добавленный элемент. Быстрее продвигается в глубину, но путь часто оказывается далеко не оптимальным. + +### A* (A-звездочка) +Применяет приоритетную очередь с эвристической функцией `f = g + h`, где `g` — реальная стоимость пути от старта, `h` — Манхэттенское расстояние до выхода. Всегда находит оптимальный путь при допустимой эвристике и обычно посещает меньше клеток. + +## 4. Экспериментальная часть + +### Тестовые лабиринты + +| Название | Размер | Особенность | +|-------------------|-----------|------------------------------------| +| Small 10x6 | 10×6 | простой коридорный лабиринт | +| Medium 10x10 | 10×10 | средняя плотность стен | +| Large 20x20 | 20×20 | сложная запутанная структура | +| Empty 15x15 | 15×15 | полностью проходимый (без стен) | +| No exit 10x10 | 10×10 | выход заблокирован стеной | + +Все эксперименты проводились с усреднением по 3 запускам для сглаживания случайных колебаний времени. + +### Результаты замеров + +| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | +|------------------|----------|------------|-----------------|------------| +| Small 10x6 | BFS | 0.037 | 27 | 10 | +| Small 10x6 | DFS | 0.028 | 18 | 14 | +| Small 10x6 | A* | 0.069 | 22 | 10 | +| Medium 10x10 | BFS | 0.042 | 40 | 15 | +| Medium 10x10 | DFS | 0.017 | 21 | 15 | +| Medium 10x10 | A* | 0.058 | 28 | 15 | +| Large 20x20 | BFS | 0.091 | 64 | 31 | +| Large 20x20 | DFS | 0.054 | 64 | 41 | +| Large 20x20 | A* | 0.217 | 60 | 31 | +| Empty 15x15 | BFS | 0.380 | 223 | 25 | +| Empty 15x15 | DFS | 0.171 | 221 | 109 | +| Empty 15x15 | A* | 0.623 | 169 | 25 | +| No exit 10x10 | BFS | 0.014 | 9 | 0 | +| No exit 10x10 | DFS | 0.013 | 9 | 0 | +| No exit 10x10 | A* | 0.024 | 9 | 0 | + +*Примечание: длина пути 0 означает, что маршрут не существует.* + +### Визуализация + +![Сравнение производительности алгоритмов](maze_performance.png) + +На графиках отражены три метрики: время выполнения, число посещённых клеток и длина найденного пути. + +## 5. Анализ результатов + +### Сравнение алгоритмов + +**BFS** +- Всегда находит кратчайший путь (длины 10, 15, 31, 25 соответственно). +- На больших и пустых лабиринтах посещает много клеток (до 223), что сказывается на времени. +- Хорошо подходит, когда оптимальность критична, а размер лабиринта умеренный. + +**DFS** +- Самый быстрый по времени (0.013–0.171 мс), но часто выдаёт длинные извилистые пути (14, 15, 41, 109). В пустом 15×15 путь составил 109 шагов вместо оптимальных 25. +- Число посещённых клеток невелико, что объясняет высокую скорость. Рекомендуется, когда важнее скорость, а не качество маршрута. + +**A*** +- Находит кратчайший путь во всех случаях (где он существует). +- Посещает в среднем меньше клеток, чем BFS (22, 28, 60, 169), что подтверждает эффективность эвристики. +- Время работы немного выше, чем у BFS, из-за накладных расходов на работу с кучей, однако разница незначительна. + +### Особые случаи + +- В лабиринте **No exit** все алгоритмы быстро обошли доступную область (9 клеток) и корректно вернули пустой путь. +- На **Empty 15×15** DFS показал наихудший путь (109 шагов), в то время как BFS и A* дали идеальные 25. Это наглядно демонстрирует, что DFS непригоден для задач, требующих оптимальности. +- **Large 20×20** показал близкие результаты для BFS и A* по длине пути, но DFS снова выдал более длинный маршрут (41). + +### Выводы и рекомендации + +- Если необходим **кратчайший путь** — выбирайте **BFS** или **A***. A* часто эффективнее по памяти, так как посещает меньше клеток. +- Если важна **максимальная скорость** и допустим неоптимальный путь — используйте **DFS**. +- **A*** является наилучшим компромиссом для большинства практических задач благодаря сбалансированному сочетанию времени и оптимальности. + +## 6. Заключение + +Разработанное приложение демонстрирует применение паттернов проектирования для построения гибкой и расширяемой архитектуры. +Экспериментальные данные подтверждают ожидаемое поведение алгоритмов: BFS и A* гарантируют оптимальность, DFS — высокую скорость ценой качества маршрута. A* показал наилучшее соотношение «затраты / качество», посещая меньше клеток и находя кратчайший путь. + +Все полученные метрики сохранены в файл `experiment_data.csv`, графики — в `maze_performance.png`. \ No newline at end of file diff --git a/YaroslavtsevAS/docs/maze_performance.png b/YaroslavtsevAS/docs/maze_performance.png new file mode 100644 index 0000000..7e4dce8 Binary files /dev/null and b/YaroslavtsevAS/docs/maze_performance.png differ