Merge pull request '[2] task2' (#255) from VildyaevAV/2026-rff_mp:VildyaevAV-task2 into develop

Reviewed-on: #255
This commit is contained in:
VladimirGub 2026-05-30 11:55:15 +00:00
commit fca5e74d90
28 changed files with 826 additions and 0 deletions

BIN
VildyaevAV/docs/image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -0,0 +1,66 @@
from abc import ABC, abstractmethod
from cell import Cell
from maze import Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename):
with open(filename, "r", encoding="utf-8") as file:
lines = [line.rstrip("\n") for line in file]
height = len(lines)
width = len(lines[0])
cells = []
start = None
exit_cell = None
for y in range(height):
row = []
for x in range(width):
symbol = lines[y][x]
cell = Cell(x, y)
if symbol == "#":
cell.is_wall = True
elif symbol == "S":
cell.is_start = True
start = cell
elif symbol == "E":
cell.is_exit = True
exit_cell = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("Start cell S not found")
if exit_cell is None:
raise ValueError("Exit cell E not found")
return Maze(
cells,
width,
height,
start,
exit_cell
)

View File

@ -0,0 +1,11 @@
class Cell:
def __init__(self, x, y, is_wall=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = False
self.is_exit = False
def is_passable(self):
return not self.is_wall

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 874 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,112 @@
import csv
from builder import TextFileMazeBuilder
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
from solver import MazeSolver
from observer import ConsoleView
def print_path(maze, path):
path_coords = {(cell.x, cell.y) for cell in path}
for row in maze.cells:
line = ""
for cell in row:
if cell.is_wall:
line += "#"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif (cell.x, cell.y) in path_coords:
line += "*"
else:
line += " "
print(line)
builder = TextFileMazeBuilder()
maze_files = [
"docs/task2/mazes/small.txt",
"docs/task2/mazes/medium.txt",
"docs/task2/mazes/blocked.txt",
"docs/task2/mazes/large.txt",
"docs/task2/mazes/empty.txt",
"docs/task2/mazes/no_exit.txt"
]
strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
results = []
for maze_file in maze_files:
print("\n======================")
print("Maze:", maze_file)
try:
maze = builder.build_from_file(maze_file)
except Exception as e:
print("Error:", e)
continue
for strategy in strategies:
print("\nStrategy:", strategy.__class__.__name__)
solver = MazeSolver(maze, strategy)
observer = ConsoleView()
solver.add_observer(observer)
runs = 5
total_time = 0
last_stats = None
last_path = []
for _ in range(runs):
stats, path = solver.solve()
total_time += stats.time_ms
last_stats = stats
last_path = path
average_time = total_time / runs
print("Average time ms:", round(average_time, 4))
print("Visited:", last_stats.visited_cells)
print("Path length:", last_stats.path_length)
if last_path:
print_path(maze, last_path)
else:
print("Path not found")
results.append([
maze_file,
strategy.__class__.__name__,
round(average_time, 4),
last_stats.visited_cells,
last_stats.path_length
])
with open("maze_results.csv", "w", newline="", encoding="utf-8") as file:
writer = csv.writer(file)
writer.writerow([
"maze",
"strategy",
"time_ms",
"visited_cells",
"path_length"
])
writer.writerows(results)
print("\nResults saved to maze_results.csv")

View File

@ -0,0 +1,35 @@
class Maze:
def __init__(self, cells, width, height, start, exit_cell):
self.cells = cells
self.width = width
self.height = height
self.start = start
self.exit = exit_cell
def get_cell(self, x, y):
if 0 <= y < self.height and 0 <= x < self.width:
return self.cells[y][x]
return None
def get_neighbors(self, cell):
directions = [
(0, -1),
(0, 1),
(-1, 0),
(1, 0)
]
neighbors = []
for dx, dy in directions:
nx = cell.x + dx
ny = cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
docs/task2/mazes/small.txt,BFSStrategy,0.0228,10,7
docs/task2/mazes/small.txt,DFSStrategy,0.0163,10,7
docs/task2/mazes/small.txt,AStarStrategy,0.0252,10,7
docs/task2/mazes/medium.txt,BFSStrategy,0.0238,18,0
docs/task2/mazes/medium.txt,DFSStrategy,0.0257,18,0
docs/task2/mazes/medium.txt,AStarStrategy,0.0344,18,0
docs/task2/mazes/blocked.txt,BFSStrategy,0.0085,3,0
docs/task2/mazes/blocked.txt,DFSStrategy,0.006,3,0
docs/task2/mazes/blocked.txt,AStarStrategy,0.0059,3,0
docs/task2/mazes/large.txt,BFSStrategy,0.0558,45,0
docs/task2/mazes/large.txt,DFSStrategy,0.0522,45,0
docs/task2/mazes/large.txt,AStarStrategy,0.0757,45,0
docs/task2/mazes/empty.txt,BFSStrategy,0.0708,56,14
docs/task2/mazes/empty.txt,DFSStrategy,0.039,49,28
docs/task2/mazes/empty.txt,AStarStrategy,0.1058,56,14
1 maze strategy time_ms visited_cells path_length
2 docs/task2/mazes/small.txt BFSStrategy 0.0228 10 7
3 docs/task2/mazes/small.txt DFSStrategy 0.0163 10 7
4 docs/task2/mazes/small.txt AStarStrategy 0.0252 10 7
5 docs/task2/mazes/medium.txt BFSStrategy 0.0238 18 0
6 docs/task2/mazes/medium.txt DFSStrategy 0.0257 18 0
7 docs/task2/mazes/medium.txt AStarStrategy 0.0344 18 0
8 docs/task2/mazes/blocked.txt BFSStrategy 0.0085 3 0
9 docs/task2/mazes/blocked.txt DFSStrategy 0.006 3 0
10 docs/task2/mazes/blocked.txt AStarStrategy 0.0059 3 0
11 docs/task2/mazes/large.txt BFSStrategy 0.0558 45 0
12 docs/task2/mazes/large.txt DFSStrategy 0.0522 45 0
13 docs/task2/mazes/large.txt AStarStrategy 0.0757 45 0
14 docs/task2/mazes/empty.txt BFSStrategy 0.0708 56 14
15 docs/task2/mazes/empty.txt DFSStrategy 0.039 49 28
16 docs/task2/mazes/empty.txt AStarStrategy 0.1058 56 14

View File

@ -0,0 +1,5 @@
#######
#S# #E#
# # # #
# ### #
#######

View File

@ -0,0 +1,9 @@
##########
#S #
# #
# #
# #
# #
# #
# E#
##########

View File

@ -0,0 +1,11 @@
####################
#S # # #
##### ### ### ### ##
# # # # #
# ### # ##### ######
# # # # #
# # ####### ###### #
# # # #
# ####### ######## #
# # #E#
####################

View File

@ -0,0 +1,7 @@
############
#S # #
### ###### #
# # #
# #### # ###
# # # E#
############

View File

@ -0,0 +1,5 @@
#######
#S# #
# ### #
# #
#######

View File

@ -0,0 +1,5 @@
#######
#S ##
# ### #
# E#
#######

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -0,0 +1,14 @@
from abc import ABC, abstractmethod
class Observer(ABC):
@abstractmethod
def update(self, event):
pass
class ConsoleView(Observer):
def update(self, event):
print(f"[Observer] {event}")

View File

@ -0,0 +1,50 @@
import pandas as pd
import matplotlib.pyplot as plt
df = pd.read_csv("maze_results.csv")
mazes = df["maze"].unique()
for maze in mazes:
maze_df = df[df["maze"] == maze]
plt.figure(figsize=(8, 5))
plt.bar(
maze_df["strategy"],
maze_df["time_ms"]
)
plt.title(f"Time for {maze}")
plt.ylabel("Time ms")
plt.xlabel("Strategy")
plt.tight_layout()
filename = maze.split("/")[-1].replace(".txt", "_time.png")
plt.savefig(filename)
plt.close()
for maze in mazes:
maze_df = df[df["maze"] == maze]
plt.figure(figsize=(8, 5))
plt.bar(
maze_df["strategy"],
maze_df["visited_cells"]
)
plt.title(f"Visited cells for {maze}")
plt.ylabel("Visited cells")
plt.xlabel("Strategy")
plt.tight_layout()
filename = maze.split("/")[-1].replace(".txt", "_visited.png")
plt.savefig(filename)
plt.close()
print("Graphs created")

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,50 @@
import time
class SearchStats:
def __init__(self, time_ms, visited_cells, path_length):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
class MazeSolver:
def __init__(self, maze, strategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def set_strategy(self, strategy):
self.strategy = strategy
def add_observer(self, observer):
self.observers.append(observer)
def notify(self, event):
for observer in self.observers:
observer.update(event)
def solve(self):
self.notify("search_started")
start_time = time.perf_counter()
path, visited_cells = self.strategy.find_path(
self.maze,
self.maze.start,
self.maze.exit
)
end_time = time.perf_counter()
self.notify("search_finished")
time_ms = (end_time - start_time) * 1000
stats = SearchStats(
time_ms=time_ms,
visited_cells=visited_cells,
path_length=len(path)
)
return stats, path

View File

@ -0,0 +1,187 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze, start, exit_cell):
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
queue = deque([start])
visited = set()
visited.add((start.x, start.y))
parent = {}
while queue:
current = queue.popleft()
if current == exit_cell:
return self.restore_path(parent, start, exit_cell), len(visited)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
if key not in visited:
visited.add(key)
parent[key] = current
queue.append(neighbor)
return [], len(visited)
def restore_path(self, parent, start, exit_cell):
path = []
current = exit_cell
while current != start:
path.append(current)
current = parent[(current.x, current.y)]
path.append(start)
path.reverse()
return path
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit_cell):
stack = [start]
visited = set()
visited.add((start.x, start.y))
parent = {}
while stack:
current = stack.pop()
if current == exit_cell:
return self.restore_path(parent, start, exit_cell), len(visited)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
if key not in visited:
visited.add(key)
parent[key] = current
stack.append(neighbor)
return [], len(visited)
def restore_path(self, parent, start, exit_cell):
path = []
current = exit_cell
while current != start:
path.append(current)
current = parent[(current.x, current.y)]
path.append(start)
path.reverse()
return path
class AStarStrategy(PathFindingStrategy):
def heuristic(self, cell, exit_cell):
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze, start, exit_cell):
heap = []
heapq.heappush(heap, (0, start.x, start.y, start))
visited = set()
parent = {}
g_score = {
(start.x, start.y): 0
}
while heap:
_, _, _, current = heapq.heappop(heap)
key_current = (current.x, current.y)
if key_current in visited:
continue
visited.add(key_current)
if current == exit_cell:
return self.restore_path(parent, start, exit_cell), len(visited)
for neighbor in maze.get_neighbors(current):
key = (neighbor.x, neighbor.y)
tentative = g_score[key_current] + 1
if key not in g_score or tentative < g_score[key]:
g_score[key] = tentative
priority = tentative + self.heuristic(neighbor, exit_cell)
heapq.heappush(
heap,
(priority, neighbor.x, neighbor.y, neighbor)
)
parent[key] = current
return [], len(visited)
def restore_path(self, parent, start, exit_cell):
path = []
current = exit_cell
while current != start:
path.append(current)
current = parent[(current.x, current.y)]
path.append(start)
path.reverse()
return path

