[1, 2] ProninVV #242

Merged
AndreyUrs merged 24 commits from ProninVV/2026-rff_mp:ProninVV into develop 2026-05-30 11:23:50 +00:00
19 changed files with 5157 additions and 0 deletions
Showing only changes of commit 303bcf3eae - Show all commits

View File

@ -0,0 +1,41 @@
from Maze import Cell, Maze
from strategy import PathFindingStrategy
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
def heuristik(cell):
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
parents = {start: None}
queue = [start]
if not start or not exit:
return [], 0
while len(queue) != 0:
best_cell = queue[0]
for cell in queue:
if heuristik(cell) < heuristik(best_cell):
best_cell = cell
u = best_cell
queue.remove(u)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
childs = maze.getNeighbors(u)
for child in childs:
if child not in parents:
parents[child] = u
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,32 @@
from strategy import PathFindingStrategy
from Maze import Maze, Cell
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start: Cell, exit: Cell):
# очерель: перывй вошел - первый вышел
queue = [start]
# будем хранить откуда в какую клетку пришли
parents = {start: None}
if not start or not exit:
return [], 0
while (len(queue) != 0):
u = queue.pop(0)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
childs = maze.getNeighbors(u)
for child in childs:
if child not in parents:
parents[child] = u
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,45 @@
from abc import ABC, abstractmethod
class Player:
def __init__(self, start_cell):
self.current_cell = start_cell
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
"""Выполняет действие. Возвращает True, если ход успешен."""
pass
@abstractmethod
def undo(self) -> None:
"""Откатывает действие назад."""
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze, dx: int, dy: int):
self.player = player
self.maze = maze
self.dx = dx
self.dy = dy
self.previous_cell = None
def execute(self) -> bool:
new_x = self.player.current_cell.x + self.dx
new_y = self.player.current_cell.y + self.dy
next_cell = self.maze.getCell(new_x, new_y)
if next_cell and next_cell.isPassable():
self.previous_cell = self.player.current_cell
self.player.current_cell = next_cell
return True
print("Ошибка: Там стена или край лабиринта!")
return False
def undo(self) -> None:
if self.previous_cell:
self.player.current_cell = self.previous_cell

View File

@ -0,0 +1,40 @@
from Observer import Observer, Event
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.type == "maze_loaded":
print("\n[Система] Лабиринт успешно загружен!")
self.render(event.data.get("maze"))
elif event.type == "path_found":
print("\n[Система] Алгоритм нашёл решение!")
self.render(event.data.get("maze"), path=event.data.get("path"))
elif event.type == "move":
print(
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
self.render(event.data.get("maze"),
player_position=event.data.get("player_pos"))
def render(self, maze, player_position=None, path=None) -> None:
path_set = set(path) if path else set()
for y in range(maze.height):
row_chars = []
for x in range(maze.width):
cell = maze.getCell(x, y)
if player_position and cell == player_position:
row_chars.append("P")
elif cell.isStart:
row_chars.append("S")
elif cell.isExit:
row_chars.append("E")
elif cell in path_set:
row_chars.append(".")
elif cell.isWall:
row_chars.append("#")
else:
row_chars.append(" ")
print("".join(row_chars))

View File

@ -0,0 +1,43 @@
from strategy import PathFindingStrategy
from Maze import Maze, Cell
class DeikstraFind(PathFindingStrategy):
def findPath(self, maze, start, exit):
if not start or not exit:
return [], len(parents)
queue = [start]
distances = {start: 0}
parents = {start: None}
while len(queue) != 0:
best_cell = queue[0]
for cell in queue:
if distances[cell] < distances[best_cell]:
best_cell = cell
u = best_cell
queue.remove(u)
if u == exit:
path = []
current = exit
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path, len(parents)
for child in maze.getNeighbors(u):
distance_through_u = distances[u] + 1
if distance_through_u < distances.get(child, float('inf')):
distances[child] = distance_through_u
parents[child] = u
if child not in queue:
queue.append(child)
return [], len(parents)

View File

@ -0,0 +1,40 @@
import sys
from strategy import PathFindingStrategy
from Maze import Maze, Cell
sys.setrecursionlimit(15000)
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze: Maze, start, exit):
if not start or not exit:
return [], 0
visited = set()
path = []
count_cell = 0
def dfs(root: Cell) -> bool:
visited.add(root)
path.append(root)
# count_cell += 1
if root == exit:
return True
neighbors = maze.getNeighbors(root)
for neighbor in neighbors:
if neighbor not in visited:
if dfs(neighbor):
return True
path.pop()
return False
if dfs(start):
return path, len(visited)
return [], len(visited)

