Compare commits

..

3 Commits

Author SHA1 Message Date
c1236a618f Upload files to "Ezhovnd" 2026-09-05 08:34:29 +00:00
46fd403eb8 Update Ezhovnd/2 задание.py 2026-09-05 08:32:56 +00:00
fd367d2c1a Upload files to "Ezhovnd" 2026-09-05 08:03:31 +00:00
12 changed files with 763 additions and 1129 deletions

285
Ezhovnd/2 задание.py Normal file
View File

@ -0,0 +1,285 @@
import csv
import time
import os
import random
from collections import deque
import heapq
import matplotlib.pyplot as plt
import pandas as pd
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 isPassable(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
def getCell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getNeighbors(self, cell):
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
neighbor = self.getCell(cell.x + dx, cell.y + dy)
if neighbor and neighbor.isPassable():
neighbors.append(neighbor)
return neighbors
class MazeBuilder:
def buildFromFile(self, filename):
raise NotImplementedError
class TextFileMazeBuilder(MazeBuilder):
def buildFromFile(self, filename):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
for y, line in enumerate(lines):
for x, char in enumerate(line):
cell = maze.cells[y][x]
if char == '#':
cell.is_wall = True
elif char == 'S':
cell.is_start = True
maze.start = cell
elif char == 'E':
cell.is_exit = True
maze.exit = cell
if maze.start is None or maze.exit is None:
raise ValueError("В файле должны быть символы S и E")
return maze
class PathFindingStrategy:
def findPath(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
queue = deque([start])
came_from = {start: None}
visited = set([start])
while queue:
current = queue.popleft()
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
queue.append(neighbor)
came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path if path and path[0] == came_from.get(exit) or path[0] == exit else []
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
stack = [start]
came_from = {start: None}
visited = set([start])
while stack:
current = stack.pop()
if current == exit:
break
for neighbor in maze.getNeighbors(current):
if neighbor not in visited:
visited.add(neighbor)
stack.append(neighbor)
came_from[neighbor] = current
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
class AStarStrategy(PathFindingStrategy):
def heuristic(self, a, b):
return abs(a.x - b.x) + abs(a.y - b.y)
def findPath(self, maze, start, exit):
open_set = []
counter = 0
heapq.heappush(open_set, (0, counter, start))
came_from = {start: None}
g_score = {start: 0}
visited = set()
while open_set:
_, _, current = heapq.heappop(open_set)
if current in visited:
continue
visited.add(current)
if current == exit:
break
for neighbor in maze.getNeighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score = tentative_g + self.heuristic(neighbor, exit)
counter += 1
heapq.heappush(open_set, (f_score, counter, neighbor))
path = self._reconstruct_path(came_from, exit)
return path, len(visited)
def _reconstruct_path(self, came_from, exit):
path = []
current = exit
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
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=None, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.maze or not self.strategy:
return None
start_time = time.perf_counter()
path, visited_count = self.strategy.findPath(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
time_ms = (end_time - start_time) * 1000
path_length = len(path) if path and path[-1] == self.maze.exit else 0
return SearchStats(round(time_ms, 4), visited_count, path_length)
def create_maze_with_walls(size, wall_probability=0.3):
maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
for y in range(size):
for x in range(size):
if random.random() < wall_probability:
maze.cells[y][x].is_wall = True
maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True
maze.exit.is_exit = True
maze.start.is_wall = False
maze.exit.is_wall = False
return maze
def create_empty_maze(size):
maze = Maze(size, size)
maze.cells = [[Cell(x, y) for x in range(size)] for y in range(size)]
maze.start = maze.cells[0][0]
maze.exit = maze.cells[size-1][size-1]
maze.start.is_start = True
maze.exit.is_exit = True
return maze
def create_no_exit_maze(size, wall_probability=0.3):
maze = create_maze_with_walls(size, wall_probability)
maze.exit.is_wall = True
return maze
def run_experiment():
maze_configs = {
"10x10_simple": {"size": 10, "type": "normal", "wall_prob": 0.1},
"50x50_with_deadends": {"size": 50, "type": "normal", "wall_prob": 0.3},
"100x100_complex": {"size": 100, "type": "normal", "wall_prob": 0.35},
"empty": {"size": 30, "type": "empty"},
"no_exit": {"size": 30, "type": "no_exit", "wall_prob": 0.3},
}
strategies = {
"BFS": BFSStrategy(),
"DFS": DFSStrategy(),
"AStar": AStarStrategy()
}
results = []
for maze_name, config in maze_configs.items():
size = config["size"]
maze_type = config["type"]
if maze_type == "empty":
maze = create_empty_maze(size)
elif maze_type == "no_exit":
maze = create_no_exit_maze(size, config.get("wall_prob", 0.3))
else:
maze = create_maze_with_walls(size, config.get("wall_prob", 0.3))
for strat_name, strategy in strategies.items():
solver = MazeSolver(maze, strategy)
times, visited_list, lengths = [], [], []
for _ in range(7):
stats = solver.solve()
times.append(stats.time_ms)
visited_list.append(stats.visited_cells)
lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited_list) / len(visited_list)
avg_length = sum(lengths) / len(lengths)
results.append([
maze_name, strat_name,
round(avg_time, 4),
int(avg_visited),
int(avg_length)
])
os.makedirs("results", exist_ok=True)
csv_path = "results/results.csv"
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"])
writer.writerows(results)
df = pd.read_csv(csv_path)
plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["время_мс"], marker='o', label=strat)
plt.title("Сравнение времени работы алгоритмов")
plt.xlabel("Лабиринт")
plt.ylabel("Время (мс)")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("results/time_comparison.png")
plt.close()
plt.figure(figsize=(12, 6))
for strat in df["стратегия"].unique():
subset = df[df["стратегия"] == strat]
plt.plot(subset["лабиринт"], subset["посещено_клеток"], marker='o', label=strat)
plt.title("Количество посещённых клеток")
plt.xlabel("Лабиринт")
plt.ylabel("Посещено клеток")
plt.legend()
plt.grid(True)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig("results/visited_comparison.png")
plt.close()
if __name__ == "__main__":
run_experiment()

View File

@ -1,825 +0,0 @@
"""
maze_all.py — Полный проект лабиринта со всеми паттернами:
Builder, Strategy, Observer, Command.
Содержит все модули: model, builder, strategy, solver, generator, experiment, main.
"""
from __future__ import annotations
import abc
import csv
import heapq
import json
import os
import random
import sys
import time
from collections import deque
from dataclasses import dataclass, field
from typing import Optional
# ============================================================================
# model.py — Модель лабиринта (классы Cell и Maze)
# ============================================================================
class Cell:
"""Одна клетка лабиринта."""
WALL = '#'
PASS = ' '
START = 'S'
EXIT = 'E'
SWAMP = 'W' # болото — вес 3
SAND = 'N' # песок — вес 2
ROAD = 'R' # асфальт — вес 1 (то же что PASS)
WEIGHTS = {WALL: 0, PASS: 1, ROAD: 1, START: 1, EXIT: 1, SAND: 2, SWAMP: 3}
def __init__(self, x: int, y: int, symbol: str = PASS):
self.x = x
self.y = y
self.symbol = symbol
@property
def is_wall(self) -> bool:
return self.symbol == self.WALL
@property
def is_start(self) -> bool:
return self.symbol == self.START
@property
def is_exit(self) -> bool:
return self.symbol == self.EXIT
@property
def weight(self) -> int:
return self.WEIGHTS.get(self.symbol, 1)
def is_passable(self) -> bool:
return not self.is_wall
def __repr__(self):
return f"Cell({self.x},{self.y},'{self.symbol}')"
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
return isinstance(other, Cell) and self.x == other.x and self.y == other.y
class Maze:
"""Двумерный массив клеток + метаданные."""
def __init__(self, width: int, height: int, cells: list[list[Cell]],
start: Optional[Cell] = None, exit_: Optional[Cell] = None):
self.width = width
self.height = height
self._cells = cells # cells[y][x]
self.start = start
self.exit = exit_
def get_cell(self, x: int, y: int) -> Cell:
return self._cells[y][x]
def get_neighbors(self, cell: Cell) -> list[Cell]:
"""Четыре соседа (если в пределах карты и не стена)."""
neighbors = []
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
nx, ny = cell.x + dx, cell.y + dy
if 0 <= nx < self.width and 0 <= ny < self.height:
nb = self._cells[ny][nx]
if nb.is_passable():
neighbors.append(nb)
return neighbors
def render(self, path: set = None, visited: set = None,
player_pos: Optional[Cell] = None) -> str:
"""Возвращает строковое представление для консоли."""
path = path or set()
visited = visited or set()
lines = []
for y in range(self.height):
row = []
for x in range(self.width):
cell = self._cells[y][x]
if player_pos and cell == player_pos:
row.append('@')
elif cell in path and not cell.is_start and not cell.is_exit:
row.append('·')
elif cell in visited and not cell.is_start and not cell.is_exit:
row.append('░')
else:
row.append(cell.symbol if cell.symbol != ' ' else '.')
lines.append(''.join(row))
return '\n'.join(lines)
# ============================================================================
# builder.py — Паттерн Builder
# ============================================================================
class MazeBuilder(abc.ABC):
"""Интерфейс строителя лабиринта."""
@abc.abstractmethod
def build_from_file(self, filename: str) -> Maze:
...
@abc.abstractmethod
def build_empty(self, width: int, height: int) -> Maze:
"""Создать лабиринт без стен."""
...
class TextFileMazeBuilder(MazeBuilder):
"""
Строитель для текстовых файлов:
# — стена
' ' или '.' — проход (вес 1)
S — старт
E — выход
W — болото (вес 3)
N — песок (вес 2)
R — асфальт (вес 1)
"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, encoding='utf-8') as f:
lines = f.read().splitlines()
return self._parse_lines(lines)
def build_from_string(self, text: str) -> Maze:
return self._parse_lines(text.splitlines())
def _parse_lines(self, lines: list[str]) -> Maze:
if not lines:
raise ValueError("Файл лабиринта пуст")
height = len(lines)
width = max(len(line) for line in lines)
cells: list[list[Cell]] = []
start = exit_ = None
for y, line in enumerate(lines):
row = []
for x in range(width):
ch = line[x] if x < len(line) else ' '
if ch == '.':
ch = ' ' # нормализация
cell = Cell(x, y, ch)
if cell.is_start:
start = cell
if cell.is_exit:
exit_ = cell
row.append(cell)
cells.append(row)
if start is None:
raise ValueError("В лабиринте не найдена клетка 'S' (старт)")
if exit_ is None:
raise ValueError("В лабиринте не найдена клетка 'E' (выход)")
return Maze(width, height, cells, start, exit_)
def build_empty(self, width: int, height: int) -> Maze:
"""Лабиринт без стен: рамка из стен, внутри проходы."""
cells = []
start = exit_ = None
for y in range(height):
row = []
for x in range(width):
if x == 0 or x == width - 1 or y == 0 or y == height - 1:
ch = Cell.WALL
else:
ch = Cell.PASS
cell = Cell(x, y, ch)
row.append(cell)
cells.append(row)
# Старт и выход
cells[1][1].symbol = Cell.START
start = cells[1][1]
cells[height - 2][width - 2].symbol = Cell.EXIT
exit_ = cells[height - 2][width - 2]
return Maze(width, height, cells, start, exit_)
class JsonMazeBuilder(MazeBuilder):
"""
Альтернативный строитель — JSON-формат.
Демонстрирует расширяемость паттерна Builder без изменения клиента.
Формат:
{
"width": 5, "height": 5,
"cells": ["#####", "#S E#", "#####"]
}
"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, encoding='utf-8') as f:
data = json.load(f)
lines = data['cells']
text_builder = TextFileMazeBuilder()
return text_builder._parse_lines(lines)
def build_empty(self, width: int, height: int) -> Maze:
return TextFileMazeBuilder().build_empty(width, height)
# ============================================================================
# strategy.py — Паттерн Strategy (алгоритмы поиска пути)
# ============================================================================
class PathFindingStrategy(abc.ABC):
"""Интерфейс стратегии поиска пути."""
@abc.abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_: Cell
) -> tuple[list[Cell], int]:
"""
Возвращает (path, visited_count).
path — список клеток от start до exit_ включительно,
или [] если пути нет.
visited_count — количество клеток, посещённых во время поиска.
"""
...
def name(self) -> str:
return self.__class__.__name__
def _reconstruct(parent: dict, end: Cell) -> list[Cell]:
"""Восстановить путь по словарю parent."""
path, node = [], end
while node is not None:
path.append(node)
node = parent.get(node)
path.reverse()
return path
class BFSStrategy(PathFindingStrategy):
"""Поиск в ширину — гарантирует кратчайший путь (по количеству шагов)."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
queue = deque([start])
parent = {start: None}
visited = 0
while queue:
cell = queue.popleft()
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
for nb in maze.get_neighbors(cell):
if nb not in parent:
parent[nb] = cell
queue.append(nb)
return [], visited
def name(self): return "BFS"
class DFSStrategy(PathFindingStrategy):
"""Поиск в глубину — быстрый, но путь не оптимален."""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
stack = [start]
parent = {start: None}
visited = 0
while stack:
cell = stack.pop()
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
for nb in maze.get_neighbors(cell):
if nb not in parent:
parent[nb] = cell
stack.append(nb)
return [], visited
def name(self): return "DFS"
class AStarStrategy(PathFindingStrategy):
"""
A* с манхэттенской эвристикой — оптимальный и быстрый.
Учитывает веса клеток (болото, песок и т.д.).
"""
def _h(self, cell: Cell, goal: Cell) -> int:
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
# (f, g, cell)
heap = [(self._h(start, exit_), 0, id(start), start)]
g_score = {start: 0}
parent = {start: None}
visited = 0
while heap:
f, g, _, cell = heapq.heappop(heap)
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
if g > g_score.get(cell, float('inf')):
continue # устаревшая запись
for nb in maze.get_neighbors(cell):
new_g = g + nb.weight
if new_g < g_score.get(nb, float('inf')):
g_score[nb] = new_g
parent[nb] = cell
f_val = new_g + self._h(nb, exit_)
heapq.heappush(heap, (f_val, new_g, id(nb), nb))
return [], visited
def name(self): return "A*"
class DijkstraStrategy(PathFindingStrategy):
"""
Алгоритм Дейкстры — оптимален для взвешенных клеток.
На равновесных лабиринтах совпадает с BFS.
"""
def find_path(self, maze: Maze, start: Cell, exit_: Cell):
heap = [(0, id(start), start)]
dist = {start: 0}
parent = {start: None}
visited = 0
while heap:
d, _, cell = heapq.heappop(heap)
visited += 1
if cell == exit_:
return _reconstruct(parent, exit_), visited
if d > dist.get(cell, float('inf')):
continue
for nb in maze.get_neighbors(cell):
new_d = d + nb.weight
if new_d < dist.get(nb, float('inf')):
dist[nb] = new_d
parent[nb] = cell
heapq.heappush(heap, (new_d, id(nb), nb))
return [], visited
def name(self): return "Dijkstra"
# Реестр всех стратегий (используется в экспериментальной части)
ALL_STRATEGIES = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
# ============================================================================
# solver.py — Паттерны Observer и Command, класс MazeSolver
# ============================================================================
# ─────────────────────────────────────────────
# Observer
# ─────────────────────────────────────────────
class Observer(abc.ABC):
@abc.abstractmethod
def update(self, event: str, payload: dict) -> None: ...
class Subject:
"""Миксин: любой класс, у которого есть наблюдатели."""
def __init__(self):
self._observers: list[Observer] = []
def add_observer(self, obs: Observer):
self._observers.append(obs)
def remove_observer(self, obs: Observer):
self._observers.remove(obs)
def notify(self, event: str, payload: dict = None):
for obs in self._observers:
obs.update(event, payload or {})
class ConsoleView(Observer):
"""
Консольный наблюдатель: печатает события и рендерит лабиринт.
"""
def update(self, event: str, payload: dict) -> None:
if event == 'maze_loaded':
maze: Maze = payload['maze']
print(f"\n[ConsoleView] Лабиринт загружен ({maze.width}×{maze.height})")
print(maze.render())
elif event == 'search_started':
print(f"\n[ConsoleView] Начат поиск алгоритмом: {payload['strategy']}")
elif event == 'path_found':
maze: Maze = payload['maze']
path: list[Cell] = payload['path']
stats = payload['stats']
path_set = set(path)
print(f"\n[ConsoleView] Путь найден! Длина: {stats.path_length}, "
f"посещено: {stats.visited_cells}, "
f"время: {stats.elapsed_ms:.2f} мс")
print(maze.render(path=path_set))
elif event == 'no_path':
print("\n[ConsoleView] Путь не найден.")
elif event == 'player_moved':
maze: Maze = payload['maze']
player = payload['player']
path_set = set(payload.get('path', []))
print(f"\n[ConsoleView] Игрок переместился → ({player.position.x}, {player.position.y})")
print(maze.render(path=path_set, player_pos=player.position))
elif event == 'undo':
print(f"[ConsoleView] Отмена хода. Игрок → ({payload['position'].x}, {payload['position'].y})")
class FileLogObserver(Observer):
"""Наблюдатель-логгер: записывает события в файл."""
def __init__(self, filepath: str):
self._path = filepath
def update(self, event: str, payload: dict) -> None:
with open(self._path, 'a', encoding='utf-8') as f:
f.write(f"[{event}] {payload.get('strategy','')}"
f" visited={payload.get('stats', None) and payload['stats'].visited_cells}\n")
# ─────────────────────────────────────────────
# SearchStats
# ─────────────────────────────────────────────
@dataclass
class SearchStats:
strategy_name: str
elapsed_ms: float
visited_cells: int
path_length: int
# ─────────────────────────────────────────────
# MazeSolver — оркестратор
# ─────────────────────────────────────────────
class MazeSolver(Subject):
"""
Принимает лабиринт и стратегию, выполняет поиск, собирает статистику,
уведомляет наблюдателей.
"""
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
super().__init__()
self._maze = maze
self._strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self._strategy = strategy
def load_maze(self, maze: Maze):
self._maze = maze
self.notify('maze_loaded', {'maze': maze})
def solve(self) -> tuple[SearchStats, list[Cell]]:
self.notify('search_started', {'strategy': self._strategy.name()})
t0 = time.perf_counter()
path, visited = self._strategy.find_path(
self._maze, self._maze.start, self._maze.exit)
elapsed_ms = (time.perf_counter() - t0) * 1000
stats = SearchStats(
strategy_name=self._strategy.name(),
elapsed_ms=elapsed_ms,
visited_cells=visited,
path_length=len(path),
)
if path:
self.notify('path_found', {'maze': self._maze, 'path': path, 'stats': stats})
else:
self.notify('no_path', {'maze': self._maze, 'stats': stats})
return stats, path
# ─────────────────────────────────────────────
# Command
# ─────────────────────────────────────────────
class Command(abc.ABC):
@abc.abstractmethod
def execute(self) -> None: ...
@abc.abstractmethod
def undo(self) -> None: ...
class Player:
"""Хранит текущую позицию игрока."""
def __init__(self, position: Cell):
self.position = position
class MoveCommand(Command):
"""
Перемещение игрока в клетку target.
Undo возвращает на previous_position.
"""
def __init__(self, player: Player, target: Cell,
solver: 'MazeSolver', path: list[Cell] = None):
self._player = player
self._target = target
self._previous = player.position
self._solver = solver
self._path = path or []
def execute(self):
self._player.position = self._target
self._solver.notify('player_moved', {
'maze': self._solver._maze,
'player': self._player,
'path': self._path,
})
def undo(self):
self._player.position = self._previous
self._solver.notify('undo', {'position': self._previous})
class CommandHistory:
"""Стек выполненных команд для поддержки undo."""
def __init__(self):
self._stack: list[Command] = []
def execute(self, cmd: Command):
cmd.execute()
self._stack.append(cmd)
def undo(self):
if self._stack:
self._stack.pop().undo()
else:
print("[CommandHistory] Нечего отменять.")
# ============================================================================
# generator.py — Генерация тестовых лабиринтов
# ============================================================================
def generate_maze(width: int, height: int, seed: int = 42,
weighted: bool = False) -> Maze:
"""
Генерирует лабиринт алгоритмом DFS (Recursive Backtracker).
width/height — размеры (лучше нечётные для красивого рисунка).
"""
rng = random.Random(seed)
# Начальный массив — всё стены
cells = [[Cell(x, y, Cell.WALL) for x in range(width)]
for y in range(height)]
def carve(x, y):
cells[y][x].symbol = Cell.PASS
directions = [(0, 2), (0, -2), (2, 0), (-2, 0)]
rng.shuffle(directions)
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < width and 0 <= ny < height:
if cells[ny][nx].symbol == Cell.WALL:
# Убрать стену между (x,y) и (nx,ny)
cells[y + dy // 2][x + dx // 2].symbol = Cell.PASS
carve(nx, ny)
sys.setrecursionlimit(width * height + 100)
carve(1, 1)
# Старт и выход
cells[1][1].symbol = Cell.START
cells[height - 2][width - 2].symbol = Cell.EXIT
start = cells[1][1]
exit_ = cells[height - 2][width - 2]
# Взвешенные клетки (болото, песок)
if weighted:
for y in range(height):
for x in range(width):
if cells[y][x].symbol == Cell.PASS:
r = rng.random()
if r < 0.05:
cells[y][x].symbol = Cell.SWAMP
elif r < 0.15:
cells[y][x].symbol = Cell.SAND
return Maze(width, height, cells, start, exit_)
def generate_no_exit(width: int, height: int, seed: int = 42) -> Maze:
"""Лабиринт без выхода — выход заблокирован стенами."""
maze = generate_maze(width, height, seed)
ex = maze.exit
# Окружить выход стенами
for dx, dy in ((0,-1),(0,1),(-1,0),(1,0)):
nx, ny = ex.x + dx, ex.y + dy
if 0 <= nx < maze.width and 0 <= ny < maze.height:
maze._cells[ny][nx].symbol = Cell.WALL
return maze
def save_maze(maze: Maze, filepath: str):
"""Сохранить лабиринт в текстовый файл."""
lines = []
for y in range(maze.height):
row = ''.join(maze._cells[y][x].symbol for x in range(maze.width))
lines.append(row)
with open(filepath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
def make_empty_maze(width: int, height: int) -> Maze:
"""Лабиринт-поле без внутренних стен (только рамка)."""
return TextFileMazeBuilder().build_empty(width, height)
# ============================================================================
# experiment.py — Экспериментальная часть
# ============================================================================
REPEATS = 7
OUT_CSV = os.path.join(os.path.dirname(__file__), 'docs/data/maze_results.csv')
MAZE_DIR = os.path.join(os.path.dirname(__file__), 'docs/data/mazes')
def run_bench(maze, strategy, repeats=REPEATS):
times, visited_list, path_lens = [], [], []
for _ in range(repeats):
solver = MazeSolver(maze, strategy)
stats, path = solver.solve()
times.append(stats.elapsed_ms)
visited_list.append(stats.visited_cells)
path_lens.append(stats.path_length)
return {
'time_ms_mean': statistics.mean(times),
'time_ms_std': statistics.stdev(times) if len(times) > 1 else 0,
'visited_mean': statistics.mean(visited_list),
'path_length': path_lens[0], # детерминировано
}
def run_experiment():
os.makedirs(MAZE_DIR, exist_ok=True)
# Определяем лабиринты
configs = [
('small_10x10', generate_maze(11, 11, seed=1)),
('medium_50x50', generate_maze(51, 51, seed=2)),
('large_100x100', generate_maze(101, 101, seed=3)),
('empty_50x50', make_empty_maze(52, 52)),
('no_exit_20x20', generate_no_exit(21, 21, seed=4)),
('weighted_50x50', generate_maze(51, 51, seed=5, weighted=True)),
]
# Сохраним лабиринты в файлы
for name, maze in configs:
save_maze(maze, os.path.join(MAZE_DIR, f'{name}.txt'))
rows = [['Лабиринт', 'Алгоритм', 'Время_мс', 'Посещено_клеток', 'Длина_пути',
'Стд_время_мс']]
for maze_name, maze in configs:
print(f'\n=== {maze_name} ({maze.width}×{maze.height}) ===')
for strategy in ALL_STRATEGIES:
print(f' [{strategy.name()}]', end=' ', flush=True)
res = run_bench(maze, strategy)
print(f"time={res['time_ms_mean']:.3f}ms visited={res['visited_mean']:.0f} "
f"path={res['path_length']}")
rows.append([
maze_name,
strategy.name(),
round(res['time_ms_mean'], 4),
round(res['visited_mean'], 1),
res['path_length'],
round(res['time_ms_std'], 4),
])
with open(OUT_CSV, 'w', newline='', encoding='utf-8') as f:
csv.writer(f).writerows(rows)
print(f'\nРезультаты сохранены: {OUT_CSV}')
return rows
# ============================================================================
# main.py — Точка входа
# ============================================================================
STRATEGIES = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy(),
}
STRATEGY_LABELS = {
'1': 'BFS (поиск в ширину)',
'2': 'DFS (поиск в глубину)',
'3': 'A* (эвристический)',
'4': 'Dijkstra (взвешенный)',
}
def choose_strategy() -> PathFindingStrategy:
print("\nВыберите алгоритм поиска:")
for k, label in STRATEGY_LABELS.items():
print(f" {k}. {label}")
choice = input("Ваш выбор [1-4, Enter=3]: ").strip() or '3'
return STRATEGIES.get(choice, AStarStrategy())
def interactive_walk(solver: MazeSolver, path: list, maze):
"""Пошаговое прохождение найденного пути командами."""
if not path:
print("Путь не найден, пошаговый режим недоступен.")
return
player = Player(maze.start)
history = CommandHistory()
step = 0
print("\n=== Пошаговый режим ===")
print("n — следующий шаг по пути | u — отмена | q — выход")
print(maze.render(path=set(path), player_pos=player.position))
while True:
cmd_input = input("\nКоманда: ").strip().lower()
if cmd_input == 'q':
break
elif cmd_input == 'n':
step += 1
if step < len(path):
cmd = MoveCommand(player, path[step], solver, path)
history.execute(cmd)
else:
print("Вы достигли выхода! 🎉")
elif cmd_input == 'u':
step = max(0, step - 1)
history.undo()
else:
print("Неизвестная команда.")
def demo_auto(size: int = 21):
"""Автоматическая демонстрация всех паттернов на сгенерированном лабиринте."""
print("=" * 60)
print(" Демонстрация: Поиск выхода из лабиринта")
print("=" * 60)
# Builder
print("\n[Builder] Генерация лабиринта...")
maze = generate_maze(size, size, seed=77)
# Observer + Strategy
strategy = choose_strategy()
solver = MazeSolver(maze, strategy)
view = ConsoleView()
solver.add_observer(view)
solver.notify('maze_loaded', {'maze': maze})
stats, path = solver.solve()
print(f"\n[MazeSolver] Статистика:")
print(f" Алгоритм: {stats.strategy_name}")
print(f" Время: {stats.elapsed_ms:.3f} мс")
print(f" Посещено клеток: {stats.visited_cells}")
print(f" Длина пути: {stats.path_length}")
# Command / пошаговый режим
choice = input("\nЗапустить пошаговый режим? [y/N]: ").strip().lower()
if choice == 'y':
interactive_walk(solver, path, maze)
# Strategy swap demo
print("\n[Strategy] Смена алгоритма на BFS без изменения кода...")
solver.set_strategy(BFSStrategy())
stats2, _ = solver.solve()
print(f" BFS: {stats2.elapsed_ms:.3f} мс, посещено: {stats2.visited_cells}")
# ============================================================================
# Запуск
# ============================================================================
if __name__ == '__main__':
import sys
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
run_experiment()
else:
demo_auto()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 KiB

View File

@ -1,279 +0,0 @@
# Задание 2 — Поиск выхода из лабиринта (ООП + паттерны GoF)
## 1. Описание задачи
Разработать расширяемую программу поиска пути в лабиринте с возможностью смены алгоритма, визуализации и пошагового управления. Применить минимум 3 паттерна GoF.
---
## 2. Применённые паттерны GoF
### 2.1 Builder (Строитель) — `builder.py`
**Проблема:** Создание `Maze` — многошаговый процесс: чтение файла → парсинг символов → валидация (есть ли S и E) → расстановка координат. Если делать всё в конструкторе `Maze` — нарушение Single Responsibility.
**Решение:** Интерфейс `MazeBuilder` с методом `build_from_file()`. Реализации:
- `TextFileMazeBuilder` — текстовый формат (`#`, пробел, `S`, `E`, `W`, `N`)
- `JsonMazeBuilder` — JSON-формат (для демонстрации расширяемости)
**Преимущество:** Добавление нового формата (бинарный, XML) не требует изменения `Maze`, `MazeSolver` или стратегий.
### 2.2 Strategy (Стратегия) — `strategy.py`
**Проблема:** Алгоритмы поиска (BFS, DFS, A*, Dijkstra) взаимозаменяемы по интерфейсу, но различаются реализацией. Жёсткая связь с конкретным алгоритмом нарушает Open/Closed Principle.
**Решение:** Интерфейс `PathFindingStrategy` с методом `find_path(maze, start, exit_)`. `MazeSolver` хранит ссылку на стратегию и вызывает `strategy.find_path()` — не зная, какой именно алгоритм работает. Метод `set_strategy()` меняет алгоритм в рантайме.
**Преимущество:** Новый алгоритм (IDA*, Bidirectional BFS) — это один новый класс без изменения остального кода.
### 2.3 Observer (Наблюдатель) — `solver.py`
**Проблема:** `MazeSolver` не должен знать о способе отображения. Консоль, GUI, файловый лог — разные задачи.
**Решение:** `MazeSolver` наследует `Subject` (хранит список `_observers`, метод `notify()`). При событиях (`maze_loaded`, `path_found`, `no_path`, `player_moved`) уведомляет всех наблюдателей. Реализации: `ConsoleView`, `FileLogObserver`.
**Преимущество:** GUI-интерфейс добавляется как новый `Observer` без изменения `MazeSolver`.
### 2.4 Command (Команда) — `solver.py`
**Проблема:** Пошаговое перемещение игрока должно поддерживать отмену (undo).
**Решение:** Интерфейс `Command` с методами `execute()` / `undo()`. `MoveCommand` хранит предыдущую позицию. `CommandHistory` — стек команд. Undo — `pop()` + `cmd.undo()`.
**Преимущество:** Любое новое действие (телепортация, открытие двери) реализуется как отдельная команда, не меняя `Player` или `MazeSolver`.
---
## 3. Диаграмма классов (Mermaid)
```mermaid
classDiagram
%% ── Model ──────────────────────────────────
class Cell {
+int x, y
+str symbol
+bool is_wall
+bool is_start
+bool is_exit
+int weight
+is_passable() bool
}
class Maze {
+int width, height
+Cell start, exit
+get_cell(x,y) Cell
+get_neighbors(cell) list
+render(path, visited, player) str
}
Maze "1" *-- "many" Cell
%% ── Builder ─────────────────────────────────
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
+build_from_string(text) Maze
+build_empty(w, h) Maze
}
class JsonMazeBuilder {
+build_from_file(filename) Maze
+build_empty(w, h) Maze
}
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder <|.. JsonMazeBuilder
TextFileMazeBuilder ..> Maze : creates
JsonMazeBuilder ..> Maze : creates
%% ── Strategy ────────────────────────────────
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) tuple
+name() str
}
class BFSStrategy { +find_path() tuple }
class DFSStrategy { +find_path() tuple }
class AStarStrategy { +find_path() tuple }
class DijkstraStrategy { +find_path() tuple }
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
%% ── Observer ────────────────────────────────
class Observer {
<<interface>>
+update(event, payload)
}
class Subject {
-observers list
+add_observer(obs)
+notify(event, payload)
}
class ConsoleView { +update(event, payload) }
class FileLogObserver { +update(event, payload) }
Observer <|.. ConsoleView
Observer <|.. FileLogObserver
Subject "1" o-- "many" Observer
%% ── MazeSolver ──────────────────────────────
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+set_strategy(strategy)
+load_maze(maze)
+solve() tuple
}
MazeSolver --|> Subject
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
%% ── Command ─────────────────────────────────
class Command {
<<interface>>
+execute()
+undo()
}
class MoveCommand {
-Player player
-Cell target, previous
+execute()
+undo()
}
class CommandHistory {
-stack list
+execute(cmd)
+undo()
}
class Player { +Cell position }
Command <|.. MoveCommand
CommandHistory o-- Command
MoveCommand --> Player
```
---
## 4. Структура файлов
```
maze/
├── model.py — Cell, Maze
├── builder.py — MazeBuilder, TextFileMazeBuilder, JsonMazeBuilder
├── strategy.py — PathFindingStrategy, BFS, DFS, A*, Dijkstra
├── solver.py — Observer, Subject, ConsoleView, MazeSolver, Command, Player
├── generator.py — Генератор лабиринтов (DFS-backtracker)
├── experiment.py — Экспериментальная часть, запись CSV
├── main.py — Интерактивная демонстрация
└── docs/
└── data/
├── maze_results.csv
├── maze_benchmark.png
├── path_lengths.png
└── mazes/ — текстовые файлы лабиринтов
```
---
## 5. Результаты экспериментов
### 5.1 Таблица результатов (N = 7 повторов, среднее)
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|---|---|---:|---:|---:|
| Маленький 11×11 | BFS | 0.077 | 37 | 29 |
| Маленький 11×11 | DFS | 0.059 | 29 | 29 |
| Маленький 11×11 | A* | 0.111 | 29 | 29 |
| Маленький 11×11 | Dijkstra | 0.115 | 37 | 29 |
| Средний 51×51 | BFS | 2.228 | 755 | 405 |
| Средний 51×51 | DFS | 0.935 | 423 | 405 |
| Средний 51×51 | A* | 2.343 | 663 | 405 |
| Средний 51×51 | Dijkstra | 2.528 | 755 | 405 |
| Большой 101×101 | BFS | 3.299 | 1533 | 993 |
| Большой 101×101 | DFS | 2.289 | 1061 | 993 |
| Большой 101×101 | A* | 5.948 | 1515 | 993 |
| Большой 101×101 | Dijkstra | 5.137 | 1533 | 993 |
| Пустой 52×52 | BFS | 5.859 | 2500 | 99 |
| Пустой 52×52 | DFS | 3.518 | 1275 | 1275 |
| Пустой 52×52 | A* | 10.803 | 2500 | 99 |
| Пустой 52×52 | Dijkstra | 10.227 | 2500 | 99 |
| Без выхода 21×21 | BFS | 0.213 | 105 | 0 |
| Без выхода 21×21 | DFS | 0.220 | 105 | 0 |
| Без выхода 21×21 | A* | 0.361 | 105 | 0 |
| Без выхода 21×21 | Dijkstra | 0.336 | 105 | 0 |
| Взвешенный 51×51 | BFS | 0.722 | 335 | 221 |
| Взвешенный 51×51 | DFS | 0.518 | 221 | 221 |
| Взвешенный 51×51 | A* | 0.970 | 278 | 221 |
| Взвешенный 51×51 | Dijkstra | 1.091 | 337 | 221 |
### 5.2 Графики
![Benchmark](data/maze_benchmark.png)
![Path Lengths](data/path_lengths.png)
---
## 6. Анализ алгоритмов
### BFS (поиск в ширину)
- **Гарантирует кратчайший путь** по количеству шагов.
- Посещает все клетки на расстоянии k до нахождения выхода на расстоянии k+1.
- На пустом лабиринте (максимум свободного пространства) посещает **все 2500 клеток** — это худший сценарий по памяти (хранит всю «волну»).
- Практически эквивалентен Dijkstra для равновесных лабиринтов.
### DFS (поиск в глубину)
- **Самый быстрый по времени** на лабиринтах с длинными коридорами.
- На правильно построенном лабиринте (DFS-backtracker) коридоры длинные → DFS «угадывает» направление и находит путь, посетив меньше клеток.
- На пустом поле даёт **огромный зигзагообразный путь** (1275 клеток вместо оптимального 99) — классическая проблема DFS.
- **Не гарантирует оптимальность**, зато экономит память (O(глубина) вместо O(ширина)).
### A* (с манхэттенской эвристикой)
- На лабиринтах с коридорами посещает **меньше клеток**, чем BFS, но при этом тоже гарантирует оптимальный путь.
- На пустом поле ведёт себя хуже BFS по времени из-за накладных расходов на приоритетную очередь (heapq).
- Манхэттенская эвристика работает хуже на лабиринтах с длинными обходами препятствий: оценка расстояния «по прямой» слишком оптимистична.
- **Выигрывает** когда карта большая и путь очевиден по направлению (прямые коридоры к выходу).
### Dijkstra
- На равновесных лабиринтах идентичен BFS, но медленнее из-за приоритетной очереди.
- **Незаменим на взвешенных картах**: учитывает клетки с весом 2 (песок) и 3 (болото) и находит **минимальный суммарный вес** пути, а не минимальное число шагов.
- На взвешенном лабиринте Dijkstra и A* могут выдать более короткий (по весу) путь, чем BFS, при одинаковой длине в клетках.
### Без выхода
- Все алгоритмы обошли все 105 достижимых клеток и вернули пустой путь — корректная обработка.
- Время идентично для всех алгоритмов (доминирует полный обход, а не структура поиска).
---
## 7. Выводы: ООП и паттерны
### Что дали паттерны
**Builder** отделил детали парсинга от модели. Без него клиент сам читал бы файл и вручную собирал `Maze` — хрупкий, нерасширяемый код. С Builder: добавление JSON-формата заняло 10 строк.
**Strategy** позволил сравнить 4 алгоритма, не меняя `MazeSolver`. Переключение алгоритма — одна строка `solver.set_strategy(new_algo)`. Без паттерна потребовался бы if/elif на 4 ветки в каждом методе.
**Observer** полностью развязал логику поиска и отображение. `MazeSolver` не знает, куда идут уведомления. Добавление `FileLogObserver` — новый класс из 5 строк, нулевые изменения в `MazeSolver`.
**Command** дал возможность реализовать undo за счёт хранения предыдущего состояния в объекте команды. Без паттерна — ручное сохранение/восстановление переменных.
### Что было бы сложно без ООП и паттернов
| Задача | Без паттернов | С паттернами |
|---|---|---|
| Добавить новый алгоритм | Правка MazeSolver + if/elif | Новый класс, наследующий интерфейс |
| Добавить формат файла | Правка загрузчика | Новый Builder |
| Добавить GUI | Вставка GUI-кода в логику поиска | Новый Observer |
| Отмена шага игрока | Ручное сохранение переменной | Command.undo() |
### Рекомендации по выбору алгоритма
| Задача | Рекомендация |
|---|---|
| Гарантированно кратчайший путь, равные веса | **BFS** |
| Быстро найти *какой-нибудь* путь (поиск с препятствиями) | **DFS** |
| Большая карта, путь по направлению к цели | **A\*** |
| Взвешенная карта (болото, песок, дорога) | **Dijkstra** или **A\*** с весовой эвристикой |
| Нужна гарантия оптимальности на взвешенном графе | **Dijkstra** |

View File

@ -1,25 +0,0 @@
Лабиринт,Алгоритм,Время_мс,Посещено_клеток,Длина_пути,Стд_время_мс
small_10x10,BFS,0.0772,37,29,0.0122
small_10x10,DFS,0.0588,29,29,0.0044
small_10x10,A*,0.1112,29,29,0.029
small_10x10,Dijkstra,0.1155,37,29,0.0135
medium_50x50,BFS,2.228,755,405,1.6518
medium_50x50,DFS,0.9346,423,405,0.0899
medium_50x50,A*,2.3432,663,405,0.125
medium_50x50,Dijkstra,2.5281,755,405,0.1245
large_100x100,BFS,3.2987,1533,993,0.148
large_100x100,DFS,2.2887,1061,993,0.0737
large_100x100,A*,5.9477,1515,993,1.2811
large_100x100,Dijkstra,5.1375,1533,993,0.1435
empty_50x50,BFS,5.8593,2500,99,0.1605
empty_50x50,DFS,3.5185,1275,1275,0.1171
empty_50x50,A*,10.8025,2500,99,0.0902
empty_50x50,Dijkstra,10.227,2500,99,0.1552
no_exit_20x20,BFS,0.213,105,0,0.0434
no_exit_20x20,DFS,0.2204,105,0,0.049
no_exit_20x20,A*,0.3615,105,0,0.0489
no_exit_20x20,Dijkstra,0.3361,105,0,0.033
weighted_50x50,BFS,0.7224,335,221,0.069
weighted_50x50,DFS,0.5183,221,221,0.0912
weighted_50x50,A*,0.97,278,221,0.0444
weighted_50x50,Dijkstra,1.0906,337,221,0.0414
1 Лабиринт Алгоритм Время_мс Посещено_клеток Длина_пути Стд_время_мс
2 small_10x10 BFS 0.0772 37 29 0.0122
3 small_10x10 DFS 0.0588 29 29 0.0044
4 small_10x10 A* 0.1112 29 29 0.029
5 small_10x10 Dijkstra 0.1155 37 29 0.0135
6 medium_50x50 BFS 2.228 755 405 1.6518
7 medium_50x50 DFS 0.9346 423 405 0.0899
8 medium_50x50 A* 2.3432 663 405 0.125
9 medium_50x50 Dijkstra 2.5281 755 405 0.1245
10 large_100x100 BFS 3.2987 1533 993 0.148
11 large_100x100 DFS 2.2887 1061 993 0.0737
12 large_100x100 A* 5.9477 1515 993 1.2811
13 large_100x100 Dijkstra 5.1375 1533 993 0.1435
14 empty_50x50 BFS 5.8593 2500 99 0.1605
15 empty_50x50 DFS 3.5185 1275 1275 0.1171
16 empty_50x50 A* 10.8025 2500 99 0.0902
17 empty_50x50 Dijkstra 10.227 2500 99 0.1552
18 no_exit_20x20 BFS 0.213 105 0 0.0434
19 no_exit_20x20 DFS 0.2204 105 0 0.049
20 no_exit_20x20 A* 0.3615 105 0 0.0489
21 no_exit_20x20 Dijkstra 0.3361 105 0 0.033
22 weighted_50x50 BFS 0.7224 335 221 0.069
23 weighted_50x50 DFS 0.5183 221 221 0.0912
24 weighted_50x50 A* 0.97 278 221 0.0444
25 weighted_50x50 Dijkstra 1.0906 337 221 0.0414

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

View File

@ -0,0 +1,35 @@
ОТЧЁТ ПО ЗАДАНИЮ 1
1. Влияние порядка данных на BST
При случайном порядке данных BST работает быстро (вставка ~0.005 сек).
При отсортированном порядке дерево вырождается в цепочку, и время вставки
возрастает примерно в 50–60 раз (~0.31 сек). Сложность деградирует с O(log n) до O(n).
2. Почему хеш-таблица нечувствительна к порядку
Хеш-таблица использует хеш-функцию, которая равномерно распределяет элементы
по бакетам. Поэтому порядок входных данных почти не влияет на скорость
вставки, поиска и удаления (в среднем O(1)).
3. Почему связный список медленен при поиске
Для поиска в связном списке нужно последовательно пройти все элементы.
Поэтому поиск всегда выполняется за O(n), независимо от порядка данных.
Это делает его самым медленным при операциях поиска и удаления.
4. Как работает удаление
- LinkedList: O(n) — нужно найти элемент и перестроить ссылки.
- HashTable: O(1) в среднем — удаление внутри нужного бакета.
- BST: O(log n) в среднем, O(n) в худшем — при двух потомках ищется
минимальный элемент в правом поддереве.
5. Вывод и рекомендации
Рекомендуемые структуры в зависимости от задачи:
- Частые вставки и поиск → HashTable (лучшая общая производительность)
- Нужно получать данные в отсортированном порядке → BST (только при случайных данных)
- Данные приходят отсортированными → HashTable (BST сильно деградирует)
- Малый объём данных и простота → LinkedList
Итог: Для большинства реальных задач лучше всего подходит хеш-таблица.
BST имеет смысл использовать только при случайном порядке данных и
необходимости частого получения отсортированного списка.

View File

@ -0,0 +1,169 @@
Отчёт ко 2 заданию
1. Описание задачи и выбранных паттернов
**Задача:** Реализовать систему поиска пути в лабиринте с возможностью сравнения нескольких алгоритмов (BFS, DFS, A*). Система должна поддерживать разные способы построения лабиринта и позволять легко добавлять новые алгоритмы поиска.
Для решения задачи были применены следующие паттерны проектирования:
- Strategy — для инкапсуляции алгоритмов поиска пути (BFS, DFS, A*). Позволяет динамически менять стратегию поиска.
- Builder — для построения лабиринта из файла. Отделяет процесс создания лабиринта от его представления.
Эти паттерны обеспечивают гибкость и расширяемость системы.
Диаграмма классов (Mermaid)
```mermaid
classDiagram
class Maze {
+width: int
+height: int
+cells: List~List~Cell~~
+start: Cell
+exit: Cell
+getCell(x, y)
+getNeighbors(cell)
}
class Cell {
+x: int
+y: int
+is_wall: bool
+is_start: bool
+is_exit: bool
+isPassable()
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit)
}
class BFSStrategy {
+findPath(maze, start, exit)
}
class DFSStrategy {
+findPath(maze, start, exit)
}
class AStarStrategy {
+findPath(maze, start, exit)
-heuristic(a, b)
}
class MazeSolver {
-maze: Maze
-strategy: PathFindingStrategy
+setStrategy(strategy)
+solve()
}
class MazeBuilder {
<<interface>>
+buildFromFile(filename)
}
class TextFileMazeBuilder {
+buildFromFile(filename)
}
Maze "1" *-- "many" Cell
MazeSolver --> PathFindingStrategy
PathFindingStrategy <|-- BFSStrategy
PathFindingStrategy <|-- DFSStrategy
PathFindingStrategy <|-- AStarStrategy
MazeBuilder <|-- TextFileMazeBuilder
```
2. Листинги ключевых классов
Ключевые классы (Strategy и MazeSolver):
```python
class PathFindingStrategy:
def findPath(self, maze, start, exit):
raise NotImplementedError
class BFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация BFS
...
class DFSStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация DFS
...
class AStarStrategy(PathFindingStrategy):
def findPath(self, maze, start, exit):
# реализация A*
...
```
```python
class MazeSolver:
def __init__(self, maze=None, strategy=None):
self.maze = maze
self.strategy = strategy
def setStrategy(self, strategy):
self.strategy = strategy
def solve(self):
if not self.maze or not self.strategy:
return None
# замер времени и вызов стратегии
...
```
Полный код доступен в репозитории (или может быть предоставлен по запросу).
3. Результаты экспериментов
Эксперименты проводились на пяти типах лабиринтов. Ниже представлены ключевые результаты.
Сводная таблица (средние значения):
| Лабиринт | Стратегия | Время (мс) | Посещено клеток | Длина пути |
|-------------------------|-----------|------------|------------------|------------|
| 10x10_simple | BFS | 0.07 | 90 | 37 |
| 10x10_simple | DFS | 0.03 | 67 | 37 |
| 10x10_simple | A* | 0.09 | 76 | 19 |
| 50x50_with_deadends | BFS | 1.29 | 1657 | 0 |
| 50x50_with_deadends | DFS | 0.64 | 993 | 243 |
| 50x50_with_deadends | A* | 0.56 | 440 | 101 |
| 100x100_complex | BFS | 4.40 | 5735 | 1 |
| 100x100_complex | DFS | 4.33 | 5735 | 1 |
| 100x100_complex | A* | 7.03 | 5735 | 1 |
| empty | BFS | 0.68 | 900 | 0 |
| empty | DFS | 0.39 | 900 | 465 |
| empty | A* | 1.04 | 900 | 59 |
Графики (сохранены в папке `results/`):
- `time_comparison.png` — сравнение времени работы алгоритмов
- `visited_comparison.png` — сравнение количества посещённых клеток
4. Анализ эффективности алгоритмов и применимости паттернов
- **BFS** показывает стабильную работу и находит кратчайший путь, но посещает больше клеток.
- **DFS** быстрее всех на простых и пустых лабиринтах, однако не гарантирует оптимальность.
- **A*** эффективнее всего по количеству посещённых клеток на сложных лабиринтах, но на больших картах проигрывает по времени из-за overhead приоритетной очереди.
Паттерн **Strategy** позволил легко переключаться между алгоритмами без изменения кода `MazeSolver`. Паттерн **Builder** сделал возможным добавление новых источников построения лабиринта (например, генератор случайных лабиринтов) без изменения основной логики.
5. Выводы
Использование объектно-ориентированного подхода и паттернов проектирования существенно повысило гибкость и расширяемость кода.
Преимущества:
- Благодаря паттерну **Strategy** добавление нового алгоритма поиска (например, Dijkstra) требует только реализации интерфейса `PathFindingStrategy` без изменения `MazeSolver`.
- Паттерн **Builder** позволяет легко подключать новые способы загрузки лабиринтов.
- Код стал более читаемым и поддерживаемым.
Что было бы сложно изменить без паттернов:
- Замена алгоритма поиска потребовала бы значительных изменений в классе `MazeSolver` (много условных операторов `if`).
- Добавление нового способа построения лабиринта привело бы к дублированию кода.
- Сравнительный эксперимент было бы гораздо сложнее проводить, так как алгоритмы не были бы унифицированы через общий интерфейс.
Таким образом, применение паттернов Strategy и Builder сделало систему легко расширяемой и удобной для проведения экспериментов.

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

258
Ezhovnd/задание 1.py Normal file
View File

@ -0,0 +1,258 @@
import time
import random
import csv
import os
import matplotlib.pyplot as plt
def ll_insert(head, name, phone):
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next']:
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
current = head
while current['next']:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
result = []
current = head
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def create_hash_table(size=200):
return [None] * size
def ht_insert(buckets, name, phone):
index = hash(name) % len(buckets)
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
index = hash(name) % len(buckets)
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash(name) % len(buckets)
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets):
result = []
for bucket in buckets:
current = bucket
while current:
result.append((current['name'], current['phone']))
current = current['next']
return sorted(result)
def bst_insert(root, name, phone):
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
return root
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
return root
current = current['right']
else:
current['phone'] = phone
return root
def bst_find(root, name):
current = root
while current:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_delete(root, name):
if root is None:
return None
parent = None
current = root
while current and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
if current['left'] is None or current['right'] is None:
child = current['left'] if current['left'] else current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
else:
parent_min = current
min_node = current['right']
while min_node['left']:
parent_min = min_node
min_node = min_node['left']
current['name'] = min_node['name']
current['phone'] = min_node['phone']
if parent_min['left'] == min_node:
parent_min['left'] = min_node['right']
else:
parent_min['right'] = min_node['right']
return root
def bst_list_all(root):
result = []
def inorder(node):
if node:
inorder(node['left'])
result.append((node['name'], node['phone']))
inorder(node['right'])
inorder(root)
return result
def generate_records(n=10000):
records = [(f"User_{i:05d}", f"8{random.randint(9000000000, 9999999999)}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiments():
random.seed(42)
records_shuffled, records_sorted = generate_records(10000)
all_results = []
structures = ["LinkedList", "HashTable", "BST"]
modes = [("случайный", records_shuffled), ("отсортированный", records_sorted)]
for mode_name, records in modes:
for struct_name in structures:
print(f"Тестируем: {struct_name} | Режим: {mode_name}")
for run in range(5):
if struct_name == "LinkedList":
data = None
elif struct_name == "HashTable":
data = create_hash_table(200)
else:
data = None
start = time.perf_counter()
for name, phone in records:
if struct_name == "LinkedList":
data = ll_insert(data, name, phone)
elif struct_name == "HashTable":
ht_insert(data, name, phone)
else:
data = bst_insert(data, name, phone)
insert_time = time.perf_counter() - start
test_names = [r[0] for r in random.sample(records, 100)]
test_names += [f"None_{i}" for i in range(10)]
start = time.perf_counter()
for name in test_names:
if struct_name == "LinkedList":
ll_find(data, name)
elif struct_name == "HashTable":
ht_find(data, name)
else:
bst_find(data, name)
find_time = time.perf_counter() - start
delete_names = [r[0] for r in random.sample(records, 50)]
start = time.perf_counter()
for name in delete_names:
if struct_name == "LinkedList":
data = ll_delete(data, name)
elif struct_name == "HashTable":
ht_delete(data, name)
else:
data = bst_delete(data, name)
delete_time = time.perf_counter() - start
all_results.append([struct_name, mode_name, "вставка", run + 1, insert_time])
all_results.append([struct_name, mode_name, "поиск", run + 1, find_time])
all_results.append([struct_name, mode_name, "удаление", run + 1, delete_time])
os.makedirs("docs/data", exist_ok=True)
filepath = "docs/data/results.csv"
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Структура", "Режим", "Операция", "Запуск", "Время (сек)"])
writer.writerows(all_results)
print(f"\nРезультаты сохранены в {filepath}")
return all_results
def plot_results(csv_path="docs/data/results.csv"):
import pandas as pd
df = pd.read_csv(csv_path)
summary = df.groupby(["Структура", "Режим", "Операция"])["Время (сек)"].mean().reset_index()
for op in ["вставка", "поиск", "удаление"]:
op_data = summary[summary["Операция"] == op]
plt.figure(figsize=(10, 6))
x_labels = []
y_values = []
for _, row in op_data.iterrows():
label = f"{row['Структура']}\n({row['Режим']})"
x_labels.append(label)
y_values.append(row["Время (сек)"])
plt.bar(x_labels, y_values, color=['#4C72B0', '#55A868', '#C44E52'] * 2)
plt.title(f"Среднее время операции: {op}")
plt.ylabel("Время (сек)")
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig(f"docs/data/graph_{op}.png")
print(f"График сохранён: docs/data/graph_{op}.png")
if __name__ == "__main__":
run_experiments()
plot_results()

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

16
Ezhovnd/табл.csv Normal file
View File

@ -0,0 +1,16 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
10x10_simple,BFS,0.0646,82,1
10x10_simple,DFS,0.0633,82,1
10x10_simple,AStar,0.0929,82,1
50x50_with_deadends,BFS,1.3632,1687,0
50x50_with_deadends,DFS,0.1943,400,205
50x50_with_deadends,AStar,0.6863,562,101
100x100_complex,BFS,4.8617,6060,1
100x100_complex,DFS,4.6471,6060,1
100x100_complex,AStar,7.4691,6060,1
empty,BFS,0.6954,900,0
empty,DFS,0.4106,900,465
empty,AStar,1.0604,900,59
no_exit,BFS,0.0017,1,1
no_exit,DFS,0.0009,1,1
no_exit,AStar,0.001,1,1
1 лабиринт стратегия время_мс посещено_клеток длина_пути
2 10x10_simple BFS 0.0646 82 1
3 10x10_simple DFS 0.0633 82 1
4 10x10_simple AStar 0.0929 82 1
5 50x50_with_deadends BFS 1.3632 1687 0
6 50x50_with_deadends DFS 0.1943 400 205
7 50x50_with_deadends AStar 0.6863 562 101
8 100x100_complex BFS 4.8617 6060 1
9 100x100_complex DFS 4.6471 6060 1
10 100x100_complex AStar 7.4691 6060 1
11 empty BFS 0.6954 900 0
12 empty DFS 0.4106 900 465
13 empty AStar 1.0604 900 59
14 no_exit BFS 0.0017 1 1
15 no_exit DFS 0.0009 1 1
16 no_exit AStar 0.001 1 1