[2] 2nd zadanie, otchet, benchmark & cool_pics

This commit is contained in:
danch0us 2026-05-22 17:54:05 +03:00
parent 949252d245
commit 56a26a676a
9 changed files with 605 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

48
kornevma/docs/2/main.py Normal file
View File

@ -0,0 +1,48 @@
import csv
from maze import *
from maze_generator import *
def run_experiments():
maze_configs = [
("small_random", lambda: random_maze(15, 15, wall_prob=0.3)),
("medium_recursive_div", lambda: recursive_division_maze(31, 31)), #odd хз
("large_empty", lambda: empty_maze(100, 100)),
("large_random", lambda: random_maze(100, 100, wall_prob=0.25)),
("no_path", lambda: no_path_maze(20, 20)),
]
algorithms = [("BFS", BFSPathFinding()),
("DFS", DFSPathFinding()),
("A*", AStarPathFinding())]
results = []
for name, gen_func in maze_configs:
maze = gen_func()
for alg_name, strategy in algorithms:
solver = MazeSolver(maze, strategy)
times, visited, lengths = [], [], []
for _ in range(5):
stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited)
lengths.append(stats.path_length)
avg_t = sum(times) / len(times)
avg_v = sum(visited) / len(visited)
avg_l = sum(lengths) / len(lengths)
results.append([name, alg_name, avg_t, avg_v, avg_l])
print(f"{name:20} {alg_name:5} time={avg_t:8.2f}ms visited={avg_v:8.1f} length={avg_l:5.1f}")
with open("results_maze.csv", "w", newline="") as f:
writer = csv.writer(f)
writer.writerow(["maze", "algorithm", "time_ms", "visited", "path_length"])
writer.writerows(results)
print("saved results_maze.csv")
if __name__ == "__main__":
import sys
DEBUG = True
if (len(sys.argv) > 1 and sys.argv[1] == "exp") or DEBUG:
run_experiments()
else:
#run_interactive()
pass

296
kornevma/docs/2/maze.py Normal file
View File