View File

@ -0,0 +1,49 @@
# модель клетки лабиринта
class Cell:
def __init__(self, x, y, isWall=False, isStart=False, isExit=False):
self.x = x
self.y = y
self.isWall = isWall
self.isStart = isStart
self.isExit = isExit
def isPassable(self):
return not self.isWall
# модель лабиринта
class Maze:
def __init__(self, height, width, start=None, exit=None):
self.height = height # строки
self.width = width # столбцы
self.__grid = [[Cell(x, y) for x in range(width)]
for y in range(height)]
self.start = start
self.exit = exit
def getCell(self, x, y) -> Cell:
if (0 <= x < self.width) and (0 <= y < self.height):
return self.__grid[y][x]
return None
def getNeighbors(self, cell):
dirs = {'left': (-1, 0), 'right': (1, 0),
'up': (0, 1), 'down': (0, -1)}
neighbors = []
for _, val in dirs.items():
dx, dy = val
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.getCell(nx, ny)
if neighbor and isinstance(neighbor, Cell) and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
if __name__ == "__main__":
maze1 = Maze(height=5, width=5, start=0, exit=4)
cell1 = maze1.getCell(2, 2)
print(maze1.getNeighbors(cell1))

View File

@ -0,0 +1,47 @@
from abc import ABC, abstractmethod
from Maze import Maze, Cell
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def __init__(self):
self._maze = None
@property
def maze(self):
return self._maze
def buildFromFile(self, filename: str):
with open(filename, mode='r', encoding='utf-8') as file:
lines = file.read().splitlines()
height = len(lines)
width = len(lines[0])
self._maze = Maze(height, width)
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = self._maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == 'S':
cell.isStart = True
self._maze.start = cell
elif char == 'E':
cell.isExit = True
self._maze.exit = cell
self._validate()
return self._maze
def _validate(self):
if self._maze.start is None:
raise "в лабиринте нет старта"
if self._maze.exit is None:
raise "в лабиринте нет начала"

View File

