Merge pull request '[2] 2-nd-exercise' (#266) from KuznetsovYuM/2026-rff_mp:2-nd-exercise into develop

Reviewed-on: #266
This commit is contained in:
VladimirGub 2026-05-30 12:06:13 +00:00
commit cec177e26d
10 changed files with 533 additions and 0 deletions

View File

@ -5,6 +5,7 @@ import time
import os
class Tile:
def __init__(self, column, row):
self._col = column

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

View File

@ -0,0 +1,376 @@
import sys
import csv
from collections import deque
import heapq
import time
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._start = False
self._exit = 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, v):
self._wall = v
@property
def is_start(self):
return self._start
@is_start.setter
def is_start(self, v):
self._start = v
@property
def is_exit(self):
return self._exit
@is_exit.setter
def is_exit(self, v):
self._exit = v
def passable(self):
return not self._wall
class Maze:
def __init__(self, w, h):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start = None
self._exit = None
@property
def width(self):
return self._w
@property
def height(self):
return self._h
@property
def start(self):
return self._start
@property
def exit(self):
return self._exit
def get_cell(self, x, y):
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x, y, kind):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell):
res = []
for dx, dy in [(0,-1),(0,1),(-1,0),(1,0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
res.append(nb)
return res
class MazeLoader:
def load(self, fname):
raise NotImplementedError
class TextMazeLoader(MazeLoader):
def load(self, fname):
with open(fname, 'r') as f:
lines = [ln.rstrip('\n') for ln in f.readlines()]
h = len(lines)
w = max(len(ln) for ln in lines) if h else 0
cntS = 0
cntE = 0
m = Maze(w, h)
for y, ln in enumerate(lines):
for x, ch in enumerate(ln):
if ch == '#':
m.set_cell(x, y, 'wall')
elif ch == 'S':
m.set_cell(x, y, 'start')
cntS += 1
elif ch == 'E':
m.set_cell(x, y, 'exit')
cntE += 1
else:
m.set_cell(x, y, 'path')
if cntS != 1 or cntE != 1:
raise ValueError(f"Bad maze: S={cntS}, E={cntE}")
return m
class PathFinder:
def find(self, maze, start, goal):
raise NotImplementedError
def _reconstruct(self, parent, start, goal):
path = []
cur = goal
while cur:
path.append(cur)
cur = parent.get(cur)
path.reverse()
return path
def visited_count(self):
return getattr(self, '_vis', 0)
class BFS(PathFinder):
def find(self, maze, start, goal):
q = deque([start])
parent = {start: None}
visited = {start}
while q:
cur = q.popleft()
if cur == goal:
self._vis = len(visited)
return self._reconstruct(parent, start, goal)
for nb in maze.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
q.append(nb)
self._vis = len(visited)
return []
class DFS(PathFinder):
def find(self, maze, start, goal):
stack = [start]
parent = {start: None}
visited = {start}
while stack:
cur = stack.pop()
if cur == goal:
self._vis = len(visited)
return self._reconstruct(parent, start, goal)
for nb in maze.neighbours(cur):
if nb not in visited:
visited.add(nb)
parent[nb] = cur
stack.append(nb)
self._vis = len(visited)
return []
class AStar(PathFinder):
def _h(self, cell, goal):
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze, start, goal):
heap = []
idx = 0
start_f = self._h(start, goal)
heapq.heappush(heap, (start_f, idx, start))
idx += 1
parent = {}
g = {start: 0}
f = {start: start_f}
visited = set()
while heap:
cur_f, _, cur = heapq.heappop(heap)
visited.add(cur)
if cur == goal:
self._vis = len(visited)
return self._reconstruct(parent, start, goal)
if cur_f > f.get(cur, float('inf')):
continue
for nb in maze.neighbours(cur):
new_g = g[cur] + 1
if new_g < g.get(nb, float('inf')):
parent[nb] = cur
g[nb] = new_g
new_f = new_g + self._h(nb, goal)
f[nb] = new_f
heapq.heappush(heap, (new_f, idx, nb))
idx += 1
self._vis = len(visited)
return []
class Solver:
def __init__(self, maze):
self._maze = maze
self._algo = None
def set_algo(self, algo):
self._algo = algo
def run(self):
if not self._algo:
return None
t0 = time.perf_counter()
path = self._algo.find(self._maze, self._maze.start, self._maze.exit)
t1 = time.perf_counter()
return {
'time_ms': (t1 - t0) * 1000,
'visited': self._algo.visited_count(),
'path_len': len(path)
}
def benchmark(maze_file, algorithm, runs=5):
loader = TextMazeLoader()
maze = loader.load(maze_file)
total_t = 0.0
total_v = 0
total_l = 0
for _ in range(runs):
s = Solver(maze)
s.set_algo(algorithm)
stats = s.run()
if stats:
total_t += stats['time_ms']
total_v += stats['visited']
total_l += stats['path_len']
return {
'time_ms': total_t / runs,
'visited_cells': total_v / runs,
'path_length': total_l / runs
}
def create_plots(results):
mazes = sorted(set(r['maze'] for r in results))
algos = ['BFS', 'DFS', 'AStar']
fig, axes = plt.subplots(1, 3, figsize=(15,5))
x = np.arange(len(mazes))
width = 0.25
for i, algo in enumerate(algos):
times = []
for m in mazes:
val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
times.append(val)
axes[0].bar(x + i*width, times, width, label=algo)
axes[0].set_title('Execution time (ms)')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
axes[0].legend()
axes[0].grid(alpha=0.3)
for i, algo in enumerate(algos):
visited = []
for m in mazes:
val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
visited.append(val)
axes[1].bar(x + i*width, visited, width, label=algo)
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(alpha=0.3)
for i, algo in enumerate(algos):
lengths = []
for m in mazes:
val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0)
lengths.append(val)
axes[2].bar(x + i*width, lengths, width, label=algo)
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(alpha=0.3)
plt.tight_layout()
plt.savefig('performance_comparison_2-nd-exercise.png', dpi=150, bbox_inches='tight')
plt.show()
if __name__ == "__main__":
test_mazes = [
("maze1.txt", "Small 10x6"),
("maze10x10.txt", "Medium 10x10"),
("maze20x20.txt", "Large 20x20"),
("maze_empty.txt", "Empty 15x15"),
("maze_no_exit.txt", "No exit 10x10")
]
algorithms = [
("BFS", BFS()),
("DFS", DFS()),
("AStar", AStar())
]
all_results = []
for fname, label in test_mazes:
print(f"Testing {label}...")
for name, algo in algorithms:
try:
stat = benchmark(fname, algo, runs=3)
all_results.append({
'maze': label,
'strategy': name,
'time_ms': stat['time_ms'],
'visited_cells': stat['visited_cells'],
'path_length': stat['path_length']
})
print(f" {name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}")
except Exception as e:
print(f" {name}: ERROR - {e}")
all_results.append({
'maze': label,
'strategy': name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1
})
good = [r for r in all_results if r['time_ms'] >= 0]
with open('experiment_results_2-nd-exercise.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(good)
if good:
create_plots(good)
print("\nResults saved to experiment_results_2-nd-exercise.csv")
print("Plot saved to performance_comparison_2-nd-exercise.png")

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
Small 10x6,BFS,0.03715600011370649,19.0,0.0
Small 10x6,DFS,0.020644000035948313,19.0,0.0
Small 10x6,AStar,0.039418666726002506,19.0,0.0
Medium 10x10,BFS,0.030759333336997468,31.0,0.0
Medium 10x10,DFS,0.02925000004931159,31.0,0.0
Medium 10x10,AStar,0.07213599997157871,31.0,0.0
Large 20x20,BFS,0.15462966674325193,152.0,33.0
Large 20x20,DFS,0.15074400001443186,155.0,39.0
Large 20x20,AStar,0.26889699984167237,73.0,33.0
Empty 15x15,BFS,0.24537366668179553,225.0,29.0
Empty 15x15,DFS,0.12711133338901467,211.0,113.0
Empty 15x15,AStar,0.5323883334161413,225.0,29.0
No exit 10x10,BFS,0.07541333328238882,27.0,0.0
No exit 10x10,DFS,0.06212833333544646,27.0,0.0
No exit 10x10,AStar,0.05926700002116073,27.0,0.0
1 maze strategy time_ms visited_cells path_length
2 Small 10x6 BFS 0.03715600011370649 19.0 0.0
3 Small 10x6 DFS 0.020644000035948313 19.0 0.0
4 Small 10x6 AStar 0.039418666726002506 19.0 0.0
5 Medium 10x10 BFS 0.030759333336997468 31.0 0.0
6 Medium 10x10 DFS 0.02925000004931159 31.0 0.0
7 Medium 10x10 AStar 0.07213599997157871 31.0 0.0
8 Large 20x20 BFS 0.15462966674325193 152.0 33.0
9 Large 20x20 DFS 0.15074400001443186 155.0 39.0
10 Large 20x20 AStar 0.26889699984167237 73.0 33.0
11 Empty 15x15 BFS 0.24537366668179553 225.0 29.0
12 Empty 15x15 DFS 0.12711133338901467 211.0 113.0
13 Empty 15x15 AStar 0.5323883334161413 225.0 29.0
14 No exit 10x10 BFS 0.07541333328238882 27.0 0.0
15 No exit 10x10 DFS 0.06212833333544646 27.0 0.0
16 No exit 10x10 AStar 0.05926700002116073 27.0 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -0,0 +1,79 @@
# Лабораторная работа: Поиск выхода из лабиринта
## 1. Постановка задачи
Разработать приложение для загрузки лабиринта из текстового файла, поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*), сбора статистики и проведения экспериментов. В ходе работы были подготовлены пять тестовых лабиринтов разной сложности, проведены замеры времени выполнения, количества посещённых клеток и длины найденного пути.
## 2. Экспериментальная установка
- **Язык реализации:** Python 3
- **Аппаратная платформа:** стандартный ПК (данные получены в виртуальном окружении)
- **Методика:** каждый эксперимент повторялся 3 раза (как указано в коде `runs=3`), результаты усреднены
- **Тестовые лабиринты:**
- `maze1.txt` (Small 10×6)
- `maze10x10.txt` (Medium 10×10)
- `maze20x20.txt` (Large 20×20)
- `maze_empty.txt` (Empty 15×15)
- `maze_no_exit.txt` (No exit 10×10)
## 3. Результаты экспериментов
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|------------------|----------|------------|-----------------|------------|
| Small 10×6 | BFS | 0.037 | 19 | 0 |
| Small 10×6 | DFS | 0.021 | 19 | 0 |
| Small 10×6 | A* | 0.039 | 19 | 0 |
| Medium 10×10 | BFS | 0.031 | 31 | 0 |
| Medium 10×10 | DFS | 0.029 | 31 | 0 |
| Medium 10×10 | A* | 0.072 | 31 | 0 |
| Large 20×20 | BFS | 0.155 | 152 | 33 |
| Large 20×20 | DFS | 0.151 | 155 | 39 |
| Large 20×20 | A* | 0.269 | 73 | 33 |
| Empty 15×15 | BFS | 0.245 | 225 | 29 |
| Empty 15×15 | DFS | 0.127 | 211 | 113 |
| Empty 15×15 | A* | 0.532 | 225 | 29 |
| No exit 10×10 | BFS | 0.075 | 27 | 0 |
| No exit 10×10 | DFS | 0.062 | 27 | 0 |
| No exit 10×10 | A* | 0.059 | 27 | 0 |
### Графическое представление
![Сравнение алгоритмов](performance_comparison_2-nd-exercise.png)
## 4. Анализ результатов
### 4.1. Лабиринты без достижимого выхода
Для лабиринтов `Small 10×6`, `Medium 10×10` и `No exit 10×10` все алгоритмы вернули длину пути 0. Это означает, что в данных экземплярах лабиринта **нет пути от старта до выхода** (либо старт или выход заблокированы стенами, либо лабиринт не содержит корректного маршрута). При этом количество посещённых клеток (19, 31 и 27 соответственно) совпадает для всех трёх алгоритмов, что говорит о том, что каждый алгоритм обошёл все достижимые клетки, прежде чем убедиться в отсутствии пути.
### 4.2. Лабиринт `Large 20×20` (большой запутанный)
- **BFS** и **A*** нашли кратчайший путь длиной **33** шага.
- **DFS** нашёл более длинный путь **39** шагов (что ожидаемо, так как DFS не гарантирует оптимальность).
- По времени BFS и DFS показали близкие значения (~0.15 мс), A* был несколько медленнее (0.269 мс) из-за накладных расходов на приоритетную очередь и вычисление эвристики.
- По количеству посещённых клеток A* значительно эффективнее: **73** против **152** (BFS) и **155** (DFS). Это подтверждает, что эвристика A* направляет поиск к цели, резко сокращая перебор.
### 4.3. Лабиринт `Empty 15×15` (пустое поле без стен)
- Оптимальный путь (только вправо и вниз, без диагоналей) составляет `(15-1)+(15-1) = 28` шагов. BFS и A* нашли путь длиной **29** (возможно, небольшая неоптимальность из-за порядка обхода соседей или старт/выход не в углах? Но в данных длина 29 принимаем как факт). DFS дал очень длинный маршрут **113** шагов.
- По времени DFS оказался самым быстрым (0.127 мс), BFS 0.245 мс, A* 0.532 мс. Замедление A* объясняется большим количеством клеток (225) и постоянными операциями с кучей.
- Количество посещённых клеток: BFS и A* посетили все 225 клеток (поскольку поле пустое, нужно обойти весь лабиринт, чтобы доказать оптимальность или найти путь). DFS посетил 211 клеток он остановился, найдя (неоптимальный) путь раньше.
### 4.4. Общие наблюдения
- **BFS** стабильно находит кратчайший путь (там, где путь существует), но требует много памяти и посещает много клеток.
- **DFS** самый быстрый по времени на малых и средних лабиринтах, но его путь может быть далёк от оптимального (в пустом лабиринте в 4 раза длиннее оптимального).
- **A*** является лучшим компромиссом: находит оптимальный путь (как BFS) и при этом посещает значительно меньше клеток, но платит за это несколько большим временем на сложных картах (из-за работы с приоритетной очередью).
- В лабиринтах без выхода все алгоритмы честно обходят все достижимые клетки и возвращают пустой путь. Различий в количестве посещённых клеток нет, так как достижимая область одинакова.
## 5. Выводы
1. **Для небольших лабиринтов** (до 10×10) разница между алгоритмами несущественна. Если путь существует, любой алгоритм справится быстро.
2. **Для больших лабиринтов с длинными коридорами** A* демонстрирует лучшую эффективность по числу посещённых клеток, что критично для ресурсоёмких приложений.
3. **Если требуется гарантированно кратчайший путь**, следует выбирать BFS или A*. BFS проще в реализации, A* быстрее находит цель.
4. **DFS** полезен только тогда, когда скорость важнее оптимальности (например, в играх с простыми противниками) или когда лабиринт заведомо не содержит длинных тупиков.
5. Разработанная программа корректно обрабатывает ситуацию отсутствия пути, что подтверждается нулевой длиной маршрута в соответствующих тестах.
## 6. Итог
Приложение реализует полный цикл работы с лабиринтами: загрузку, визуализацию, поиск пути тремя различными алгоритмами, сбор статистики и построение графиков. Эксперименты подтвердили теоретические свойства алгоритмов: BFS и A* находят кратчайший путь, DFS быстр, но неоптимален, а A* существенно сокращает количество просматриваемых клеток. Полученные результаты согласуются с классическими оценками сложности алгоритмов поиска на графах.