@ -0,0 +1,296 @@
import heapq
import time
import os
from collections import deque
from abc import ABC, abstractmethod
import itertools
class Cell:
def __init__(self, x, y):
self.x = x
self.y = y
self.isWall = False
self.isStart = False
self.isExit = False
self.weight = 1
def isPassable(self):
return not self.isWall
def __repr__(self):
return f"({self.x},{self.y})"
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.grid = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start = None
self.exit = None
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.grid[x][y]
return None
def getNeighbors(self, cell):
dirs = [(-1,0), (1,0), (0,-1), (0,1)]
result = []
for dx, dy in dirs:
nx, ny = cell.x + dx, cell.y + dy
ncell = self.getCell(nx, ny)
if ncell and ncell.isPassable():
result.append(ncell)
return result
class MazeBuilder(ABC):
@abstractmethod
def buildFromFile(self, filename):
pass
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, 'r') as f:
lines = [line.rstrip('\n') for line in f if line.strip() != '']
height = len(lines)
width = len(lines[0]) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = maze.getCell(x, y)
if ch == '#':
cell.isWall = True
elif ch == 'S':
cell.isStart = True
maze.start = cell
elif ch == 'E':
cell.isExit = True
maze.exit = cell
elif ch == ' ':
pass
else:
if ch.isdigit():
cell.weight = int(ch)
else:
raise ValueError(f"err '{ch}' at ({x},{y})")
if maze.start is None or maze.exit is None:
raise ValueError("not e or/and s")
return maze
class PathFindingStrategy(ABC):
def __init__(self):
self.visited_count = 0
@abstractmethod
def findPath(self, maze, start, exit_cell):
pass
class BFSPathFinding(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
self.visited_count = 0
queue = deque()
queue.append(start)
parent = {start: None}
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, exit_cell)
for neighbor in maze.getNeighbors(current):
if neighbor not in parent:
parent[neighbor] = current
queue.append(neighbor)
return []
def _reconstruct_path(self, parent, end):
path = []
cur = end
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
class DFSPathFinding(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
self.visited_count = 0
stack = [start]
parent = {start: None}
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parent, exit_cell)
for neighbor in maze.getNeighbors(current):
if neighbor not in parent:
parent[neighbor] = current
stack.append(neighbor)
return []
def _reconstruct_path(self, parent, end):
path = []
cur = end
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
class AStarPathFinding(PathFindingStrategy):
def findPath(self, maze, start, exit_cell):
self.visited_count = 0
def heuristic(cell):
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
open_set = []
counter = itertools.count() #
heapq.heappush(open_set, (0 + heuristic(start), 0, next(counter), start))
parent = {start: None}
g_score = {start: 0}
closed = set()
while open_set:
_, cost, _, current = heapq.heappop(open_set)
self.visited_count += 1
if current in closed:
continue
if current == exit_cell:
return self._reconstruct_path(parent, exit_cell)
closed.add(current)
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + neighbor.weight
if neighbor not in g_score or tentative_g < g_score[neighbor]:
g_score[neighbor] = tentative_g
f = tentative_g + heuristic(neighbor)
heapq.heappush(open_set, (f, tentative_g, next(counter), neighbor))
parent[neighbor] = current
return []
def _reconstruct_path(self, parent, end):
path = []
cur = end
while cur is not None:
path.append(cur)
cur = parent[cur]
path.reverse()
return path
class SearchStats:
def __init__(self, time_ms, visited, path_length, path):
self.time_ms = time_ms
self.visited = visited
self.path_length = path_length
self.path = path
class MazeSolver:
def __init__(self, maze, strategy):
self.maze = maze
self.strategy = strategy
self.observers = []
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
start = self.maze.start
exit_cell = self.maze.exit
t0 = time.perf_counter()
path = self.strategy.findPath(self.maze, start, exit_cell)
t1 = time.perf_counter()
ms = (t1 - t0) * 1000
visited = self.strategy.visited_count
stats = SearchStats(ms, visited, len(path), path)
self.notify("path_found", stats)
return stats
def addObserver(self, observer):
self.observers.append(observer)
def notify(self, event, data=None):
for obs in self.observers:
obs.update(event, data)
class Observer(ABC):
@abstractmethod
def update(self, event, data):
pass
class ConsoleView(Observer):
def __init__(self, maze):
self.maze = maze
def update(self, event, data):
if event == "path_found":
self.render(data.path, data)
def render(self, path, stats=None):
os.system('cls' if os.name == 'nt' else 'clear')
path_set = set(path) if path else set()
for y in range(self.maze.height):
line = ""
for x in range(self.maze.width):
cell = self.maze.getCell(x, y)
if cell == self.maze.start:
line += "S"
elif cell == self.maze.exit:
line += "E"
elif cell.isWall:
line += "#"
elif cell in path_set:
line += "."
else:
line += " "
print(line)
if stats:
print(f"\npath: {stats.path_length}, visit: {stats.visited}, time: {stats.time_ms:.2f} ms")
class Player:
def __init__(self, start_cell):
self.current = start_cell
self.history = []
def move(self, dx, dy, maze):
nx, ny = self.current.x + dx, self.current.y + dy
ncell = maze.getCell(nx, ny)
if ncell and ncell.isPassable():
self.history.append(self.current)
self.current = ncell
return True
return False
def undo(self):
if self.history:
self.current = self.history.pop()
return True
return False
class Command(ABC):
@abstractmethod
def execute(self):
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player, maze, dx, dy):
self.player = player
self.maze = maze
self.dx = dx
self.dy = dy
self.executed = False
def execute(self):
if not self.executed:
success = self.player.move(self.dx, self.dy, self.maze)
self.executed = success
return success
return False
def undo(self):
if self.executed:
self.player.undo()
self.executed = False
return True
return False

View File

