302 lines
13 KiB
TeX
302 lines
13 KiB
TeX
|
|
\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}
|