@ -0,0 +1,57 @@
import time
from Maze import Maze
from strategy import PathFindingStrategy
class SearchStats:
def __init__(self, execution_time, visited_count, path_length, path):
self.execution_time = execution_time
self.visited_count = visited_count
self.path_length = path_length
self.path = path
def __str__(self):
return ("f == Статистика поиска == =\n"
f"Время выполнения: {self.execution_time_ms:.4f} мс\n"
f"Посещено клеток: {self.visited_count}\n"
f"Длина пути: {self.path_length} клеток\n")
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self._maze = maze
self._strategy = strategy
self._observers = []
def addObserver(self, observer):
"""Регистрация нового наблюдателя (например, ConsoleView)"""
self._observers.append(observer)
def notify(self, event):
"""Уведомление всех подписчиков о событии"""
for observer in self._observers:
observer.update(event)
def setStrategy(self, strategy):
self._strategy = strategy
def solve(self):
if not self._maze or not self._strategy:
raise ValueError("Не задан лабиринт или стратегия поиска!")
start_time = time.perf_counter()
path, visited_count = self._strategy.findPath(
self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
path_length = len(path)
from ConsoleView import Event
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
return SearchStats(execution_time_ms, visited_count, path_length, path)

View File

@ -0,0 +1,13 @@
from abc import ABC, abstractmethod
class Event:
def __init__(self, type: str, data: dict = None):
self.type = type # "maze_loaded", "move", "path_found"
self.data = data if data else {}
class Observer(ABC):
@abstractmethod
def update(self, event: Event) -> None:
pass

View File

@ -0,0 +1,8 @@
from MazeBuilder import TextFileMazeBuilder
from BreadthFirstSearch import BFSStrategy
from Maze import Maze
maze1 = TextFileMazeBuilder().buildFromFile("text.txt")
pathh = BFSStrategy.findPath(maze1, maze1.start, maze1.exit)
print(pathh)

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -0,0 +1,302 @@
\input{preambule.tex}
\begin{document}
\thispagestyle{empty}
\centerline{МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РФ}
\centerline{НАЦИОНАЛЬНЫЙ ИССЛЕДОВАТЕЛЬСКИЙ НИЖЕГОРОДСКИЙ}
\centerline{ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ ИМ Н. И. ЛОБАЧЕВСКОГО}
\centerline{Радиофизический факультет}
\vfill
\centerline{\Large{Отчет к лабораторной работе}}
\centerline{\large{по Методам программирования}}
\centerline{\Large{Поиск выхода из лабиринта }}
\centerline{\Large{(объектно-ориентированная реализация с паттернами)}}
\vfill
Студент группы 427 \hfill Пронин Владислав Владимирович
Преподаватель \hfill Морозов Н. С.
\vfill
\centerline{Н. Новгород, 2026}
\clearpage
\newpage
\tableofcontents
\newpage
\section{Цель работы}
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
\section{Описание задачи и выбранных паттернов}
Используемые Паттерны:
\begin{itemize}
\item Strategy (Стратегия) \textemdash \ это поведенческий паттерн проектирования, который определяет семейство схожих алгоритмов и помещает каждый из них в собственный класс, после чего алгоритмы можно взаимозаменять прямо во время исполнения программы. Выбран, так как в данной лабораторной работе используются несколько алгоритмов, выполняющих одно и то же действие \ \textemdash \ обход графа.
\item Builder (строитель) \textemdash \ абстрактный класс/интерфейс, который определяет все этапы, необходимые для производства сложного объекта-продукта. Позволяет отделить построение сложного объекта от его представления, создает сложные объекты, используя простые объекты и поэтапный подход. Выбран для изоляции сложного процесса парсинга текстовоо файла.
\item Observer (Наблюдатель) \ \textemdash \ это поведенческий паттерн проектирования, который создаёт механизм подписки, позволяющий одним объектам следить и реагировать на события, происходящие в других объектах. Выбран для отделения логики приложения от вывода на экран (принцип MVC). Класс ConsoleView подписывается на события GameController и перерисовывает карту только тогда, когда игрок перемещается или путь найден.
\end{itemize}
\section{Диаграмма классов}
\begin{figure}[H]
\centering
\includegraphics[scale=0.06]{plan.png}
\end{figure}
\section{Листиги Классов}
\subsection{Maze Solver}
\begin{lstlisting}
import time
from Maze import Maze
from strategy import PathFindingStrategy
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self._maze = maze
self._strategy = strategy
self._observers = []
def addObserver(self, observer):
"""Регистрация нового наблюдателя (например, ConsoleView)"""
self._observers.append(observer)
def notify(self, event):
"""Уведомление всех подписчиков о событии"""
for observer in self._observers:
observer.update(event)
def setStrategy(self, strategy):
self._strategy = strategy
def solve(self):
if not self._maze or not self._strategy:
raise ValueError("Не задан лабиринт или стратегия поиска!")
start_time = time.perf_counter()
path, visited_count = self._strategy.findPath(
self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
path_length = len(path)
from ConsoleView import Event
self.notify(Event("path_found", {"maze": self._maze, "path": path}))
return SearchStats(execution_time_ms, visited_count, path_length, path)
\end{lstlisting}
\subsection{Maze Builder}
\begin{lstlisting}
from abc import ABC, abstractmethod
from Maze import Maze, Cell
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def __init__(self):
self._maze = None
@property
def maze(self):
return self._maze
def buildFromFile(self, filename: str):
with open(filename, mode='r', encoding='utf-8') as file:
lines = file.read().splitlines()
height = len(lines)
width = len(lines[0])
self._maze = Maze(height, width)
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = self._maze.getCell(x, y)
if char == '#':
cell.isWall = True
elif char == 'S':
cell.isStart = True
self._maze.start = cell
elif char == 'E':
cell.isExit = True
self._maze.exit = cell
self._validate()
return self._maze
def _validate(self):
if self._maze.start is None:
raise "в лабиринте нет старта"
if self._maze.exit is None:
raise "в лабиринте нет начала"
\end{lstlisting}
\subsection{OBserver}
\begin{lstlisting}
from Observer import Observer, Event
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.type == "maze_loaded":
print("\n[Система] Лабиринт успешно загружен!")
self.render(event.data.get("maze"))
elif event.type == "path_found":
print("\n[Система] Алгоритм нашёл решение!")
self.render(event.data.get("maze"), path=event.data.get("path"))
elif event.type == "move":
print(
f"\n[Игрок] Переместился в точку: ({event.data.get('player_pos').x}, {event.data.get('player_pos').y})")
self.render(event.data.get("maze"),
player_position=event.data.get("player_pos"))
def render(self, maze, player_position=None, path=None) -> None:
path_set = set(path) if path else set()
for y in range(maze.height):
row_chars = []
for x in range(maze.width):
cell = maze.getCell(x, y)
if player_position and cell == player_position:
row_chars.append("P")
elif cell.isStart:
row_chars.append("S")
elif cell.isExit:
row_chars.append("E")
elif cell in path_set:
row_chars.append(".")
elif cell.isWall:
row_chars.append("#")
else:
row_chars.append(" ")
print("".join(row_chars))
\end{lstlisting}
\section{Результаты}
Таблицы замеров времени и посещенных клеток:
\begin{table}[H]
\centering
\caption{Результаты экспериментального сравнения алгоритмов поиска пути}
\label{tab:maze_benchmark}
\begin{tabular}{llccc}
\toprule
\textbf{Лабиринт} & \textbf{Стратегия} & \textbf{Время (мс)} & \textbf{Посещено клеток} & \textbf{Длина пути} \\
\midrule
\multirow{4}{*}{Маленький (10×10)}
& BFS & 0.0516 & 28 & 15 \\
& DFS & 0.0275 & 15 & 15 \\
& A* & 0.0360 & 16 & 15 \\
& Дейкстра & 0.0722 & 28 & 15 \\
\midrule
\multirow{4}{*}{Пустой (30×30)}
& BFS & 1.1863 & 870 & 58 \\
& DFS & 1.5568 & 842 & 842 \\
& A* & 0.4405 & 113 & 58 \\
& Дейкстра & 2.8607 & 870 & 58 \\
\midrule
\multirow{4}{*}{Без выхода (15×15)}
& BFS & 0.2230 & 160 & 0 \\
& DFS & 0.2959 & 160 & 0 \\
& A* & 0.9378 & 160 & 0 \\
& Дейкстра & 0.4148 & 160 & 0 \\
\midrule
\multirow{4}{*}{Средний (50×50)}
& BFS & 3.2247 & 1779 & 95 \\
& DFS & 1.6985 & 873 & 873 \\
& A* & 0.7348 & 158 & 95 \\
& Дейкстра & 6.1264 & 1779 & 95 \\
\midrule
\multirow{4}{*}{Большой (100×100)}
& BFS & 10.1308 & 7320 & 195 \\
& DFS & 6.1878 & 3549 & 3549 \\
& A* & 2.8441 & 328 & 195 \\
& Дейкстра & 35.2250 & 7320 & 195 \\
\bottomrule
\end{tabular}
\end{table}
Графики:
\begin{figure}[H]
\includegraphics[scale=0.6]{time.eps}
\end{figure}
\begin{figure}[H]
\includegraphics[scale=0.6]{cells.eps}
\end{figure}
\section{Анализ эффективности}
Так как в нашем лабиринте вес всех ребер равны 1, то Дейкстра выродился в Поиск в ширину. Также Дейкстра несколько медленнее из за дополнительных расчетов на сортировку стоимостей.
Самым лучшим по скорости стал алгоритм А*. Он в среднем 3-4 раза быстрее поиска в ширину, так как на каждом шаге он выбирает самого оптимального соседа для каждого узла, а поиск в ширину проверяет всех соседей.
В разработанной рекурсивной стратегии DFS метрика посещенных клеток совпадает с длиной пути, так как алгоритм фиксирует состояние успешно развернутого стека вызовов в момент достижения целевой точки. Все тупиковые ветви, из которых рекурсия вышла до момента нахождения exit, отсекаются архитектурой возврата флага True, что демонстрирует специфику работы рекурсивного бэктрекинга в Python
\section{Выводы по ООП}
В ходе выполнения лабораторной работы была спроектирована и реализована объектно-ориентированная система поиска пути в лабиринтах. Применение принципов ООП и паттернов проектирования GoF позволило полностью разделить зоны ответственности классов (принцип Single Responsibility) и обеспечить высокий уровень гибкости и расширяемости приложения.
1. Как паттерны помогли сделать код гибким и расширяемым
\begin{itemize}
\item Разделение логики построения и представления (Паттерн Builder):
Процесс создания лабиринта инкапсулирован внутри класса TextFileMazeBuilder. Сам лабиринт (Maze) и алгоритмы поиска никак не завязаны на формат хранения данных. Если в будущем потребуется сменить текстовый формат .txt на структуру .json достаточно будет создать нового строителя, реализующего интерфейс MazeBuilder.
\item Изоляция и динамическая смена алгоритмов (Паттерн Strategy):
Каждый алгоритм обхода графа вынесен в отдельный класс-стратегию с единым интерфейсом PathfindingStrategy. Класс-оркестратор MazeSolver работает исключительно с абстракцией.
\item Использование Observer позволило отделить вычислительную составляющую от графической. Maze SOlver никак не учитывает где и как будут отображаться данные, он только отдает сигнал о событиях. Это позволяет если нужно изменить графический инт6ерфейс.
\end{itemize}
\end{document}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,83 @@
%\documentclass[a4paper, 12pt]{article}
\documentclass[a4paper, 14pt]{extarticle}
\usepackage[english, russian]{babel}
\usepackage[T2A]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{comment}
\usepackage{fontspec}
\setmainfont{Times New Roman}
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage{geometry}
\usepackage{titleps}
\usepackage{graphicx}
\DeclareGraphicsExtensions{.pdf, .jpg}
\usepackage{wrapfig}
\usepackage{indentfirst}
\geometry{top=20mm}
\geometry{bottom=25mm}
\geometry{left=30mm}
\geometry{right=10mm}
\usepackage{float}
\usepackage{wrapfig}
\newpagestyle{main}{
\setheadrule{0.4pt}
\sethead{ННГУ им Н.И. Лобачесвкого}{}{В. В. Пронин}
\setfoot{}{\thepage}{}
}
\pagestyle{main}
%\setcounter{page}{2}
\linespread{1.5}
\setlength{\parindent}{10mm}
\setlength{\parskip}{1ex}
\usepackage{listings}
\usepackage{xcolor}
% Настройка цветов для аккуратного кода
\definecolor{codegreen}{rgb}{0,0.5,0}
\definecolor{codegray}{rgb}{0.5,0.5,0.5}
\definecolor{codepurple}{rgb}{0.58,0,0.82}
\definecolor{backcolour}{rgb}{0.97,0.97,0.96}
\lstset{
backgroundcolor=\color{backcolour},
commentstyle=\color{codegreen},
keywordstyle=\color{blue}\bfseries,
numberstyle=\tiny\color{codegray},
stringstyle=\color{codepurple},
basicstyle=\ttfamily\small, % Моноширинный аккуратный шрифт
breakatwhitespace=false,
breaklines=true, % Автоперенос длинных строк
captionpos=b, % Подпись снизу
keepspaces=true,
numbers=left, % Нумерация строк слева
numbersep=8pt,
showspaces=false,
showstringspaces=false,
showtabs=false,
tabsize=4,
language=Python,
frame=single, % Тонкая рамка вокруг кода
rulecolor=\color{lightgray}
}
\usepackage{booktabs} % Для красивых горизонтальных линий
\usepackage{multirow} % Для объединения строк по вертикали
\usepackage{float} % Для точного позиционирования таблицы [H]

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,12 @@
from abc import ABC, abstractmethod
from typing import List
from Maze import Maze, Cell
# интерфейс стратегий
class PathFindingStrategy(ABC):
@abstractmethod
def findPath(maze: Maze, start, exit) -> List[Cell]:
""" возвращает список клеток пути (от старта до выхода включительно) или пустой список, если пути нет """
pass

134
ProninVV/task-2-oop/test.py Normal file
View File

@ -0,0 +1,134 @@
import csv
import time
import os
import matplotlib.pyplot as plt
import numpy as np
from MazeBuilder import TextFileMazeBuilder
from MazeSolver import MazeSolver, SearchStats
from DepthFirstSearch import DFSStrategy
from BreadthFirstSearch import BFSStrategy
from Deikstra import DeikstraFind
from AStarStrategy import AStarStrategy
from ConsoleView import ConsoleView
def run_benchmarks():
files = ["mazes/maze_small.txt", "mazes/maze_empty.txt",
"mazes/maze_no_exit.txt", "mazes/maze_medium.txt", "mazes/maze_large.txt"]
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"A*": AStarStrategy(),
"Deikstra": DeikstraFind()
}
view = ConsoleView()
NUM_RUNS = 5
results = []
print("Запуск экспериментов...")
for file in files:
if not os.path.exists(file):
print(f"Файл {file} не найден. Пропуск.")
continue
for name, strategy in strategies.items():
total_time = 0.0
visited_counts = []
path_lengths = []
print(f" работает {name}")
for _ in range(NUM_RUNS):
# Пересоздаем лабиринт
builder = TextFileMazeBuilder()
builder.buildFromFile(file)
maze = builder.maze
solver = MazeSolver(maze, strategy)
solver.addObserver(view)
stats = solver.solve()
total_time += stats.execution_time
visited_counts.append(stats.visited_count)
path_lengths.append(stats.path_length)
# средние значения
avg_time = total_time / NUM_RUNS
avg_visited = int(np.mean(visited_counts))
avg_path = int(np.mean(path_lengths))
results.append({
"лабиринт": file,
"стратегия": name,
"время_мс": round(avg_time, 4),
"посещено_клеток": avg_visited,
"длина_пути": avg_path
})
# Запись в CSV
csv_file = "results/maze_benchmark_results.csv"
with open(csv_file, mode="w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=[
"лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
writer.writeheader()
writer.writerows(results)
print(f"Результаты успешно сохранены в {csv_file}")
return results
# Построение графиков
def plot_results(results):
print("Генерация графиков...")
mazes = sorted(list(set(r["лабиринт"] for r in results)))
strategies = ["BFS", "DFS", "A*"]
# График Количество посещенных клеток
fig, ax = plt.subplots(figsize=(10, 6))
x = np.arange(len(mazes))
width = 0.25
for i, strat in enumerate(strategies):
visited = [next(r["посещено_клеток"] for r in results if r["лабиринт"]
== m and r["стратегия"] == strat) for m in mazes]
ax.bar(x + i*width, visited, width, label=strat)
ax.set_ylabel('Количество посещенных клеток')
ax.set_title('Сравнение эффективности обхода лабиринтов (меньше = лучше)')
ax.set_xticks(x + width)
ax.set_xticklabels(mazes, rotation=15)
ax.legend()
plt.tight_layout()
plt.savefig("results/benchmark_visited_cells.png", dpi=200)
plt.savefig("results/benchmark_visited_cells.eps", dpi=200)
plt.close()
# График Время выполнения
fig, ax = plt.subplots(figsize=(10, 6))
for i, strat in enumerate(strategies):
times = [next(r["время_мс"] for r in results if r["лабиринт"]
== m and r["стратегия"] == strat) for m in mazes]
ax.bar(x + i*width, times, width, label=strat)
ax.set_ylabel('Время выполнения (мс)')
ax.set_title('Сравнение времени работы алгоритмов')
ax.set_xticks(x + width)
ax.set_xticklabels(mazes, rotation=15)
ax.legend()
plt.tight_layout()
plt.savefig("results/benchmark_execution_time.png", dpi=200)
plt.savefig("results/benchmark_execution_time.eps", dpi=200)
plt.close()
print("Графики сохранены в текущую директорию.")
if __name__ == "__main__":
data = run_benchmarks()
plot_results(data)