Merge pull request '[2] YaroslavtsevAS-lab2' (#298) from YaroslavtsevAS/2026-rff_mp:YaroslavtsevAS-lab2 into develop

Reviewed-on: #298
This commit is contained in:
AndreyUrs 2026-05-30 12:00:16 +00:00
commit 0b1738c0ff
10 changed files with 721 additions and 0 deletions

View File

@ -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
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.036793333189658974 27.0 10.0
3 Small 10x6 DFS 0.027613000080843147 18.0 14.0
4 Small 10x6 AStar 0.06921633333452822 22.0 10.0
5 Medium 10x10 BFS 0.04238766662941392 40.0 15.0
6 Medium 10x10 DFS 0.01747666677450373 21.0 15.0
7 Medium 10x10 AStar 0.05802666661717618 28.0 15.0
8 Large 20x20 BFS 0.09112733308332584 64.0 31.0
9 Large 20x20 DFS 0.05382399983015299 64.0 41.0
10 Large 20x20 AStar 0.21716699969450323 60.0 31.0
11 Empty 15x15 BFS 0.38009000005937804 223.0 25.0
12 Empty 15x15 DFS 0.17080266661650967 221.0 109.0
13 Empty 15x15 AStar 0.6228723332242225 169.0 25.0
14 No exit 10x10 BFS 0.014016666682437062 9.0 0.0
15 No exit 10x10 DFS 0.013433666936180089 9.0 0.0
16 No exit 10x10 AStar 0.024179666373432458 9.0 0.0

View File

@ -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')

View File

@ -0,0 +1,6 @@
##########
#S.......#
#.###.#.##
#...#....#
#.#.#.##.#
#.....E..#

View File

@ -0,0 +1,10 @@
##########
#S..#...##
#.##.#.#.#
#....#...#
#.##.#.###
#..#.....#
##.#.###.#
#......#.#
#.####.#E#
##########

View File

@ -0,0 +1,20 @@
####################
#S##################
#..#.###############
##...###############
####.###############
####. ##############
#.##..##############
#.......############
#......#############
#.....##############
#....#####E.########
#...######..########
#...#####..#########
#...####..##########
#...###..###########
#........###########
####################
####################
####################
####################

View File

@ -0,0 +1,15 @@
...............
.S.............
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
...............
.............E.
...............

View File

@ -0,0 +1,10 @@
##########
#S..#...##
#.##.#.#.#
#....#...#
##########
#....#...#
#.##.#.#.#
#..#.....#
##.#.###E#
##########

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -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.0130.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`.

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB