[2] Maze solver lab

This commit is contained in:
Кузнецов Максим 2026-09-23 12:27:43 +03:00
parent 030469c8f9
commit 0920ef78c2
16 changed files with 839 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length
simple.txt,BFS,0.01464000015403144,11.0,6.0
simple.txt,DFS,0.010180000390391797,9.0,8.0
simple.txt,A*,0.017740000475896522,9.0,6.0
dead.txt,BFS,0.3642999996372964,307.0,35.0
dead.txt,DFS,0.23493999906349927,279.0,151.0
dead.txt,A*,0.38374000068870373,235.0,35.0
large.txt,BFS,23.894459999428364,6812.0,2329.0
large.txt,DFS,84.77875999960816,6796.0,4537.0
large.txt,A*,28.69542000044021,6791.0,2329.0
empty.txt,BFS,1.2770400004228577,1176.0,48.0
empty.txt,DFS,7.602279999264283,2304.0,1176.0
empty.txt,A*,0.10093999881064519,48.0,48.0
noexit.txt,BFS,0.003699999797390774,1.0,0.0
noexit.txt,DFS,0.0032000003557186574,1.0,0.0
noexit.txt,A*,0.004120000085094944,1.0,0.0
1 maze strategy time_ms visited_cells path_length
2 simple.txt BFS 0.01464000015403144 11.0 6.0
3 simple.txt DFS 0.010180000390391797 9.0 8.0
4 simple.txt A* 0.017740000475896522 9.0 6.0
5 dead.txt BFS 0.3642999996372964 307.0 35.0
6 dead.txt DFS 0.23493999906349927 279.0 151.0
7 dead.txt A* 0.38374000068870373 235.0 35.0
8 large.txt BFS 23.894459999428364 6812.0 2329.0
9 large.txt DFS 84.77875999960816 6796.0 4537.0
10 large.txt A* 28.69542000044021 6791.0 2329.0
11 empty.txt BFS 1.2770400004228577 1176.0 48.0
12 empty.txt DFS 7.602279999264283 2304.0 1176.0
13 empty.txt A* 0.10093999881064519 48.0 48.0
14 noexit.txt BFS 0.003699999797390774 1.0 0.0
15 noexit.txt DFS 0.0032000003557186574 1.0 0.0
16 noexit.txt A* 0.004120000085094944 1.0 0.0

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -0,0 +1,212 @@
Лабораторная работа 2
Поиск выхода из лабиринта
Цель работы
-----------
Цель работы состоит в реализации программы для поиска выхода из лабиринта с использованием объектно ориентированного подхода и паттернов проектирования
В программе реализована загрузка лабиринта из файла несколько алгоритмов поиска и сравнение их работы
Структура программы
-------------------
В программе используются классы Cell для отдельной клетки лабиринта и Maze для самого лабиринта
Для загрузки используется MazeBuilder и его реализация TextFileMazeBuilder
Для поиска пути используется общий класс PathFindingStrategy и три алгоритма BFSStrategy DFSStrategy и AStarStrategy
За хранение результатов отвечает SearchStats а запуск поиска выполняет MazeSolver
Для вывода информации используются Observer и ConsoleView
Использованные паттерны
-----------------------
В работе использованы три паттерна Builder Strategy и Observer
Builder используется для загрузки лабиринта из текстового файла
TextFileMazeBuilder читает файл и создаёт объект Maze
В файле символ # обозначает стену пробел обозначает свободную клетку S является началом а E выходом
Использование Builder позволяет отдельно реализовать загрузку лабиринта и сам класс лабиринта
Strategy используется для выбора алгоритма поиска
В программе реализованы BFS DFS и A*
Все алгоритмы имеют общий интерфейс PathFindingStrategy поэтому в MazeSolver можно менять алгоритм без изменения самого решателя
Observer используется для вывода информации о поиске
MazeSolver отправляет события а ConsoleView получает их и выводит информацию в консоль
Таким образом вывод отделён от основной логики поиска
Алгоритмы поиска
----------------
BFS использует очередь и при обычных условиях находит кратчайший путь в лабиринте без весов
DFS использует стек и может найти путь быстрее но найденный путь не обязательно будет кратчайшим
A* использует очередь с приоритетом и манхэттенскую эвристику поэтому старается в первую очередь проверять клетки которые находятся ближе к выходу
Схема классов
-------------
classDiagram
class Cell {
x
y
is_wall
is_start
is_exit
is_passable()
}
class Maze {
width
height
cells
start
exit
get_cell()
get_neighbors()
}
class MazeBuilder {
build_from_file()
}
class TextFileMazeBuilder {
build_from_file()
}
class PathFindingStrategy {
find_path()
}
class BFSStrategy {
find_path()
}
class DFSStrategy {
find_path()
}
class AStarStrategy {
find_path()
}
class SearchStats {
path
time_ms
visited_count
path_length
}
class MazeSolver {
maze
strategy
set_strategy()
solve()
}
class Observer {
update()
}
class ConsoleView {
update()
}
MazeBuilder <|-- TextFileMazeBuilder
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
Observer <|-- ConsoleView
MazeSolver --> Maze
MazeSolver --> PathFindingStrategy
MazeSolver --> Observer
Maze --> Cell
Тестирование
------------
Для проверки использовалось пять разных лабиринтов
simple.txt представляет простой лабиринт dead.txt содержит тупики large.txt является большим запутанным лабиринтом empty.txt не содержит стен а в noexit.txt выход недостижим
Каждый алгоритм запускался пять раз
Во время эксперимента измерялось время поиска количество посещённых клеток и длина найденного пути
Результаты сохранялись в файл results.csv
Результаты
----------
simple.txt
Алгоритм Время мс Посещено Путь
BFS 0.01464 11 6
DFS 0.01018 9 8
A* 0.01774 9 6
Все алгоритмы работают быстро
BFS и A* нашли более короткий путь чем DFS
dead.txt
Алгоритм Время мс Посещено Путь
BFS 0.36430 307 35
DFS 0.23494 279 151
A* 0.38374 235 35
DFS работал немного быстрее но нашёл более длинный путь
BFS и A* нашли короткий путь
large.txt
Алгоритм Время мс Посещено Путь
BFS 23.89446 6812 2329
DFS 84.77876 6796 4537
A* 28.69542 6791 2329
На большом лабиринте DFS показал худшее время и самый длинный путь
BFS и A* нашли одинаковый путь
empty.txt
Алгоритм Время мс Посещено Путь
BFS 1.277
04 1176 48
DFS 7.60228 2304 1176
A* 0.10094 48 48
В лабиринте без стен лучше всего показал себя A*
Он посетил меньше всего клеток и работал быстрее
noexit.txt
Алгоритм Время мс Посещено Путь
BFS 0.00370 1 0
DFS 0.00320 1 0
A* 0.00412 1 0
В этом лабиринте выход недостижим поэтому все алгоритмы быстро закончили поиск
Графики
-------
Для сравнения времени работы были построены графики для каждого лабиринта
Графики находятся в папке docs/data
simple_time.png dead_time.png large_time.png empty_time.png и noexit_time.png
Вывод
-----
В работе была создана программа для поиска выхода из лабиринта
Были реализованы BFS DFS и A* а также использованы паттерны Builder Strategy и Observer
По результатам эксперимента BFS хорошо подходит для поиска кратчайшего пути DFS может найти путь быстрее но он не всегда получается коротким A* хорошо показывает себя на больших и открытых лабиринтах

View File

@ -0,0 +1,93 @@
import csv
import os
from maze_solver import (
TextFileMazeBuilder,
MazeSolver,
BFSStrategy,
DFSStrategy,
AStarStrategy
)
REPEATS = 5
MAZES = [
"simple.txt",
"dead.txt",
"large.txt",
"empty.txt",
"noexit.txt"
]
STRATEGIES = [
("BFS", BFSStrategy()),
("DFS", DFSStrategy()),
("A*", AStarStrategy())
]
def average(values):
return sum(values) / len(values)
def run():
builder = TextFileMazeBuilder()
results = []
for maze_name in MAZES:
filename = os.path.join("lab2", "mazes", maze_name)
print("Maze:", maze_name)
for strategy_name, strategy in STRATEGIES:
times = []
visited = []
path_lengths = []
for _ in range(REPEATS):
maze = builder.build_from_file(filename)
solver = MazeSolver(maze, strategy)
stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_count)
path_lengths.append(stats.path_length)
results.append([
maze_name,
strategy_name,
average(times),
average(visited),
average(path_lengths)
])
print(
strategy_name,
"time =", average(times),
"visited =", average(visited),
"path =", average(path_lengths)
)
os.makedirs("lab2/docs/data", exist_ok=True)
with open(
"lab2/docs/data/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("Results saved to lab2/docs/data/results.csv")
if __name__ == "__main__":
run()

View File

@ -0,0 +1,23 @@
import csv
import matplotlib.pyplot as plt
with open("lab2/docs/data/results.csv", encoding="utf-8") as file:
rows = list(csv.DictReader(file))
mazes = ["simple.txt", "dead.txt", "large.txt", "empty.txt", "noexit.txt"]
strategies = ["BFS", "DFS", "A*"]
for maze in mazes:
values = []
for strategy in strategies:
for row in rows:
if row["maze"] == maze and row["strategy"] == strategy:
values.append(float(row["time_ms"]))
plt.bar(strategies, values)
plt.title("Время поиска: " + maze)
plt.xlabel("Стратегия")
plt.ylabel("Время, мс")
plt.savefig("lab2/docs/data/" + maze.replace(".txt", "_time.png"))
plt.close()

View File

@ -0,0 +1,26 @@
lines = []
for y in range(100):
row = [" "] * 100
if y == 0 or y == 99:
row = ["#"] * 100
else:
row[0] = "#"
row[99] = "#"
lines.append(row)
lines[1][1] = "S"
lines[98][98] = "E"
for x in range(4, 96, 4):
gap = 1 if (x // 4) % 2 == 0 else 98
for y in range(1, 99):
if y != gap:
lines[y][x] = "#"
with open("lab2/mazes/large.txt", "w", encoding="utf-8") as file:
for row in lines:
file.write("".join(row) + "\n")

View File

@ -0,0 +1,284 @@
from abc import ABC, abstractmethod
from collections import deque
import heapq
import time
class Cell:
def __init__(self, x, y):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
def is_passable(self):
return not self.is_wall
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = []
self.start = None
self.exit = None
for y in range(height):
row = []
for x in range(width):
row.append(Cell(x, y))
self.cells.append(row)
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, cell):
neighbors = []
directions = [
(0, -1),
(0, 1),
(-1, 0),
(1, 0)
]
for dx, dy in directions:
neighbor = self.get_cell(
cell.x + dx,
cell.y + dy
)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
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]
if not lines:
raise ValueError("Файл лабиринта пустой")
width = len(lines[0])
for line in lines:
if len(line) != width:
raise ValueError("Строки лабиринта имеют разную длину")
maze = Maze(width, len(lines))
for y, line in enumerate(lines):
for x, symbol in enumerate(line):
cell = maze.get_cell(x, y)
if symbol == "#":
cell.is_wall = True
elif symbol == "S":
if maze.start is not None:
raise ValueError("В лабиринте несколько стартов")
maze.start = cell
cell.is_start = True
elif symbol == "E":
if maze.exit is not None:
raise ValueError("В лабиринте несколько выходов")
maze.exit = cell
cell.is_exit = True
elif symbol == " ":
pass
else:
raise ValueError("Неизвестный символ в лабиринте")
if maze.start is None:
raise ValueError("В лабиринте нет старта")
if maze.exit is None:
raise ValueError("В лабиринте нет выхода")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze, start, exit):
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit):
if start is None or exit is None:
return [], 0
queue = deque([(start, [start])])
visited = {start}
while queue:
current, path = queue.popleft()
if current == exit:
return path, len(visited)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
queue.append((neighbor, path + [neighbor]))
return [], len(visited)
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze, start, exit):
if start is None or exit is None:
return [], 0
stack = [(start, [start])]
visited = {start}
while stack:
current, path = stack.pop()
if current == exit:
return path, len(visited)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append((neighbor, path + [neighbor]))
return [], len(visited)
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze, start, exit):
if start is None or exit is None:
return [], 0
heap = []
counter = 0
heapq.heappush(
heap,
(self.heuristic(start, exit), counter, start, [start])
)
g_score = {start: 0}
visited = set()
while heap:
_, _, current, path = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == exit:
return path, len(visited)
for neighbor in maze.get_neighbors(current):
new_cost = g_score[current] + 1
if neighbor not in g_score or new_cost < g_score[neighbor]:
g_score[neighbor] = new_cost
counter += 1
priority = new_cost + self.heuristic(
neighbor,
exit
)
heapq.heappush(
heap,
(
priority,
counter,
neighbor,
path + [neighbor]
)
)
return [], len(visited)
class SearchStats:
def __init__(self, path, time_ms, visited_count):
self.path = path
self.time_ms = time_ms
self.visited_count = visited_count
self.path_length = len(path) if path else 0
class MazeSolver:
def __init__(self, maze, strategy=None):
self.maze = maze
self.strategy = strategy
self.observers = []
def attach(self, observer):
self.observers.append(observer)
def detach(self, observer):
self.observers.remove(observer)
def notify(self, event, data=None):
for observer in self.observers:
observer.update(event, data)
def set_strategy(self, strategy):
self.strategy = strategy
def solve(self):
if self.strategy is None:
raise ValueError("Стратегия не установлена")
self.notify("search_started")
start_time = time.perf_counter()
path, visited_count = self.strategy.find_path(
self.maze,
self.maze.start,
self.maze.exit
)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
self.notify("search_finished", time_ms)
self.notify("path_found", path)
return SearchStats(
path,
time_ms,
visited_count
)
class Observer(ABC):
@abstractmethod
def update(self, event, data=None):
pass
class ConsoleView(Observer):
def update(self, event, data=None):
if event == "search_started":
print("Поиск начат")
elif event == "search_finished":
print(f"Поиск завершен за {data:.3f} мс")
elif event == "path_found":
print(f"Длина пути: {len(data)}")

View File

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

View File

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

View File

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

View File

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

View File

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