View File

@ -0,0 +1,243 @@
# Отчет по заданию 2
## Тема
Реализация поиска пути в лабиринте с использованием паттернов проектирования и различных алгоритмов поиска.
---
# Цель работы
Изучить применение ООП и паттернов проектирования при реализации алгоритмов поиска пути в лабиринте.
Реализовать:
- BFS
- DFS
- A*
Сравнить эффективность алгоритмов по:
- времени выполнения
- количеству посещённых клеток
- длине найденного пути
---
# Используемые паттерны
## Builder
Используется для загрузки лабиринта из файла.
## Strategy
Используется для переключения алгоритмов поиска пути.
## Observer
Используется для уведомлений о начале и окончании поиска.
---
# Структура проекта
```text
docs/task2/
├── mazes/
│ ├── small.txt
│ ├── medium.txt
│ ├── blocked.txt
│ ├── no_exit.txt
│ ├── large.txt
│ └── empty.txt
├── cell.py
├── maze.py
├── builder.py
├── strategies.py
├── solver.py
├── observer.py
├── main.py
├── plot_results.py
├── maze_results.csv
└── task2_report.md
```
---
# UML диаграмма
В проекте была построена UML-диаграмма классов с использованием Mermaid.
![alt text](image.png)
---
# Описание классов
## Cell
Класс клетки лабиринта.
Хранит:
- координаты
- тип клетки
- признаки стены, старта и выхода
---
## Maze
Класс лабиринта.
Содержит:
- двумерный массив клеток
- размеры лабиринта
- стартовую клетку
- выход
---
## TextFileMazeBuilder
Загружает лабиринт из текстового файла.
---
## BFSStrategy
Алгоритм поиска в ширину.
Находит кратчайший путь.
---
## DFSStrategy
Алгоритм поиска в глубину.
Работает быстро, но путь может быть не кратчайшим.
---
## AStarStrategy
Эвристический алгоритм поиска.
Использует манхэттенское расстояние.
---
## MazeSolver
Основной класс-оркестратор.
Запускает алгоритм поиска и собирает статистику.
---
# Результаты экспериментов
Результаты сохраняются в файл:
```text
maze_results.csv
```
Проводилось сравнение:
- времени работы
- количества посещённых клеток
- длины пути
## Таблица результатов
| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |
|---|---|---|---|---|
| small | BFS | 0.0396 | 10 | 7 |
| small | DFS | 0.0251 | 10 | 7 |
| small | A* | 0.0359 | 10 | 7 |
| medium | BFS | 0.0312 | 18 | 0 |
| medium | DFS | 0.0277 | 18 | 0 |
| medium | A* | 0.0359 | 18 | 0 |
| blocked | BFS | 0.0123 | 3 | 0 |
| blocked | DFS | 0.0089 | 3 | 0 |
| blocked | A* | 0.0133 | 3 | 0 |
| large | BFS | 0.0602 | 45 | 0 |
| large | DFS | 0.0509 | 45 | 0 |
| large | A* | 0.0682 | 45 | 0 |
| empty | BFS | 0.0711 | 56 | 14 |
| empty | DFS | 0.0419 | 49 | 28 |
| empty | A* | 0.1144 | 56 | 14 |
---
## Графики
### Время выполнения
![blocked](blocked_time.png)
![small](small_time.png)
![medium](medium_time.png)
![large](large_time.png)
![empty](empty_time.png)
---
### Количество посещённых клеток
![blocked](blocked_visited.png)
![small](small_visited.png)
![medium](medium_visited.png)
![large](large_visited.png)
![empty](empty_visited.png)
---
# Графики
Построены графики:
- времени выполнения
- количества посещённых клеток
Для каждого лабиринта.
---
# Анализ эффективности алгоритмов
В ходе экспериментов были сравнены алгоритмы BFS, DFS и A* на лабиринтах различной сложности.
## BFS
Алгоритм BFS гарантированно находит кратчайший путь, однако может посещать большое количество клеток. На больших лабиринтах время работы увеличивается.
## DFS
DFS работает быстрее других алгоритмов, так как уходит в глубину и не исследует все возможные пути. Однако найденный путь может быть не кратчайшим.
## A*
Алгоритм A* использует эвристику и старается двигаться к выходу наиболее оптимальным образом. На простых лабиринтах показывает хорошие результаты, однако на некоторых картах из-за вычисления эвристики работает медленнее DFS.
## Вывод по экспериментам
- DFS показал наименьшее время выполнения.
- BFS обеспечивает поиск кратчайшего пути.
- A* хорошо подходит для сложных лабиринтов с большим количеством вариантов движения.
- На лабиринтах без выхода все алгоритмы посещают примерно одинаковое количество клеток.
# Выводы
В ходе работы была реализована система поиска пути в лабиринте с использованием объектно-ориентированного подхода и паттернов проектирования.
Были использованы паттерны:
- Builder — для загрузки лабиринта из файла.
- Strategy — для переключения алгоритмов поиска пути.
- Observer — для уведомлений о событиях поиска.
Использование паттернов позволило сделать архитектуру гибкой и расширяемой.
Например:
- можно легко добавить новый алгоритм поиска пути;
- можно реализовать другой способ загрузки лабиринта;
- можно подключить новые способы отображения информации.
Без применения паттернов код был бы более связанным и сложным для расширения и поддержки.