@ -0,0 +1,114 @@
import random
from collections import deque
from maze import Maze, Cell
def empty_maze(width, height):
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.grid[x][y].isWall = False
maze.start = maze.getCell(0, 0)
maze.start.isStart = True
maze.exit = maze.getCell(width-1, height-1)
maze.exit.isExit = True
return maze
def random_maze(width, height, wall_prob=0.3, ensure_path=True):
while True:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < wall_prob:
maze.grid[x][y].isWall = True
else:
maze.grid[x][y].isWall = False
start_cell = maze.getCell(0, 0)
exit_cell = maze.getCell(width-1, height-1)
start_cell.isWall = False
start_cell.isStart = True
exit_cell.isWall = False
exit_cell.isExit = True
maze.start = start_cell
maze.exit = exit_cell
if not ensure_path:
return maze
if _path_exists(maze, start_cell, exit_cell):
return maze
def no_path_maze(width, height):
maze = empty_maze(width, height)
exit_cell = maze.exit
for dx, dy in [(-1,0),(1,0),(0,-1),(0,1)]:
nx, ny = exit_cell.x + dx, exit_cell.y + dy
neighbor = maze.getCell(nx, ny)
if neighbor:
neighbor.isWall = True
return maze
def recursive_division_maze(width, height):
if width % 2 == 0:
width += 1
if height % 2 == 0:
height += 1
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.grid[x][y].isWall = False
maze.start = maze.getCell(0, 0)
maze.start.isStart = True
maze.exit = maze.getCell(width-1, height-1)
maze.exit.isExit = True
for x in range(width):
maze.getCell(x, 0).isWall = True
maze.getCell(x, height-1).isWall = True
for y in range(height):
maze.getCell(0, y).isWall = True
maze.getCell(width-1, y).isWall = True
maze.start.isWall = False
maze.exit.isWall = False
def divide(x1, y1, x2, y2):
if x2 - x1 < 2 or y2 - y1 < 2:
return
vertical = (x2 - x1) > (y2 - y1) and (x2 - x1) >= 2
if vertical:
wall_x = random.randrange(x1 + 1, x2, 2)
hole_y = random.randrange(y1, y2 + 1, 2) if (y2 - y1) > 0 else y1
for y in range(y1, y2 + 1):
if y != hole_y:
maze.getCell(wall_x, y).isWall = True
divide(x1, y1, wall_x - 1, y2)
divide(wall_x + 1, y1, x2, y2)
else:
wall_y = random.randrange(y1 + 1, y2, 2)
hole_x = random.randrange(x1, x2 + 1, 2) if (x2 - x1) > 0 else x1
for x in range(x1, x2 + 1):
if x != hole_x:
maze.getCell(x, wall_y).isWall = True
divide(x1, y1, x2, wall_y - 1)
divide(x1, wall_y + 1, x2, y2)
divide(0, 0, width-1, height-1)
return maze
def _path_exists(maze, start, exit_cell):
visited = set()
queue = deque([start])
visited.add(start)
while queue:
cur = queue.popleft()
if cur == exit_cell:
return True
for n in maze.getNeighbors(cur):
if n not in visited:
visited.add(n)
queue.append(n)
return False

View File

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

BIN
kornevma/docs/2/mermaid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

View File

@ -0,0 +1,16 @@
maze,algorithm,time_ms,visited,path_length
small_random,BFS,0.2025800000410527,79.0,31.0
small_random,DFS,0.18165999208576977,75.0,35.0
small_random,A*,0.24926000041887164,62.0,31.0
medium_recursive_div,BFS,0.003279995871707797,1.0,0.0
medium_recursive_div,DFS,0.002820009831339121,1.0,0.0
medium_recursive_div,A*,0.004719995195046067,1.0,0.0
large_empty,BFS,28.160699998261407,10000.0,199.0
large_empty,DFS,16.872200003126636,5149.0,4951.0
large_empty,A*,47.75527999736369,10000.0,199.0
large_random,BFS,20.68703998811543,7396.0,201.0
large_random,DFS,18.394460005220026,6029.0,615.0
large_random,A*,10.62775999889709,2215.0,201.0
no_path,BFS,1.0112400050275028,397.0,0.0
no_path,DFS,1.0159599944017828,397.0,0.0
no_path,A*,1.6842399956658483,397.0,0.0
1 maze algorithm time_ms visited path_length
2 small_random BFS 0.2025800000410527 79.0 31.0
3 small_random DFS 0.18165999208576977 75.0 35.0
4 small_random A* 0.24926000041887164 62.0 31.0
5 medium_recursive_div BFS 0.003279995871707797 1.0 0.0
6 medium_recursive_div DFS 0.002820009831339121 1.0 0.0
7 medium_recursive_div A* 0.004719995195046067 1.0 0.0
8 large_empty BFS 28.160699998261407 10000.0 199.0
9 large_empty DFS 16.872200003126636 5149.0 4951.0
10 large_empty A* 47.75527999736369 10000.0 199.0
11 large_random BFS 20.68703998811543 7396.0 201.0
12 large_random DFS 18.394460005220026 6029.0 615.0
13 large_random A* 10.62775999889709 2215.0 201.0
14 no_path BFS 1.0112400050275028 397.0 0.0
15 no_path DFS 1.0159599944017828 397.0 0.0
16 no_path A* 1.6842399956658483 397.0 0.0

