diff --git a/BoriskovaDV/docs/data/2-nd-exercise/experiment_results.csv b/BoriskovaDV/docs/data/2-nd-exercise/experiment_results.csv new file mode 100644 index 0000000..03f0486 --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/experiment_results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +Small 10x6,BFS,0.05722500009142095,25.0,16.0 +Small 10x6,DFS,0.05680966667872175,24.0,16.0 +Small 10x6,AStar,0.04801966664066034,23.0,16.0 +Medium 10x10,BFS,0.04772166676048073,47.0,16.0 +Medium 10x10,DFS,0.034641333362136116,44.0,30.0 +Medium 10x10,AStar,0.0983669999641279,47.0,16.0 +Large 20x20,BFS,0.09949400002066493,100.0,36.0 +Large 20x20,DFS,0.07004933331700158,75.0,68.0 +Large 20x20,AStar,0.16450733316257052,85.0,36.0 +Empty 15x15,BFS,0.13264433331035738,133.0,17.0 +Empty 15x15,DFS,0.11371733338213137,161.0,89.0 +Empty 15x15,AStar,0.1543506666621397,65.0,17.0 +No exit 10x10,BFS,0.04392100011803753,25.0,0.0 +No exit 10x10,DFS,0.05871466661725814,25.0,0.0 +No exit 10x10,AStar,0.046440666665148456,25.0,0.0 diff --git a/BoriskovaDV/docs/data/2-nd-exercise/main.py b/BoriskovaDV/docs/data/2-nd-exercise/main.py new file mode 100644 index 0000000..2baa9c2 --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/main.py @@ -0,0 +1,438 @@ +import sys +import os +from collections import deque +import heapq +import time +import csv +import matplotlib.pyplot as plt +import numpy as np + +class GridPoint: + def __init__(self, x, y): + self.x = x + self.y = y + self.blocked = False + self.is_start = False + self.is_exit = False + + def can_step(self): + return not self.blocked + +class Labyrinth: + def __init__(self, w, h): + self.w = w + self.h = h + self.grid = [[GridPoint(x, y) for x in range(w)] for y in range(h)] + self.start_point = None + self.exit_point = None + + def get_point(self, x, y): + if 0 <= x < self.w and 0 <= y < self.h: + return self.grid[y][x] + return None + + def set_point(self, x, y, typ): + p = self.get_point(x, y) + if not p: + return + if typ == 'wall': + p.blocked = True + elif typ == 'start': + if self.start_point: + self.start_point.is_start = False + p.is_start = True + p.blocked = False + self.start_point = p + elif typ == 'exit': + if self.exit_point: + self.exit_point.is_exit = False + p.is_exit = True + p.blocked = False + self.exit_point = p + elif typ == 'path': + p.blocked = False + + def neighbors(self, p): + dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)] + res = [] + for dx, dy in dirs: + nx, ny = p.x + dx, p.y + dy + nb = self.get_point(nx, ny) + if nb and nb.can_step(): + res.append(nb) + return res + +class MazeLoader: + def load(self, filename): + raise NotImplementedError + +class TextMazeLoader(MazeLoader): + def load(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + start_cnt = 0 + exit_cnt = 0 + lab = Labyrinth(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == '#': + lab.set_point(x, y, 'wall') + elif ch == 'S': + lab.set_point(x, y, 'start') + start_cnt += 1 + elif ch == 'E': + lab.set_point(x, y, 'exit') + exit_cnt += 1 + else: + lab.set_point(x, y, 'path') + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Need exactly one S and one E. Found S={start_cnt}, E={exit_cnt}") + return lab + +class SearchAlgorithm: + def find_way(self, lab, start, goal): + raise NotImplementedError + + def _build_path(self, prev, start, goal): + path = [] + cur = goal + while cur: + path.append(cur) + cur = prev.get(cur) + path.reverse() + return path + + def get_visited(self): + return getattr(self, '_visited', 0) + +class BreadthFirst(SearchAlgorithm): + def find_way(self, lab, start, goal): + q = deque([start]) + prev = {start: None} + seen = {start} + while q: + cur = q.popleft() + if cur == goal: + self._visited = len(seen) + return self._build_path(prev, start, goal) + for nb in lab.neighbors(cur): + if nb not in seen: + seen.add(nb) + prev[nb] = cur + q.append(nb) + self._visited = len(seen) + return [] + +class DepthFirst(SearchAlgorithm): + def find_way(self, lab, start, goal): + stack = [start] + prev = {start: None} + seen = {start} + while stack: + cur = stack.pop() + if cur == goal: + self._visited = len(seen) + return self._build_path(prev, start, goal) + for nb in lab.neighbors(cur): + if nb not in seen: + seen.add(nb) + prev[nb] = cur + stack.append(nb) + self._visited = len(seen) + return [] + +class AStar(SearchAlgorithm): + def _dist(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_way(self, lab, start, goal): + heap = [] + cnt = 0 + start_f = self._dist(start, goal) + heapq.heappush(heap, (start_f, cnt, start)) + cnt += 1 + prev = {} + g = {start: 0} + f = {start: start_f} + seen = set() + while heap: + cur_f, _, cur = heapq.heappop(heap) + seen.add(cur) + if cur == goal: + self._visited = len(seen) + return self._build_path(prev, start, goal) + if cur_f > f.get(cur, float('inf')): + continue + for nb in lab.neighbors(cur): + new_g = g[cur] + 1 + if new_g < g.get(nb, float('inf')): + prev[nb] = cur + g[nb] = new_g + new_f = new_g + self._dist(nb, goal) + f[nb] = new_f + heapq.heappush(heap, (new_f, cnt, nb)) + cnt += 1 + self._visited = len(seen) + return [] + +class LabyrinthSolver: + def __init__(self, lab): + self.lab = lab + self.algorithm = None + + def set_algorithm(self, algo): + self.algorithm = algo + + def solve(self): + if not self.algorithm: + return None + t0 = time.perf_counter() + path = self.algorithm.find_way(self.lab, self.lab.start_point, self.lab.exit_point) + t1 = time.perf_counter() + ms = (t1 - t0) * 1000 + return ms, self.algorithm.get_visited(), len(path) + +class Player: + def __init__(self, start, lab): + self.current = start + self.last = None + self.lab = lab + + def move(self, cell): + if cell and cell.can_step(): + self.last = self.current + self.current = cell + return True + return False + + def undo(self): + if self.last: + self.current, self.last = self.last, None + return True + return False + +class Command: + def do(self): + raise NotImplementedError + def revert(self): + raise NotImplementedError + +class MoveCommand(Command): + def __init__(self, player, dx, dy, lab): + self.player = player + self.dx = dx + self.dy = dy + self.lab = lab + self.done = False + + def do(self): + nx = self.player.current.x + self.dx + ny = self.player.current.y + self.dy + target = self.lab.get_point(nx, ny) + if target and target.can_step(): + self.player.move(target) + self.done = True + return True + return False + + def revert(self): + if self.done: + self.player.undo() + self.done = False + return True + return False + +class InteractiveView: + def __init__(self, lab, player): + self.lab = lab + self.player = player + + def render(self): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (self.lab.w * 2 + 4)) + print(" LABYRINTH (P = player)") + print("=" * (self.lab.w * 2 + 4)) + for y in range(self.lab.h): + print(" ", end='') + for x in range(self.lab.w): + p = self.lab.get_point(x, y) + if self.player.current == p: + print('P', end=' ') + elif p == self.lab.start_point: + print('S', end=' ') + elif p == self.lab.exit_point: + print('E', end=' ') + elif p.blocked: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (self.lab.w * 2 + 4)) + print(f" Position: ({self.player.current.x},{self.player.current.y})") + print(" Controls: h(left) j(down) k(up) l(right) u=undo q=quit") + print(" Auto-search: b=BFS d=DFS a=A*") + +def run_experiment(maze_file, algo, runs=5): + loader = TextMazeLoader() + lab = loader.load(maze_file) + total_ms = 0 + total_visited = 0 + total_len = 0 + for _ in range(runs): + solver = LabyrinthSolver(lab) + solver.set_algorithm(algo) + stats = solver.solve() + if stats: + ms, vis, plen = stats + total_ms += ms + total_visited += vis + total_len += plen + return total_ms / runs, total_visited / runs, total_len / runs + +def generate_plots(results): + mazes = list(set([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 = [] + for maze in mazes: + val = next((r['time_ms'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0) + times.append(val) + axes[0].bar(x + i*width, times, width, label=strat) + axes[0].set_xlabel('Maze') + axes[0].set_ylabel('Time (ms)') + axes[0].set_title('Execution Time') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=45, ha='right') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + for i, strat in enumerate(strategies): + visited = [] + for maze in mazes: + val = next((r['visited_cells'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0) + visited.append(val) + axes[1].bar(x + i*width, visited, width, label=strat) + axes[1].set_xlabel('Maze') + axes[1].set_ylabel('Visited Cells') + axes[1].set_title('Visited Cells') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=45, ha='right') + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + for i, strat in enumerate(strategies): + lengths = [] + for maze in mazes: + val = next((r['path_length'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0) + lengths.append(val) + axes[2].bar(x + i*width, lengths, width, label=strat) + axes[2].set_xlabel('Maze') + axes[2].set_ylabel('Path Length') + axes[2].set_title('Path Length') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=45, ha='right') + axes[2].legend() + axes[2].grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight') + plt.show() + +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + print("Running experiments on all mazes...") + maze_files = [ + ("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") + ] + algorithms = [ + ("BFS", BreadthFirst()), + ("DFS", DepthFirst()), + ("AStar", AStar()) + ] + results = [] + for fname, label in maze_files: + print(f"Testing {label}...") + for aname, algo in algorithms: + try: + avg_t, avg_v, avg_l = run_experiment(fname, algo, runs=3) + results.append({ + 'maze': label, + 'strategy': aname, + 'time_ms': avg_t, + 'visited_cells': avg_v, + 'path_length': avg_l + }) + print(f" {aname}: time={avg_t:.3f}ms visited={avg_v:.0f} length={avg_l:.0f}") + except Exception as e: + print(f" {aname}: ERROR {e}") + # save csv + with open('experiment_results.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(results) + generate_plots(results) + print("Done. Results saved to experiment_results.csv and performance_comparison.png") + sys.exit(0) + + # else interactive mode + loader = TextMazeLoader() + lab = loader.load("maze/maze1.txt") + player = Player(lab.start_point, lab) + view = InteractiveView(lab, player) + view.render() + + solver = LabyrinthSolver(lab) + history = [] + + while True: + key = input("\n > ").lower() + if key == 'q': + print("Goodbye!") + break + elif key == 'b': + solver.set_algorithm(BreadthFirst()) + ms, vis, plen = solver.solve() + print(f"BFS: {ms:.3f}ms, visited={vis}, length={plen}") + elif key == 'd': + solver.set_algorithm(DepthFirst()) + ms, vis, plen = solver.solve() + print(f"DFS: {ms:.3f}ms, visited={vis}, length={plen}") + elif key == 'a': + solver.set_algorithm(AStar()) + ms, vis, plen = solver.solve() + print(f"A*: {ms:.3f}ms, visited={vis}, length={plen}") + elif key in ('h','j','k','l'): + moves = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)} + dx, dy = moves[key] + cmd = MoveCommand(player, dx, dy, lab) + if cmd.do(): + history.append(cmd) + view.render() + if player.current == lab.exit_point: + print("\n*** YOU REACHED THE EXIT! ***") + print(f"Total moves: {len(history)}") + break + else: + print("Can't go there - wall!") + elif key == 'u': + if history: + cmd = history.pop() + cmd.revert() + view.render() + print("Undo last move") + else: + print("Nothing to undo") + else: + print("Unknown command") \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/maze/maze1.txt b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze1.txt new file mode 100644 index 0000000..89b0bf7 --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze1.txt @@ -0,0 +1,7 @@ +########## +#S # +# ####### # +# # # # +# # ### # # +# # E # +########## \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/maze/maze10x10.txt b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze10x10.txt new file mode 100644 index 0000000..c8e24ac --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze10x10.txt @@ -0,0 +1,10 @@ +########## +#S # +# # #### # +# # # +# #### # # +# # # +# #### # # +# # # +# # +########E# \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/maze/maze20x20.txt b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze20x20.txt new file mode 100644 index 0000000..648e1df --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze20x20.txt @@ -0,0 +1,21 @@ +#################### +#S # +# ############### # +# # # # +# # ######### # # # +# # # # # # # +# # # ##### # # # # +# # # # # # # # # +# # # # # # # # # # +# # # # # # # # # +# # # ##### # # # # +# # # # # # # +# # ######### # # # +# # # # +# ############### # +# # +# ############### # +# # # # +# # ########### # # +# E# +#################### \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_empty.txt b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_empty.txt new file mode 100644 index 0000000..70f6e29 --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_empty.txt @@ -0,0 +1,15 @@ +############### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E # +############### \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_no_exit.txt b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_no_exit.txt new file mode 100644 index 0000000..1568be0 --- /dev/null +++ b/BoriskovaDV/docs/data/2-nd-exercise/maze/maze_no_exit.txt @@ -0,0 +1,7 @@ +########## +#S # +# # # +# # #### # +# # # +########## +E######### \ No newline at end of file diff --git a/BoriskovaDV/docs/data/2-nd-exercise/performance_comparison.png b/BoriskovaDV/docs/data/2-nd-exercise/performance_comparison.png new file mode 100644 index 0000000..67f9189 Binary files /dev/null and b/BoriskovaDV/docs/data/2-nd-exercise/performance_comparison.png differ diff --git a/BoriskovaDV/docs/performance_comparison-2-nd-exercise.png b/BoriskovaDV/docs/performance_comparison-2-nd-exercise.png new file mode 100644 index 0000000..67f9189 Binary files /dev/null and b/BoriskovaDV/docs/performance_comparison-2-nd-exercise.png differ diff --git a/BoriskovaDV/docs/report2.md b/BoriskovaDV/docs/report2.md new file mode 100644 index 0000000..b18c829 --- /dev/null +++ b/BoriskovaDV/docs/report2.md @@ -0,0 +1,92 @@ +# Отчёт по лабораторной работе: Алгоритмы поиска пути в лабиринте + +## 1. Цель работы + +Разработка программы для загрузки лабиринта из текстового файла, реализации трёх алгоритмов поиска пути (BFS, DFS, A\*) и проведения экспериментального сравнения их эффективности на лабиринтах различной сложности. + +## 2. Структура программы + +Программа написана на Python 3 и состоит из следующих основных классов: + +- `GridPoint` – представление клетки лабиринта (координаты, проходимость, флаги старта/выхода); +- `Labyrinth` – модель лабиринта (сетка клеток, методы получения соседей); +- `TextMazeLoader` – загрузка лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход); +- `SearchAlgorithm` (и наследники `BreadthFirst`, `DepthFirst`, `AStar`) – реализация алгоритмов поиска; +- `LabyrinthSolver` – класс-оркестратор, позволяющий сменить стратегию и измеряющий время выполнения; +- `Player`, `Command`, `MoveCommand`, `InteractiveView` – для интерактивного режима с отменой ходов; +- функции `run_experiment` и `generate_plots` – для многократных запусков и построения графиков. + +## 3. Описание алгоритмов + +### 3.1 BFS (поиск в ширину) +Использует очередь. Гарантирует нахождение кратчайшего пути (по числу шагов). Обходит клетки в порядке увеличения расстояния от старта. + +### 3.2 DFS (поиск в глубину) +Использует стек. Идёт «вглубь» по одному пути, не гарантирует кратчайший путь. Обычно быстрее по времени и памяти на больших лабиринтах. + +### 3.3 A* (звездочка) +Использует приоритетную очередь и эвристику (манхэттенское расстояние). Оценивает клетку по формуле `f = g + h`, где `g` – пройденное расстояние, `h` – эвристика. Находит оптимальный путь, если эвристика допустима. + +## 4. Методика эксперимента + +Для каждого лабиринта каждый алгоритм запускался 3 раза, результаты усреднялись. Измерялись: +- время выполнения (в миллисекундах); +- количество посещённых клеток; +- длина найденного пути. + +Тестовые лабиринты: + +| Название | Размер | Описание | +|----------|--------|-----------| +| Small 10x6 | 10×6 | Простой лабиринт с извилистым коридором | +| Medium 10x10 | 10×10 | Лабиринт среднего размера с несколькими тупиками | +| Large 20x20 | 20×20 | Большой запутанный лабиринт | +| Empty 15x15 | 15×15 | Пустой лабиринт без стен (прямая линия от S до E) | +| No exit 10x10 | 10×10 | Лабиринт без буквы E (путь отсутствует) | + +## 5. Результаты экспериментов + +| Лабиринт | Алгоритм | Время, мс | Посещено клеток | Длина пути | +|----------------|----------|-----------|-----------------|------------| +| Small 10x6 | BFS | 0.057 | 25 | 16 | +| Small 10x6 | DFS | 0.057 | 24 | 16 | +| Small 10x6 | A* | 0.048 | 23 | 16 | +| Medium 10x10 | BFS | 0.048 | 47 | 16 | +| Medium 10x10 | DFS | 0.035 | 44 | 30 | +| Medium 10x10 | A* | 0.098 | 47 | 16 | +| Large 20x20 | BFS | 0.099 | 100 | 36 | +| Large 20x20 | DFS | 0.070 | 75 | 68 | +| Large 20x20 | A* | 0.165 | 85 | 36 | +| Empty 15x15 | BFS | 0.133 | 133 | 17 | +| Empty 15x15 | DFS | 0.114 | 161 | 89 | +| Empty 15x15 | A* | 0.154 | 65 | 17 | +| No exit 10x10 | BFS | 0.044 | 25 | 0 | +| No exit 10x10 | DFS | 0.059 | 25 | 0 | +| No exit 10x10 | A* | 0.046 | 25 | 0 | + +## 6. Анализ результатов + +### 6.1. Нахождение кратчайшего пути +- **BFS** и **A*** нашли оптимальные пути во всех лабиринтах, где выход существовал (длина пути совпадает для них в каждом случае). +- **DFS** в лабиринтах Medium, Large и Empty дал существенно более длинные пути (30 против 16, 68 против 36, 89 против 17), что характерно для глубинного обхода без эвристики. + +### 6.2. Время выполнения +- На малых лабиринтах все алгоритмы работают сопоставимо (0.035–0.099 мс). +- На лабиринте Large 20×20 BFS выполнился за 0.099 мс, A* – 0.165 мс (медленнее из-за сложности поддержки очереди с приоритетом), DFS – быстрее всех (0.070 мс). +- В пустом лабиринте BFS и A* обошли почти все клетки (133 и 65 посещённых соответственно), но A* за счёт эвристики посетил вдвое меньше клеток, хотя время оказалось чуть выше, чем у BFS (0.154 против 0.133 мс). Это объясняется накладными расходами на вычисление эвристики и управление кучей. + +### 6.3. Количество посещённых клеток +- **A*** показал лучшую эффективность в пустом лабиринте (65 посещённых против 133 у BFS и 161 у DFS). В лабиринтах со стенами разница не столь заметна, но A* почти всегда посещал меньше клеток, чем BFS. +- **DFS** в среднем посещает меньше клеток, чем BFS, но при этом путь часто неоптимален. +- **BFS** вынужден обходить всю область равных расстояний, поэтому посещённых клеток обычно больше. + +### 6.4. Поведение при отсутствии выхода +Все алгоритмы корректно завершились, вернув пустой путь (длина 0). В лабиринте без выхода BFS, DFS и A* посетили 25 клеток – это все доступные клетки. + +## 7. Выводы + +1. **BFS** надёжен для поиска кратчайшего пути, но может быть медленнее на больших открытых пространствах из-за широкого обхода. +2. **DFS** – самый быстрый по времени и экономный по памяти, но не гарантирует оптимальность пути. Его применение оправдано, когда любой путь подходит. +3. **A*** демонстрирует лучший баланс: находит кратчайший путь и при этом посещает меньше клеток, чем BFS. Небольшое замедление на сложных лабиринтах компенсируется меньшим числом обработанных клеток. +4. Программа успешно справляется с лабиринтами разного размера и конфигурации, включая отсутствие выхода. +5. Интерактивный режим с отменой ходов (паттерн Command) и выбором алгоритма (паттерн Strategy) реализован и работает корректно.