125
kornevma/docs/report2.md Normal file
View File

@ -0,0 +1,125 @@
1. Постановка задачи и цели
Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации и экспериментального сравнения. Код построен в объектно-ориентированном стиле с применением паттернов проектирования (Builder, Strategy, Observer, Command). Цели:
Обеспечить лёгкую замену формата лабиринта (Builder).
Сделать переключение алгоритмов поиска независимым от остальной логики (Strategy).
Организовать уведомление наблюдателей о событиях (Observer).
Инкапсулировать действия игрока с возможностью отмены (Command).
Экспериментально сравнить BFS, DFS и A* на различных лабиринтах.
2. Архитектура приложения (диаграмма классов Mermaid)
![mermaid_diagramm]("2"/mermaid.png)
3. Применённые паттерны проектирования
Паттерн Назначение Реализация
Builder Отделяет создание сложного объекта Maze от его представления. TextFileMazeBuilder парсит текстовый файл и строит лабиринт. При смене формата (JSON, бинарный) достаточно реализовать новый класс-строитель, не трогая модель.
Strategy Инкапсулирует взаимозаменяемые алгоритмы поиска пути. Интерфейс PathFindingStrategy реализуется классами BFSPathFinding, DFSPathFinding, AStarPathFinding. MazeSolver работает со стратегией через общий интерфейс, позволяя переключать алгоритмы на лету.
Observer Обеспечивает слабую связь между решателем и представлением. ConsoleView подписывается на MazeSolver и автоматически обновляет консоль при нахождении пути. Можно добавить другие наблюдатели (логгер, GUI) без изменения решателя.
Command Превращает запросы на перемещение игрока в объекты с поддержкой отмены. MoveCommand хранит направление, игрока и лабиринт; при выполнении двигает игрока, при отмене возвращает на предыдущую клетку. Можно организовать макрокоманды и историю.
4. Реализация ключевых фрагментов
Полный код выложен в репозиторий (ветка kornevma). Здесь приведены только сигнатуры.
Модель: Cell с координатами и флагами, Maze с сеткой и методом getNeighbors.
Строитель: TextFileMazeBuilder.buildFromFile(filename) → Maze.
Стратегии:
BFSPathFinding.findPath использует collections.deque, гарантирует кратчайший путь.
DFSPathFinding.findPath использует стек (list), путь может быть длиннее.
AStarPathFinding.findPath с манхэттенской эвристикой и heapq (с уникальным счётчиком для избежания сравнения Cell).
Решатель: MazeSolver.solve() возвращает SearchStats (время, посещённые клетки, длина пути).
Визуализация: ConsoleView.render отображает лабиринт, путь и статистику.
Игрок и команды: MoveCommand и Player с возможностью отмены ходов.
5. Экспериментальное сравнение алгоритмов
5.1 Тестовые лабиринты
Сгенерированы с помощью модуля maze_generator.py:
small_random (15×15, вероятность стен 30%) маленький случайный.
medium_recursive_div (31×31, рекурсивное деление) средний с красивой структурой.
large_empty (100×100) без стен, для демонстрации максимальной нагрузки.
large_random (100×100, вероятность стен 25%) большой случайный.
no_path (20×20) выход заблокирован стенами.
5.2 Результаты замеров (усреднение по 5 запускам)
Лабиринт Алгоритм Время (мс) Посещено клеток Длина пути
small_random BFS 0.20 79 31
small_random DFS 0.18 75 35
small_random A* 0.25 62 31
medium_recursive_div BFS 0.003 1 0
medium_recursive_div DFS 0.003 1 0
medium_recursive_div A* 0.005 1 0
large_empty BFS 28.16 10000 199
large_empty DFS 16.87 5149 4951
large_empty A* 47.76 10000 199
large_random BFS 20.69 7396 201
large_random DFS 18.39 6029 615
large_random A* 10.63 2215 201
no_path BFS 1.01 397 0
no_path DFS 1.02 397 0
no_path A* 1.68 397 0
5.3 График сравнения
![benchmark]("2"/benchmark_plot.png)
6. Анализ результатов
1. Время выполнения
На маленьком лабиринте все алгоритмы работают доли миллисекунды, разница несущественна.
На больших лабиринтах (large_empty и large_random) лидирует A* (10.63 мс на случайном), так как эвристика направляет поиск к цели, посещая меньше клеток. DFS показал 18.39 мс, BFS 20.69 мс.
На пустом лабиринте без стен A* и BFS посещают все клетки (10000), но BFS работает быстрее (28.16 мс), вероятно, из-за накладных расходов на приоритетную очередь в A*. DFS закончил раньше, но нашёл очень длинный извилистый путь (4951 шаг вместо минимальных 199).
При отсутствии пути все алгоритмы вынуждены обойти всю доступную область (397 клеток), время почти одинаково (1.01.7 мс).
2. Количество посещённых клеток
BFS всегда посещает все клетки, достижимые до уровня выхода, поэтому при наличии пути в большом лабиринте число посещённых равно размеру компоненты связности (7396).
DFS ведёт себя непредсказуемо: на пустом лабиринте он «закопался» в глубину и посетил лишь 5149 клеток, но путь получился крайне длинным. На случайном лабиринте также посещено меньше (6029), но путь всё равно неоптимален (615 против 201).
A* использует эвристику, поэтому посещает значительно меньше клеток (2215 на большом случайном) и находит кратчайший путь (как BFS).
3. Длина пути
BFS всегда находит кратчайший путь (31, 199, 201).
DFS в силу природы стека может сильно отклоняться, длина пути достигает 4951 на пустом лабиринте и 615 на случайном, что в десятки раз хуже оптимального.
A* также находит кратчайший путь благодаря допустимой эвристике (манхэттенское расстояние не переоценивает стоимость). Длина совпадает с BFS.
4. Взвешенные клетки (доп. задание)
Код поддерживает вес клетки (поле weight). Если в файле лабиринта указаны цифры (1,2,3), алгоритм A* учитывает их как стоимость перехода. Для сравнения можно реализовать алгоритм Дейкстры (он же A* с нулевой эвристикой). На однородных весах (1) Дейкстра эквивалентен BFS, на неоднородных находит оптимальный путь, но посещает больше клеток, чем A*. В данной работе взвешенные лабиринты не замерялись, но архитектура Strategy позволяет легко добавить DijkstraPathFinding и провести аналогичные эксперименты.
7. Выводы
По алгоритмам:
BFS гарантирует кратчайший путь, но может исследовать много клеток. Подходит для небольших лабиринтов или когда критична оптимальность.
DFS часто быстрее находит хоть какой-то путь, но он редко бывает коротким. Полезен, если важен факт достижимости, а не длина.
A* с манхэттенской эвристикой сочетает оптимальность BFS и целенаправленность, посещая меньше клеток. Это лучший выбор для большинства практических задач поиска пути.
По паттернам и архитектуре:
Применение паттернов (Builder, Strategy, Observer, Command) обеспечило гибкость и расширяемость. Замена формата лабиринта, добавление нового алгоритма, изменение способа отображения или внедрение отмены действий не затрагивают ядро программы.
Без паттернов пришлось бы менять множество классов при каждом новом требовании. Strategy позволила в одном цикле экспериментов легко переключать алгоритмы; Builder скрыл детали создания лабиринта; Observer отделил визуализацию; Command дал основу для интерактивного управления с историей.
Полученный код может служить каркасом для более сложных проектов (игровой AI, маршрутизация, робототехника).
Итог: Разработанная программа демонстрирует преимущества объектно-ориентированного подхода с паттернами при решении задачи поиска пути в лабиринте, а экспериментальные данные подтверждают теоретические ожидания относительно эффективности алгоритмов.