Merge pull request '[2] zadanie2' (#231) from shahovaa/2026-rff_mp:zadanie2 into develop

Reviewed-on: UNN/2026-rff_mp#231
This commit is contained in:
AlexanderVah 2026-05-30 11:42:42 +00:00
commit d9a501b086
41 changed files with 5111 additions and 0 deletions

4
shahovaa/zadanie 2/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
__pycache__/
*.pyc
.pytest_cache/

View File

@ -0,0 +1,67 @@
# Поиск выхода из лабиринта
Объектно-ориентированная реализация поиска пути в лабиринте с паттернами GoF:
Builder, Strategy, Observer и Command.
## Что реализовано
- модель `Cell` и `Maze`;
- загрузка лабиринта из текстового файла через `TextFileMazeBuilder`;
- стратегии поиска пути: BFS, DFS, A*, Дейкстра;
- `MazeSolver`, который измеряет время, число посещенных клеток и длину пути;
- консольный `Observer` для сообщений и отрисовки;
- `MoveCommand` и `Player` для ручного режима с undo;
- генератор тестовых лабиринтов;
- экспериментальный скрипт, CSV и SVG-графики;
- отчет: `reports/report.md`.
## Формат лабиринта
```text
# - стена
- проход
S - старт
E - выход
2, 3, ~ - проходимые клетки с увеличенным весом
```
Все строки в файле лабиринта должны иметь одинаковую длину.
## Запуск
```bash
python3 scripts/generate_mazes.py
python3 main.py --maze data/mazes/small.txt --strategy astar --render
```
Доступные стратегии:
```text
bfs
dfs
astar
dijkstra
```
Ручной режим с командами `W/A/S/D`, undo через `Z`:
```bash
python3 main.py --maze data/mazes/small.txt --manual
```
## Эксперименты
```bash
python3 scripts/run_experiments.py
```
Скрипт перегенерирует лабиринты, запускает каждую стратегию 10 раз и сохраняет:
- `reports/results.csv`;
- SVG-графики в `reports/charts/`.
## Проверка
```bash
python3 -m unittest
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,94 @@
from __future__ import annotations
import argparse
from maze_solver import (
AStarStrategy,
BFSStrategy,
ConsoleView,
DFSStrategy,
DijkstraStrategy,
Direction,
MazeSolver,
MoveCommand,
Player,
TextFileMazeBuilder,
)
STRATEGIES = {
"bfs": BFSStrategy,
"dfs": DFSStrategy,
"astar": AStarStrategy,
"dijkstra": DijkstraStrategy,
}
def main() -> None:
parser = argparse.ArgumentParser(description="Find a path through a text maze.")
parser.add_argument("--maze", default="data/mazes/small.txt")
parser.add_argument(
"--strategy",
choices=sorted(STRATEGIES),
default="astar",
help="Path-finding algorithm.",
)
parser.add_argument("--render", action="store_true", help="Print maze with path.")
parser.add_argument(
"--manual",
action="store_true",
help="Manual W/A/S/D mode with Z undo and Q quit.",
)
args = parser.parse_args()
maze = TextFileMazeBuilder().build_from_file(args.maze)
strategy = STRATEGIES[args.strategy]()
solver = MazeSolver(maze, strategy)
view = ConsoleView()
solver.add_observer(view)
stats = solver.solve()
print(
f"Summary: strategy={stats.strategy_name}, time={stats.time_ms:.3f} ms, "
f"visited={stats.visited_cells}, path_length={stats.path_length}"
)
if args.render:
print(view.render(maze, path=stats.path))
if args.manual:
run_manual_mode(maze, view)
def run_manual_mode(maze, view: ConsoleView) -> None:
player = Player.at_start(maze)
history: list[MoveCommand] = []
while True:
print(view.render(maze, player_position=player.current_cell))
if player.current_cell == maze.exit:
print("Exit reached.")
return
key = input("Move W/A/S/D, undo Z, quit Q: ").strip().lower()
if key == "q":
return
if key == "z":
if history:
history.pop().undo()
continue
try:
command = MoveCommand(player, Direction.from_key(key))
except ValueError as exc:
print(exc)
continue
if command.execute():
history.append(command)
else:
print("Move blocked.")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,34 @@
from .builders import MazeBuilder, TextFileMazeBuilder
from .commands import Direction, MoveCommand, Player
from .models import Cell, Maze
from .observers import ConsoleView, Event, Observer
from .solver import MazeSolver, SearchStats
from .strategies import (
AStarStrategy,
BFSStrategy,
DFSStrategy,
DijkstraStrategy,
PathFindingStrategy,
PathResult,
)
__all__ = [
"AStarStrategy",
"BFSStrategy",
"Cell",
"ConsoleView",
"DFSStrategy",
"DijkstraStrategy",
"Direction",
"Event",
"Maze",
"MazeBuilder",
"MazeSolver",
"MoveCommand",
"Observer",
"PathFindingStrategy",
"PathResult",
"Player",
"SearchStats",
"TextFileMazeBuilder",
]

View File

@ -0,0 +1,75 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from pathlib import Path
from .models import Cell, Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str | Path) -> Maze:
raise NotImplementedError
def buildFromFile(self, filename: str | Path) -> Maze:
return self.build_from_file(filename)
class TextFileMazeBuilder(MazeBuilder):
WALL = "#"
START = "S"
EXIT = "E"
PASSAGES = {" ", "."}
WEIGHTS = {"1": 1, "2": 2, "3": 3, "~": 3}
def build_from_file(self, filename: str | Path) -> Maze:
path = Path(filename)
rows = path.read_text(encoding="utf-8").splitlines()
if not rows:
raise ValueError(f"Maze file is empty: {path}")
width = len(rows[0])
if width == 0:
raise ValueError("Maze width must be greater than zero")
if any(len(row) != width for row in rows):
raise ValueError("All maze rows must have the same width")
cells: list[list[Cell]] = []
start: Cell | None = None
exit: Cell | None = None
for y, row in enumerate(rows):
cell_row: list[Cell] = []
for x, char in enumerate(row):
cell = self._create_cell(x, y, char)
if cell.is_start:
if start is not None:
raise ValueError("Maze must contain exactly one start cell")
start = cell
if cell.is_exit:
if exit is not None:
raise ValueError("Maze must contain exactly one exit cell")
exit = cell
cell_row.append(cell)
cells.append(cell_row)
if start is None:
raise ValueError("Maze must contain a start cell marked with 'S'")
if exit is None:
raise ValueError("Maze must contain an exit cell marked with 'E'")
return Maze(cells, start, exit)
def _create_cell(self, x: int, y: int, char: str) -> Cell:
if char == self.WALL:
return Cell(x=x, y=y, is_wall=True, symbol=char)
if char == self.START:
return Cell(x=x, y=y, is_start=True, symbol=char)
if char == self.EXIT:
return Cell(x=x, y=y, is_exit=True, symbol=char)
if char in self.PASSAGES:
return Cell(x=x, y=y, symbol=" ")
if char in self.WEIGHTS:
return Cell(x=x, y=y, weight=self.WEIGHTS[char], symbol=char)
raise ValueError(f"Unsupported maze symbol {char!r} at ({x}, {y})")

View File

@ -0,0 +1,79 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum
from .models import Cell, Maze
class Direction(Enum):
UP = (0, -1)
RIGHT = (1, 0)
DOWN = (0, 1)
LEFT = (-1, 0)
@classmethod
def from_key(cls, key: str) -> "Direction":
mapping = {
"w": cls.UP,
"d": cls.RIGHT,
"s": cls.DOWN,
"a": cls.LEFT,
}
try:
return mapping[key.lower()]
except KeyError as exc:
raise ValueError("Use W/A/S/D for movement") from exc
@dataclass
class Player:
maze: Maze
current_cell: Cell
@classmethod
def at_start(cls, maze: Maze) -> "Player":
return cls(maze=maze, current_cell=maze.start)
def move_to(self, cell: Cell) -> None:
if not cell.is_passable():
raise ValueError("Player cannot move into a wall")
self.current_cell = cell
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
raise NotImplementedError
@abstractmethod
def undo(self) -> bool:
raise NotImplementedError
class MoveCommand(Command):
def __init__(self, player: Player, direction: Direction) -> None:
self.player = player
self.direction = direction
self.previous_cell: Cell | None = None
self.executed = False
def execute(self) -> bool:
dx, dy = self.direction.value
current = self.player.current_cell
target = self.player.maze.get_cell(current.x + dx, current.y + dy)
if target is None or not target.is_passable():
return False
self.previous_cell = current
self.player.move_to(target)
self.executed = True
return True
def undo(self) -> bool:
if not self.executed or self.previous_cell is None:
return False
self.player.move_to(self.previous_cell)
self.executed = False
return True

View File

@ -0,0 +1,81 @@
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True)
class Cell:
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
weight: int = 1
symbol: str = " "
def is_passable(self) -> bool:
return not self.is_wall
def isPassable(self) -> bool:
return self.is_passable()
class Maze:
def __init__(self, cells: list[list[Cell]], start: Cell, exit: Cell) -> None:
if not cells or not cells[0]:
raise ValueError("Maze must contain at least one cell")
width = len(cells[0])
if any(len(row) != width for row in cells):
raise ValueError("Maze rows must have equal width")
self.cells = cells
self.height = len(cells)
self.width = width
self.start = start
self.exit = exit
def get_cell(self, x: int, y: int) -> Cell | None:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def getCell(self, x: int, y: int) -> Cell | None:
return self.get_cell(x, y)
def get_neighbors(self, cell: Cell) -> list[Cell]:
neighbors: list[Cell] = []
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
neighbor = self.get_cell(cell.x + dx, cell.y + dy)
if neighbor is not None and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
def getNeighbors(self, cell: Cell) -> list[Cell]:
return self.get_neighbors(cell)
def to_text(self, path: list[Cell] | None = None, player: Cell | None = None) -> str:
path_cells = {(cell.x, cell.y) for cell in path or []}
lines: list[str] = []
for row in self.cells:
chars: list[str] = []
for cell in row:
position = (cell.x, cell.y)
if player is not None and position == (player.x, player.y):
chars.append("@")
elif cell.is_start:
chars.append("S")
elif cell.is_exit:
chars.append("E")
elif cell.is_wall:
chars.append("#")
elif position in path_cells:
chars.append(".")
elif cell.weight > 1:
chars.append(str(cell.weight))
else:
chars.append(" ")
lines.append("".join(chars))
return "\n".join(lines)

View File

@ -0,0 +1,40 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import Any
from .models import Cell, Maze
@dataclass(frozen=True)
class Event:
event_type: str
payload: dict[str, Any] = field(default_factory=dict)
class Observer(ABC):
@abstractmethod
def update(self, event: Event) -> None:
raise NotImplementedError
class ConsoleView(Observer):
def update(self, event: Event) -> None:
if event.event_type == "search_started":
print(f"Search started: {event.payload['strategy']}")
elif event.event_type in {"path_found", "path_not_found"}:
stats = event.payload["stats"]
print(
f"{event.event_type}: strategy={stats.strategy_name}, "
f"time={stats.time_ms:.3f} ms, visited={stats.visited_cells}, "
f"path_length={stats.path_length}"
)
def render(
self,
maze: Maze,
player_position: Cell | None = None,
path: list[Cell] | None = None,
) -> str:
return maze.to_text(path=path, player=player_position)

View File

@ -0,0 +1,57 @@
from __future__ import annotations
import time
from dataclasses import dataclass
from .models import Cell, Maze
from .observers import Event, Observer
from .strategies import PathFindingStrategy
@dataclass(frozen=True)
class SearchStats:
strategy_name: str
time_ms: float
visited_cells: int
path_length: int
path: list[Cell]
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy) -> None:
self.maze = maze
self.strategy = strategy
self._observers: list[Observer] = []
def set_strategy(self, strategy: PathFindingStrategy) -> None:
self.strategy = strategy
def setStrategy(self, strategy: PathFindingStrategy) -> None:
self.set_strategy(strategy)
def add_observer(self, observer: Observer) -> None:
self._observers.append(observer)
def remove_observer(self, observer: Observer) -> None:
self._observers.remove(observer)
def solve(self) -> SearchStats:
self._notify(Event("search_started", {"strategy": self.strategy.name}))
started_at = time.perf_counter()
result = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
elapsed_ms = (time.perf_counter() - started_at) * 1000
stats = SearchStats(
strategy_name=self.strategy.name,
time_ms=elapsed_ms,
visited_cells=result.visited_count,
path_length=len(result.path),
path=result.path,
)
event_name = "path_found" if result.path else "path_not_found"
self._notify(Event(event_name, {"stats": stats}))
return stats
def _notify(self, event: Event) -> None:
for observer in self._observers:
observer.update(event)

View File

@ -0,0 +1,150 @@
from __future__ import annotations
import heapq
from abc import ABC, abstractmethod
from collections import deque
from dataclasses import dataclass
from itertools import count
from .models import Cell, Maze
@dataclass(frozen=True)
class PathResult:
path: list[Cell]
visited_count: int
class PathFindingStrategy(ABC):
name = "abstract"
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
raise NotImplementedError
def findPath(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
return self.find_path(maze, start, exit)
class BFSStrategy(PathFindingStrategy):
name = "BFS"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
queue: deque[Cell] = deque([start])
parents: dict[Cell, Cell | None] = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
queue.append(neighbor)
return PathResult([], len(visited))
class DFSStrategy(PathFindingStrategy):
name = "DFS"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
stack = [start]
parents: dict[Cell, Cell | None] = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in reversed(maze.get_neighbors(current)):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
stack.append(neighbor)
return PathResult([], len(visited))
class DijkstraStrategy(PathFindingStrategy):
name = "Dijkstra"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
tie_breaker = count()
heap: list[tuple[int, int, Cell]] = [(0, next(tie_breaker), start)]
distances: dict[Cell, int] = {start: 0}
parents: dict[Cell, Cell | None] = {start: None}
visited: set[Cell] = set()
while heap:
current_distance, _, current = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
new_distance = current_distance + neighbor.weight
if new_distance < distances.get(neighbor, 10**12):
distances[neighbor] = new_distance
parents[neighbor] = current
heapq.heappush(heap, (new_distance, next(tie_breaker), neighbor))
return PathResult([], len(visited))
class AStarStrategy(PathFindingStrategy):
name = "A*"
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> PathResult:
tie_breaker = count()
start_heuristic = _manhattan(start, exit)
heap: list[tuple[int, int, int, Cell]] = [
(start_heuristic, start_heuristic, next(tie_breaker), start)
]
g_score: dict[Cell, int] = {start: 0}
parents: dict[Cell, Cell | None] = {start: None}
visited: set[Cell] = set()
while heap:
_, _, _, current = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == exit:
return PathResult(_reconstruct_path(parents, exit), len(visited))
for neighbor in maze.get_neighbors(current):
tentative_score = g_score[current] + neighbor.weight
if tentative_score < g_score.get(neighbor, 10**12):
g_score[neighbor] = tentative_score
parents[neighbor] = current
heuristic = _manhattan(neighbor, exit)
priority = tentative_score + heuristic
heapq.heappush(
heap,
(priority, heuristic, next(tie_breaker), neighbor),
)
return PathResult([], len(visited))
def _reconstruct_path(parents: dict[Cell, Cell | None], end: Cell) -> list[Cell]:
path: list[Cell] = []
current: Cell | None = end
while current is not None:
path.append(current)
current = parents[current]
path.reverse()
return path
def _manhattan(first: Cell, second: Cell) -> int:
return abs(first.x - second.x) + abs(first.y - second.y)

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Пустой 50x50: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.01</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.02</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">3.03</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4.04</text>
<rect x="109.0" y="124.5" width="96.0" height="177.5" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="116.5" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.89</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="293.4" width="96.0" height="8.6" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="285.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.14</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="287.4" width="96.0" height="14.6" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="279.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.24</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4.04</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Пустой 50x50: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">576.00</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1152.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1728.00</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2304.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2304.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="281.9" width="96.0" height="20.1" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="273.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">187.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="291.8" width="96.0" height="10.2" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="283.8" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">95.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2304.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Большой 100x100: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.16</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4.33</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">6.49</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">8.66</text>
<rect x="109.0" y="150.2" width="96.0" height="151.8" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="142.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">5.30</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="230.3" width="96.0" height="71.7" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="222.3" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.50</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">8.66</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="97.1" width="96.0" height="204.9" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="89.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">7.15</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Большой 100x100: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1200.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2400.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">3600.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">4801.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4801.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="190.7" width="96.0" height="111.3" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="182.7" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2155.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.5" width="96.0" height="247.5" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="46.5" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4791.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.1" width="96.0" height="247.9" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">4800.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Средний 50x50: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.50</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.51</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">2.01</text>
<rect x="109.0" y="144.4" width="96.0" height="157.6" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="136.4" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.28</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="189.6" width="96.0" height="112.4" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="181.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.91</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">2.01</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="91.6" width="96.0" height="210.4" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="83.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.70</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Средний 50x50: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">287.75</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">575.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">863.25</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1151.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1151.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="133.1" width="96.0" height="168.9" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="125.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">784.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="57.9" width="96.0" height="244.1" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="49.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1133.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1151.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Без пути 30x30: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<rect x="109.0" y="95.3" width="96.0" height="206.7" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="87.3" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="122.9" width="96.0" height="179.1" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="114.9" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="81.6" width="96.0" height="220.4" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="73.6" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Без пути 30x30: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">1.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">1.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Маленький 10x10: среднее время, мс</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.02</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.03</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.05</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.07</text>
<rect x="109.0" y="147.0" width="96.0" height="155.0" fill="#2f6fbb" rx="3"/>
<text x="157.0" y="139.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.04</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="202.0" width="96.0" height="100.0" fill="#2f6fbb" rx="3"/>
<text x="327.0" y="194.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.03</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="54.0" width="96.0" height="248.0" fill="#2f6fbb" rx="3"/>
<text x="497.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.07</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="100.2" width="96.0" height="201.8" fill="#2f6fbb" rx="3"/>
<text x="667.0" y="92.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">0.06</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,28 @@
<svg xmlns="http://www.w3.org/2000/svg" width="780" height="360" viewBox="0 0 780 360">
<rect width="100%" height="100%" fill="#ffffff"/>
<text x="72" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">Маленький 10x10: посещенные клетки</text>
<line x1="72" y1="302" x2="752" y2="302" stroke="#9aa5b1"/>
<line x1="72" y1="54" x2="72" y2="302" stroke="#9aa5b1"/>
<line x1="67" y1="302.0" x2="752" y2="302.0" stroke="#edf0f2"/>
<text x="62" y="306.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">0.00</text>
<line x1="67" y1="240.0" x2="752" y2="240.0" stroke="#edf0f2"/>
<text x="62" y="244.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">9.25</text>
<line x1="67" y1="178.0" x2="752" y2="178.0" stroke="#edf0f2"/>
<text x="62" y="182.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">18.50</text>
<line x1="67" y1="116.0" x2="752" y2="116.0" stroke="#edf0f2"/>
<text x="62" y="120.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">27.75</text>
<line x1="67" y1="54.0" x2="752" y2="54.0" stroke="#edf0f2"/>
<text x="62" y="58.0" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">37.00</text>
<rect x="109.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="157.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">37.00</text>
<text x="157.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">BFS</text>
<rect x="279.0" y="141.1" width="96.0" height="160.9" fill="#2f8f5b" rx="3"/>
<text x="327.0" y="133.1" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">24.00</text>
<text x="327.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">DFS</text>
<rect x="449.0" y="94.2" width="96.0" height="207.8" fill="#2f8f5b" rx="3"/>
<text x="497.0" y="86.2" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">31.00</text>
<text x="497.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">A*</text>
<rect x="619.0" y="54.0" width="96.0" height="248.0" fill="#2f8f5b" rx="3"/>
<text x="667.0" y="46.0" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">37.00</text>
<text x="667.0" y="326" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">Dijkstra</text>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,208 @@
# Отчет по заданию: поиск выхода из лабиринта
## 1. Описание задачи и выбранных паттернов
Цель работы - реализовать расширяемую программу для загрузки лабиринта из файла,
поиска пути от старта `S` до выхода `E`, визуализации результата и сравнения
алгоритмов на лабиринтах разной сложности.
В проекте реализованы четыре паттерна GoF:
| Паттерн | Где реализован | Зачем нужен |
|---|---|---|
| Builder | `MazeBuilder`, `TextFileMazeBuilder` | Изолирует парсинг и валидацию файла от остального приложения. |
| Strategy | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy`, `DijkstraStrategy` | Позволяет менять алгоритм поиска без изменения `MazeSolver`. |
| Observer | `Observer`, `ConsoleView`, события `search_started`, `path_found`, `path_not_found` | Отделяет вычисления от отображения в консоли. |
| Command | `Command`, `MoveCommand`, `Player` | Инкапсулирует ход игрока и поддерживает отмену хода. |
Диаграмма классов:
```mermaid
classDiagram
class Cell {
+int x
+int y
+bool is_wall
+bool is_start
+bool is_exit
+int weight
+is_passable() bool
}
class Maze {
+list cells
+int width
+int height
+Cell start
+Cell exit
+get_cell(x, y) Cell
+get_neighbors(cell) list
+to_text(path, player) str
}
class MazeBuilder {
<<interface>>
+build_from_file(filename) Maze
}
class TextFileMazeBuilder {
+build_from_file(filename) Maze
}
class PathFindingStrategy {
<<interface>>
+find_path(maze, start, exit) PathResult
}
class BFSStrategy
class DFSStrategy
class AStarStrategy
class DijkstraStrategy
class SearchStats {
+str strategy_name
+float time_ms
+int visited_cells
+int path_length
+list path
}
class MazeSolver {
+set_strategy(strategy)
+add_observer(observer)
+solve() SearchStats
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player_position, path) str
}
class Command {
<<interface>>
+execute() bool
+undo() bool
}
class MoveCommand
class Player
MazeBuilder <|.. TextFileMazeBuilder
MazeBuilder --> Maze : creates
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
PathFindingStrategy <|.. DijkstraStrategy
MazeSolver --> PathFindingStrategy : uses
MazeSolver --> Maze : uses
MazeSolver --> Observer : notifies
Observer <|.. ConsoleView
Command <|.. MoveCommand
MoveCommand --> Player
Player --> Cell
```
## 2. Ключевые классы
Основные файлы проекта:
| Файл | Назначение |
|---|---|
| `maze_solver/models.py` | Классы `Cell` и `Maze`, поиск соседей, текстовая отрисовка. |
| `maze_solver/builders.py` | Интерфейс Builder и загрузка лабиринта из `.txt`. |
| `maze_solver/strategies.py` | BFS, DFS, A* и Дейкстра. |
| `maze_solver/solver.py` | Оркестратор поиска и сбор статистики. |
| `maze_solver/observers.py` | Observer и консольное представление. |
| `maze_solver/commands.py` | Command, игрок и undo перемещения. |
| `main.py` | CLI для запуска поиска и ручного режима. |
| `scripts/run_experiments.py` | Замеры и построение SVG-графиков. |
Пример запуска:
```bash
python3 main.py --maze data/mazes/small.txt --strategy astar --render
```
## 3. Результаты экспериментов
Для каждого лабиринта и каждой стратегии выполнено 10 запусков. В таблице указаны
средние значения. Длина пути считается в клетках, включая старт и выход.
| Лабиринт | Стратегия | Время, мс | Посещено клеток | Длина пути | Путь найден |
|---|---:|---:|---:|---:|---|
| Маленький 10x10 | BFS | 0.0423 | 37.0 | 20.0 | да |
| Маленький 10x10 | DFS | 0.0273 | 24.0 | 22.0 | да |
| Маленький 10x10 | A* | 0.0677 | 31.0 | 20.0 | да |
| Маленький 10x10 | Dijkstra | 0.0551 | 37.0 | 20.0 | да |
| Средний 50x50 | BFS | 1.2769 | 1151.0 | 709.0 | да |
| Средний 50x50 | DFS | 0.9106 | 784.0 | 709.0 | да |
| Средний 50x50 | A* | 2.0089 | 1133.0 | 709.0 | да |
| Средний 50x50 | Dijkstra | 1.7041 | 1151.0 | 709.0 | да |
| Большой 100x100 | BFS | 5.2983 | 4801.0 | 1685.0 | да |
| Большой 100x100 | DFS | 2.5044 | 2155.0 | 1685.0 | да |
| Большой 100x100 | A* | 8.6574 | 4791.0 | 1685.0 | да |
| Большой 100x100 | Dijkstra | 7.1532 | 4800.0 | 1685.0 | да |
| Пустой 50x50 | BFS | 2.8927 | 2304.0 | 95.0 | да |
| Пустой 50x50 | DFS | 0.1404 | 187.0 | 95.0 | да |
| Пустой 50x50 | A* | 0.2374 | 95.0 | 95.0 | да |
| Пустой 50x50 | Dijkstra | 4.0408 | 2304.0 | 95.0 | да |
| Без пути 30x30 | BFS | 0.0015 | 1.0 | 0.0 | нет |
| Без пути 30x30 | DFS | 0.0013 | 1.0 | 0.0 | нет |
| Без пути 30x30 | A* | 0.0016 | 1.0 | 0.0 | нет |
| Без пути 30x30 | Dijkstra | 0.0018 | 1.0 | 0.0 | нет |
CSV с результатами сохранен в `reports/results.csv`.
Графики:
| Лабиринт | Время | Посещенные клетки |
|---|---|---|
| Маленький | ![](charts/small_time.svg) | ![](charts/small_visited.svg) |
| Средний | ![](charts/medium_time.svg) | ![](charts/medium_visited.svg) |
| Большой | ![](charts/large_time.svg) | ![](charts/large_visited.svg) |
| Пустой | ![](charts/empty_time.svg) | ![](charts/empty_visited.svg) |
| Без пути | ![](charts/no_exit_time.svg) | ![](charts/no_exit_visited.svg) |
## 4. Анализ эффективности
BFS гарантирует кратчайший путь в невзвешенном лабиринте. Это видно на маленьком
лабиринте: BFS, A* и Дейкстра нашли путь длиной 20, а DFS нашел более длинный путь
длиной 22. Недостаток BFS - широкий фронт поиска, из-за чего в пустом лабиринте он
посетил все 2304 доступные клетки.
DFS не гарантирует кратчайший путь, но часто работает быстро, потому что уходит
глубоко по одному направлению. На маленьком лабиринте это дало путь хуже оптимального.
На сгенерированных идеальных лабиринтах путь между двумя клетками единственный, поэтому
DFS, BFS, A* и Дейкстра получили одинаковую длину пути.
A* использует манхэттенскую эвристику. На пустом лабиринте он посетил только 95 клеток,
то есть фактически прошел по оптимальному маршруту. В запутанных идеальных лабиринтах
эвристика помогает слабее: прямое направление к выходу часто упирается в стены, поэтому
A* посещает почти столько же клеток, сколько BFS, а из-за приоритетной очереди тратит
больше времени.
Дейкстра в невзвешенном лабиринте по результату близок к BFS, но работает медленнее
из-за приоритетной очереди. Его преимущество проявляется при взвешенных клетках.
В проекте Builder уже поддерживает символы `2`, `3` и `~` как клетки с повышенной
стоимостью прохода, поэтому Дейкстру и A* можно использовать для дополнительного
сравнения на взвешенных картах.
Лабиринт "Без пути" проверяет корректную обработку отсутствия решения: стратегии
возвращают пустой путь, а `MazeSolver` фиксирует длину 0.
## 5. Выводы
ООП позволило разделить предметную модель, загрузку данных, алгоритмы и интерфейс.
Паттерн Builder делает формат входного файла заменяемым: можно добавить JSON-builder,
не меняя `Maze` и стратегии. Strategy позволяет добавлять новые алгоритмы без правок
в `MazeSolver`. Observer отделяет вычисления от вывода, а Command показывает, как
инкапсулировать пользовательские действия и поддержать undo.
Без этих паттернов код быстро стал бы монолитным: парсинг файла, поиск, статистика,
печать и ручное управление оказались бы в одном месте. Тогда добавление нового формата,
алгоритма или режима отображения требовало бы менять уже работающую логику.

View File

@ -0,0 +1,21 @@
лабиринт,стратегия,время_мс,посещено_клеток,длина_пути,путь_найден,запусков
Маленький 10x10,BFS,0.0423,37.0,20.0,да,10
Маленький 10x10,DFS,0.0273,24.0,22.0,да,10
Маленький 10x10,A*,0.0677,31.0,20.0,да,10
Маленький 10x10,Dijkstra,0.0551,37.0,20.0,да,10
Средний 50x50,BFS,1.2769,1151.0,709.0,да,10
Средний 50x50,DFS,0.9106,784.0,709.0,да,10
Средний 50x50,A*,2.0089,1133.0,709.0,да,10
Средний 50x50,Dijkstra,1.7041,1151.0,709.0,да,10
Большой 100x100,BFS,5.2983,4801.0,1685.0,да,10
Большой 100x100,DFS,2.5044,2155.0,1685.0,да,10
Большой 100x100,A*,8.6574,4791.0,1685.0,да,10
Большой 100x100,Dijkstra,7.1532,4800.0,1685.0,да,10
Пустой 50x50,BFS,2.8927,2304.0,95.0,да,10
Пустой 50x50,DFS,0.1404,187.0,95.0,да,10
Пустой 50x50,A*,0.2374,95.0,95.0,да,10
Пустой 50x50,Dijkstra,4.0408,2304.0,95.0,да,10
Без пути 30x30,BFS,0.0015,1.0,0.0,нет,10
Без пути 30x30,DFS,0.0013,1.0,0.0,нет,10
Без пути 30x30,A*,0.0016,1.0,0.0,нет,10
Без пути 30x30,Dijkstra,0.0018,1.0,0.0,нет,10
1 лабиринт стратегия время_мс посещено_клеток длина_пути путь_найден запусков
2 Маленький 10x10 BFS 0.0423 37.0 20.0 да 10
3 Маленький 10x10 DFS 0.0273 24.0 22.0 да 10
4 Маленький 10x10 A* 0.0677 31.0 20.0 да 10
5 Маленький 10x10 Dijkstra 0.0551 37.0 20.0 да 10
6 Средний 50x50 BFS 1.2769 1151.0 709.0 да 10
7 Средний 50x50 DFS 0.9106 784.0 709.0 да 10
8 Средний 50x50 A* 2.0089 1133.0 709.0 да 10
9 Средний 50x50 Dijkstra 1.7041 1151.0 709.0 да 10
10 Большой 100x100 BFS 5.2983 4801.0 1685.0 да 10
11 Большой 100x100 DFS 2.5044 2155.0 1685.0 да 10
12 Большой 100x100 A* 8.6574 4791.0 1685.0 да 10
13 Большой 100x100 Dijkstra 7.1532 4800.0 1685.0 да 10
14 Пустой 50x50 BFS 2.8927 2304.0 95.0 да 10
15 Пустой 50x50 DFS 0.1404 187.0 95.0 да 10
16 Пустой 50x50 A* 0.2374 95.0 95.0 да 10
17 Пустой 50x50 Dijkstra 4.0408 2304.0 95.0 да 10
18 Без пути 30x30 BFS 0.0015 1.0 0.0 нет 10
19 Без пути 30x30 DFS 0.0013 1.0 0.0 нет 10
20 Без пути 30x30 A* 0.0016 1.0 0.0 нет 10
21 Без пути 30x30 Dijkstra 0.0018 1.0 0.0 нет 10

View File

@ -0,0 +1 @@
"""Helper scripts for maze generation and experiments."""

View File

@ -0,0 +1,126 @@
from __future__ import annotations
import random
from collections import deque
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
MAZE_DIR = ROOT / "data" / "mazes"
def main() -> None:
generate_all()
def generate_all() -> None:
MAZE_DIR.mkdir(parents=True, exist_ok=True)
_write("small.txt", _small_maze())
_write("medium.txt", _perfect_maze(50, 50, seed=2026))
_write("large.txt", _perfect_maze(100, 100, seed=2027))
_write("empty.txt", _empty_maze(50, 50))
_write("no_exit.txt", _no_path_maze(30, 30))
def _write(filename: str, rows: list[str]) -> None:
(MAZE_DIR / filename).write_text("\n".join(rows) + "\n", encoding="utf-8")
def _small_maze() -> list[str]:
return [
"##########",
"#S #E#",
"# #### # #",
"# # # #",
"# # #### #",
"# # #",
"# ###### #",
"# #",
"######## #",
"##########",
]
def _empty_maze(width: int, height: int) -> list[str]:
grid = _bordered_grid(width, height, fill=" ")
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return _to_rows(grid)
def _no_path_maze(width: int, height: int) -> list[str]:
grid = [["#" for _ in range(width)] for _ in range(height)]
grid[1][1] = "S"
grid[height - 2][width - 2] = "E"
return _to_rows(grid)
def _perfect_maze(width: int, height: int, seed: int) -> list[str]:
if width < 5 or height < 5:
raise ValueError("Maze must be at least 5x5")
randomizer = random.Random(seed)
grid = [["#" for _ in range(width)] for _ in range(height)]
start = (1, 1)
stack = [start]
grid[start[1]][start[0]] = " "
while stack:
x, y = stack[-1]
candidates = []
for dx, dy in ((0, -2), (2, 0), (0, 2), (-2, 0)):
nx, ny = x + dx, y + dy
if 1 <= nx < width - 1 and 1 <= ny < height - 1 and grid[ny][nx] == "#":
candidates.append((nx, ny, dx, dy))
if not candidates:
stack.pop()
continue
nx, ny, dx, dy = randomizer.choice(candidates)
grid[y + dy // 2][x + dx // 2] = " "
grid[ny][nx] = " "
stack.append((nx, ny))
exit_x, exit_y = _farthest_open_cell(grid, start)
grid[start[1]][start[0]] = "S"
grid[exit_y][exit_x] = "E"
return _to_rows(grid)
def _farthest_open_cell(grid: list[list[str]], start: tuple[int, int]) -> tuple[int, int]:
queue = deque([start])
distances = {start: 0}
farthest = start
while queue:
x, y = queue.popleft()
if distances[(x, y)] > distances[farthest]:
farthest = (x, y)
for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)):
nx, ny = x + dx, y + dy
if (nx, ny) not in distances and grid[ny][nx] != "#":
distances[(nx, ny)] = distances[(x, y)] + 1
queue.append((nx, ny))
return farthest
def _bordered_grid(width: int, height: int, fill: str) -> list[list[str]]:
grid = [[fill for _ in range(width)] for _ in range(height)]
for x in range(width):
grid[0][x] = "#"
grid[height - 1][x] = "#"
for y in range(height):
grid[y][0] = "#"
grid[y][width - 1] = "#"
return grid
def _to_rows(grid: list[list[str]]) -> list[str]:
return ["".join(row) for row in grid]
if __name__ == "__main__":
main()

View File

@ -0,0 +1,194 @@
from __future__ import annotations
import csv
import statistics
import sys
from collections import defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from maze_solver import ( # noqa: E402
AStarStrategy,
BFSStrategy,
DFSStrategy,
DijkstraStrategy,
MazeSolver,
TextFileMazeBuilder,
)
from scripts.generate_mazes import generate_all # noqa: E402
MAZES = [
("small", "Маленький 10x10", ROOT / "data" / "mazes" / "small.txt"),
("medium", "Средний 50x50", ROOT / "data" / "mazes" / "medium.txt"),
("large", "Большой 100x100", ROOT / "data" / "mazes" / "large.txt"),
("empty", "Пустой 50x50", ROOT / "data" / "mazes" / "empty.txt"),
("no_exit", "Без пути 30x30", ROOT / "data" / "mazes" / "no_exit.txt"),
]
STRATEGIES = [BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy]
REPORTS_DIR = ROOT / "reports"
CHARTS_DIR = REPORTS_DIR / "charts"
def main(runs: int = 10) -> None:
generate_all()
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
CHARTS_DIR.mkdir(parents=True, exist_ok=True)
rows = _run_experiments(runs)
_write_csv(rows)
_write_charts(rows)
print(f"Wrote {REPORTS_DIR / 'results.csv'}")
print(f"Wrote SVG charts to {CHARTS_DIR}")
def _run_experiments(runs: int) -> list[dict[str, object]]:
builder = TextFileMazeBuilder()
rows: list[dict[str, object]] = []
for maze_key, maze_name, maze_path in MAZES:
maze = builder.build_from_file(maze_path)
for strategy_type in STRATEGIES:
measurements = []
for _ in range(runs):
stats = MazeSolver(maze, strategy_type()).solve()
measurements.append(stats)
avg_time = statistics.fmean(item.time_ms for item in measurements)
avg_visited = statistics.fmean(item.visited_cells for item in measurements)
avg_path = statistics.fmean(item.path_length for item in measurements)
found = measurements[-1].path_length > 0
rows.append(
{
"key": maze_key,
"лабиринт": maze_name,
"стратегия": measurements[-1].strategy_name,
"время_мс": f"{avg_time:.4f}",
"посещено_клеток": f"{avg_visited:.1f}",
"длина_пути": f"{avg_path:.1f}",
"путь_найден": "да" if found else "нет",
"запусков": runs,
}
)
return rows
def _write_csv(rows: list[dict[str, object]]) -> None:
csv_path = REPORTS_DIR / "results.csv"
headers = [
"лабиринт",
"стратегия",
"время_мс",
"посещено_клеток",
"длина_пути",
"путь_найден",
"запусков",
]
with csv_path.open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=headers)
writer.writeheader()
for row in rows:
writer.writerow({header: row[header] for header in headers})
def _write_charts(rows: list[dict[str, object]]) -> None:
grouped: dict[str, list[dict[str, object]]] = defaultdict(list)
for row in rows:
grouped[str(row["key"])].append(row)
for maze_key, group in grouped.items():
title = str(group[0]["лабиринт"])
_write_bar_chart(
CHARTS_DIR / f"{maze_key}_time.svg",
title=f"{title}: среднее время, мс",
rows=group,
metric="время_мс",
color="#2f6fbb",
)
_write_bar_chart(
CHARTS_DIR / f"{maze_key}_visited.svg",
title=f"{title}: посещенные клетки",
rows=group,
metric="посещено_клеток",
color="#2f8f5b",
)
def _write_bar_chart(
path: Path,
title: str,
rows: list[dict[str, object]],
metric: str,
color: str,
) -> None:
width = 780
height = 360
left = 72
right = 28
top = 54
bottom = 58
chart_width = width - left - right
chart_height = height - top - bottom
values = [float(row[metric]) for row in rows]
max_value = max(values) if values else 1.0
max_value = max_value or 1.0
bar_area = chart_width / len(rows)
bar_width = min(96, bar_area * 0.58)
parts = [
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
'<rect width="100%" height="100%" fill="#ffffff"/>',
f'<text x="{left}" y="30" font-family="Arial" font-size="18" font-weight="700" fill="#1f2933">{_escape(title)}</text>',
f'<line x1="{left}" y1="{height - bottom}" x2="{width - right}" y2="{height - bottom}" stroke="#9aa5b1"/>',
f'<line x1="{left}" y1="{top}" x2="{left}" y2="{height - bottom}" stroke="#9aa5b1"/>',
]
for tick in range(5):
ratio = tick / 4
y = height - bottom - ratio * chart_height
value = max_value * ratio
parts.append(
f'<line x1="{left - 5}" y1="{y:.1f}" x2="{width - right}" y2="{y:.1f}" stroke="#edf0f2"/>'
)
parts.append(
f'<text x="{left - 10}" y="{y + 4:.1f}" text-anchor="end" font-family="Arial" font-size="11" fill="#52616b">{value:.2f}</text>'
)
for index, row in enumerate(rows):
value = float(row[metric])
ratio = value / max_value
bar_height = ratio * chart_height
x = left + index * bar_area + (bar_area - bar_width) / 2
y = height - bottom - bar_height
label = str(row["стратегия"])
parts.append(
f'<rect x="{x:.1f}" y="{y:.1f}" width="{bar_width:.1f}" height="{bar_height:.1f}" fill="{color}" rx="3"/>'
)
parts.append(
f'<text x="{x + bar_width / 2:.1f}" y="{y - 8:.1f}" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">{value:.2f}</text>'
)
parts.append(
f'<text x="{x + bar_width / 2:.1f}" y="{height - bottom + 24}" text-anchor="middle" font-family="Arial" font-size="12" fill="#1f2933">{_escape(label)}</text>'
)
parts.append("</svg>")
path.write_text("\n".join(parts), encoding="utf-8")
def _escape(value: str) -> str:
return (
value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace('"', "&quot;")
)
if __name__ == "__main__":
main()

View File

@ -0,0 +1 @@
"""Unit tests for the maze solver project."""

View File

@ -0,0 +1,64 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from maze_solver import (
AStarStrategy,
BFSStrategy,
Direction,
MazeSolver,
MoveCommand,
Player,
TextFileMazeBuilder,
)
SIMPLE_MAZE = """\
#######
#S E#
# ### #
# #
#######"""
class MazeSolverTest(unittest.TestCase):
def build_maze(self):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory) / "maze.txt"
path.write_text(SIMPLE_MAZE, encoding="utf-8")
return TextFileMazeBuilder().build_from_file(path)
def test_builder_reads_start_exit_and_neighbors(self) -> None:
maze = self.build_maze()
self.assertEqual((maze.start.x, maze.start.y), (1, 1))
self.assertEqual((maze.exit.x, maze.exit.y), (5, 1))
self.assertTrue(maze.get_cell(2, 1).is_passable())
self.assertFalse(maze.get_cell(0, 0).is_passable())
def test_bfs_and_astar_find_shortest_path(self) -> None:
maze = self.build_maze()
bfs_stats = MazeSolver(maze, BFSStrategy()).solve()
astar_stats = MazeSolver(maze, AStarStrategy()).solve()
self.assertEqual(bfs_stats.path_length, 5)
self.assertEqual(astar_stats.path_length, 5)
self.assertEqual(bfs_stats.path[0], maze.start)
self.assertEqual(bfs_stats.path[-1], maze.exit)
def test_move_command_can_execute_and_undo(self) -> None:
maze = self.build_maze()
player = Player.at_start(maze)
command = MoveCommand(player, Direction.RIGHT)
self.assertTrue(command.execute())
self.assertEqual((player.current_cell.x, player.current_cell.y), (2, 1))
self.assertTrue(command.undo())
self.assertEqual(player.current_cell, maze.start)
if __name__ == "__main__":
unittest.main()

3
shahovaa/zadanie1/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
__pycache__/
*.py[cod]
.DS_Store

View File

@ -0,0 +1,24 @@
# Задание 1: структуры данных
Реализация телефонного справочника на трех структурах данных без классов:
- связный список;
- хеш-таблица с цепочками;
- двоичное дерево поиска.
## Запуск
Проверка базовых операций:
```bash
python3 phonebook.py
```
Экспериментальные замеры и построение графика:
```bash
python3 benchmark.py
```
По умолчанию используется `N = 10000`, `5` повторов, результаты сохраняются в
`docs/data/results.csv`, `docs/data/summary.csv` и `docs/data/performance.svg`.

View File

@ -0,0 +1,359 @@
"""Run performance experiments for the procedural phone book structures."""
import argparse
import csv
import html
import math
import random
import time
from pathlib import Path
from phonebook import (
bst_delete,
bst_find,
bst_insert,
create_hash_table,
ht_delete,
ht_find,
ht_insert,
ll_delete,
ll_find,
ll_insert,
)
STRUCTURES = ("LinkedList", "HashTable", "BST")
MODES = ("shuffled", "sorted")
OPERATIONS = ("insert", "find", "delete")
def generate_records(count):
return [(f"User_{index:05d}", f"+7-900-{index:05d}") for index in range(count)]
def prepare_records(count, seed):
records_sorted = generate_records(count)
records_shuffled = records_sorted[:]
random.Random(seed).shuffle(records_shuffled)
return {
"sorted": records_sorted,
"shuffled": records_shuffled,
}
def _insert_all(structure_name, records, bucket_count):
if structure_name == "LinkedList":
head = None
for name, phone in records:
head = ll_insert(head, name, phone)
return head
if structure_name == "HashTable":
buckets = create_hash_table(bucket_count)
for name, phone in records:
ht_insert(buckets, name, phone)
return buckets
if structure_name == "BST":
root = None
for name, phone in records:
root = bst_insert(root, name, phone)
return root
raise ValueError(f"Unknown structure: {structure_name}")
def _find_all(structure_name, structure, names):
if structure_name == "LinkedList":
for name in names:
ll_find(structure, name)
return structure
if structure_name == "HashTable":
for name in names:
ht_find(structure, name)
return structure
if structure_name == "BST":
for name in names:
bst_find(structure, name)
return structure
raise ValueError(f"Unknown structure: {structure_name}")
def _delete_all(structure_name, structure, names):
if structure_name == "LinkedList":
head = structure
for name in names:
head = ll_delete(head, name)
return head
if structure_name == "HashTable":
for name in names:
ht_delete(structure, name)
return structure
if structure_name == "BST":
root = structure
for name in names:
root = bst_delete(root, name)
return root
raise ValueError(f"Unknown structure: {structure_name}")
def _elapsed(action):
start = time.perf_counter()
result = action()
end = time.perf_counter()
return result, end - start
def run_experiment(count=10000, repeats=5, seed=42, bucket_count=20011):
record_sets = prepare_records(count, seed)
all_names = [name for name, _phone in record_sets["sorted"]]
results = []
for structure_name in STRUCTURES:
for mode in MODES:
records = record_sets[mode]
names_for_sampling = [name for name, _phone in records]
for repeat in range(1, repeats + 1):
rng = random.Random(seed + repeat * 1000 + len(structure_name) + len(mode))
find_existing = rng.sample(names_for_sampling, min(100, count))
find_missing = [f"None_{repeat}_{index}" for index in range(10)]
find_names = find_existing + find_missing
delete_names = rng.sample(all_names, min(50, count))
structure, insert_time = _elapsed(
lambda: _insert_all(structure_name, records, bucket_count)
)
results.append(
{
"structure": structure_name,
"mode": mode,
"operation": "insert",
"repeat": repeat,
"time_sec": insert_time,
"n": count,
"bucket_count": bucket_count if structure_name == "HashTable" else "",
}
)
structure, find_time = _elapsed(
lambda: _find_all(structure_name, structure, find_names)
)
results.append(
{
"structure": structure_name,
"mode": mode,
"operation": "find",
"repeat": repeat,
"time_sec": find_time,
"n": count,
"bucket_count": bucket_count if structure_name == "HashTable" else "",
}
)
structure, delete_time = _elapsed(
lambda: _delete_all(structure_name, structure, delete_names)
)
results.append(
{
"structure": structure_name,
"mode": mode,
"operation": "delete",
"repeat": repeat,
"time_sec": delete_time,
"n": count,
"bucket_count": bucket_count if structure_name == "HashTable" else "",
}
)
return results
def summarize(results):
grouped = {}
for row in results:
key = (row["structure"], row["mode"], row["operation"])
grouped.setdefault(key, []).append(row["time_sec"])
summary = []
for structure_name in STRUCTURES:
for mode in MODES:
for operation in OPERATIONS:
values = grouped[(structure_name, mode, operation)]
summary.append(
{
"structure": structure_name,
"mode": mode,
"operation": operation,
"average_time_sec": sum(values) / len(values),
"measurements_sec": ";".join(f"{value:.9f}" for value in values),
}
)
return summary
def write_csv(path, rows, fieldnames):
path.parent.mkdir(parents=True, exist_ok=True)
with path.open("w", encoding="utf-8", newline="") as file:
writer = csv.DictWriter(file, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def write_chart(path, summary):
try:
import matplotlib.pyplot as plt
except ModuleNotFoundError:
write_svg_chart(path, summary)
return
labels = [
f"{row['structure']}\n{row['mode']}\n{row['operation']}"
for row in summary
]
values = [row["average_time_sec"] for row in summary]
colors_by_operation = {
"insert": "#4C78A8",
"find": "#F58518",
"delete": "#54A24B",
}
colors = [colors_by_operation[row["operation"]] for row in summary]
path.parent.mkdir(parents=True, exist_ok=True)
plt.figure(figsize=(14, 7))
plt.bar(range(len(values)), values, color=colors)
plt.yscale("log")
plt.ylabel("Среднее время, секунд (логарифмическая шкала)")
plt.title("Сравнение операций телефонного справочника")
plt.xticks(range(len(labels)), labels, rotation=45, ha="right", fontsize=8)
plt.tight_layout()
plt.savefig(path, dpi=160)
plt.close()
def write_svg_chart(path, summary):
width = 1500
height = 760
margin_left = 90
margin_right = 40
margin_top = 70
margin_bottom = 210
plot_width = width - margin_left - margin_right
plot_height = height - margin_top - margin_bottom
baseline = margin_top + plot_height
values = [max(row["average_time_sec"], 1e-12) for row in summary]
log_min = math.floor(math.log10(min(values)))
log_max = math.ceil(math.log10(max(values)))
if log_min == log_max:
log_min -= 1
log_max += 1
def y_for(value):
log_value = math.log10(max(value, 1e-12))
return margin_top + (log_max - log_value) / (log_max - log_min) * plot_height
colors_by_operation = {
"insert": "#4C78A8",
"find": "#F58518",
"delete": "#54A24B",
}
slot_width = plot_width / len(summary)
bar_width = slot_width * 0.62
lines = [
'<?xml version="1.0" encoding="UTF-8"?>',
f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">',
'<rect width="100%" height="100%" fill="#ffffff"/>',
'<style>text{font-family:Arial,Helvetica,sans-serif;fill:#222}.axis{stroke:#222;stroke-width:1}.grid{stroke:#ddd;stroke-width:1}.label{font-size:13px}.tick{font-size:12px}.title{font-size:24px;font-weight:700}.legend{font-size:14px}</style>',
f'<text class="title" x="{width / 2}" y="35" text-anchor="middle">Сравнение операций телефонного справочника</text>',
f'<line class="axis" x1="{margin_left}" y1="{baseline}" x2="{width - margin_right}" y2="{baseline}"/>',
f'<line class="axis" x1="{margin_left}" y1="{margin_top}" x2="{margin_left}" y2="{baseline}"/>',
]
for exponent in range(log_min, log_max + 1):
value = 10 ** exponent
y = y_for(value)
lines.append(
f'<line class="grid" x1="{margin_left}" y1="{y:.2f}" x2="{width - margin_right}" y2="{y:.2f}"/>'
)
lines.append(
f'<text class="tick" x="{margin_left - 10}" y="{y + 4:.2f}" text-anchor="end">1e{exponent}</text>'
)
for index, row in enumerate(summary):
x = margin_left + index * slot_width + (slot_width - bar_width) / 2
y = y_for(row["average_time_sec"])
bar_height = baseline - y
color = colors_by_operation[row["operation"]]
label = f"{row['structure']} / {row['mode']} / {row['operation']}"
lines.append(
f'<rect x="{x:.2f}" y="{y:.2f}" width="{bar_width:.2f}" height="{bar_height:.2f}" fill="{color}"/>'
)
lines.append(
f'<text class="tick" x="{x + bar_width / 2:.2f}" y="{y - 5:.2f}" text-anchor="middle">{row["average_time_sec"]:.3g}</text>'
)
lines.append(
f'<text class="label" transform="translate({x + bar_width / 2:.2f} {baseline + 18:.2f}) rotate(55)" text-anchor="start">{html.escape(label)}</text>'
)
legend_x = margin_left
legend_y = height - 30
for offset, (operation, color) in enumerate(colors_by_operation.items()):
x = legend_x + offset * 130
lines.append(f'<rect x="{x}" y="{legend_y - 12}" width="18" height="18" fill="{color}"/>')
lines.append(f'<text class="legend" x="{x + 26}" y="{legend_y + 2}">{operation}</text>')
lines.append(
f'<text class="label" transform="translate(24 {margin_top + plot_height / 2}) rotate(-90)" text-anchor="middle">Среднее время, секунд (логарифмическая шкала)</text>'
)
lines.append("</svg>")
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("\n".join(lines), encoding="utf-8")
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--n", type=int, default=10000, help="number of generated records")
parser.add_argument("--repeats", type=int, default=5, help="number of repeated measurements")
parser.add_argument("--seed", type=int, default=42, help="random seed")
parser.add_argument("--bucket-count", type=int, default=20011, help="hash-table bucket count")
parser.add_argument("--output-dir", type=Path, default=Path("docs/data"))
args = parser.parse_args()
results = run_experiment(
count=args.n,
repeats=args.repeats,
seed=args.seed,
bucket_count=args.bucket_count,
)
summary = summarize(results)
write_csv(
args.output_dir / "results.csv",
results,
["structure", "mode", "operation", "repeat", "time_sec", "n", "bucket_count"],
)
write_csv(
args.output_dir / "summary.csv",
summary,
["structure", "mode", "operation", "average_time_sec", "measurements_sec"],
)
chart_path = args.output_dir / "performance.svg"
write_chart(chart_path, summary)
print(f"Saved detailed results to {args.output_dir / 'results.csv'}")
print(f"Saved summary to {args.output_dir / 'summary.csv'}")
print(f"Saved chart to {chart_path}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 78 KiB

View File

@ -0,0 +1,91 @@
structure,mode,operation,repeat,time_sec,n,bucket_count
LinkedList,shuffled,insert,1,1.5487497089998215,10000,
LinkedList,shuffled,find,1,0.013355207998756669,10000,
LinkedList,shuffled,delete,1,0.006138000000646571,10000,
LinkedList,shuffled,insert,2,1.6062446670002828,10000,
LinkedList,shuffled,find,2,0.014175791999150533,10000,
LinkedList,shuffled,delete,2,0.007367083000644925,10000,
LinkedList,shuffled,insert,3,1.5470056670001213,10000,
LinkedList,shuffled,find,3,0.014115500000116299,10000,
LinkedList,shuffled,delete,3,0.006011666999256704,10000,
LinkedList,shuffled,insert,4,1.5362317910003185,10000,
LinkedList,shuffled,find,4,0.01460650000080932,10000,
LinkedList,shuffled,delete,4,0.006377084000632749,10000,
LinkedList,shuffled,insert,5,1.541476624999632,10000,
LinkedList,shuffled,find,5,0.014646625000750646,10000,
LinkedList,shuffled,delete,5,0.005829540999911842,10000,
LinkedList,sorted,insert,1,1.4639895000000251,10000,
LinkedList,sorted,find,1,0.012882999999419553,10000,
LinkedList,sorted,delete,1,0.005734124999435153,10000,
LinkedList,sorted,insert,2,1.4757493329998397,10000,
LinkedList,sorted,find,2,0.013435208000373677,10000,
LinkedList,sorted,delete,2,0.006567624999661348,10000,
LinkedList,sorted,insert,3,1.474924916999953,10000,
LinkedList,sorted,find,3,0.012946166998517583,10000,
LinkedList,sorted,delete,3,0.005636875001073349,10000,
LinkedList,sorted,insert,4,1.6074728750008944,10000,
LinkedList,sorted,find,4,0.012849667000409681,10000,
LinkedList,sorted,delete,4,0.006610207999983686,10000,
LinkedList,sorted,insert,5,1.5465652919992863,10000,
LinkedList,sorted,find,5,0.012851292000050307,10000,
LinkedList,sorted,delete,5,0.005656833000102779,10000,
HashTable,shuffled,insert,1,0.005485583000336192,10000,20011
HashTable,shuffled,find,1,5.770799907622859e-05,10000,20011
HashTable,shuffled,delete,1,3.570800072338898e-05,10000,20011
HashTable,shuffled,insert,2,0.006064958999559167,10000,20011
HashTable,shuffled,find,2,5.854200026078615e-05,10000,20011
HashTable,shuffled,delete,2,3.495800046948716e-05,10000,20011
HashTable,shuffled,insert,3,0.005850707999343285,10000,20011
HashTable,shuffled,find,3,5.441699977382086e-05,10000,20011
HashTable,shuffled,delete,3,2.7292000595480204e-05,10000,20011
HashTable,shuffled,insert,4,0.005818375000671949,10000,20011
HashTable,shuffled,find,4,5.387499913922511e-05,10000,20011
HashTable,shuffled,delete,4,2.683300044736825e-05,10000,20011
HashTable,shuffled,insert,5,0.006451041999753215,10000,20011
HashTable,shuffled,find,5,5.6000000768108293e-05,10000,20011
HashTable,shuffled,delete,5,2.937499994004611e-05,10000,20011
HashTable,sorted,insert,1,0.005557000000408152,10000,20011
HashTable,sorted,find,1,5.608300125459209e-05,10000,20011
HashTable,sorted,delete,1,2.8624999686144292e-05,10000,20011
HashTable,sorted,insert,2,0.005895457999940845,10000,20011
HashTable,sorted,find,2,6.0874999689986e-05,10000,20011
HashTable,sorted,delete,2,3.199999991920777e-05,10000,20011
HashTable,sorted,insert,3,0.005766083999333205,10000,20011
HashTable,sorted,find,3,5.500000042957254e-05,10000,20011
HashTable,sorted,delete,3,2.7874999432242475e-05,10000,20011
HashTable,sorted,insert,4,0.005590124999798718,10000,20011
HashTable,sorted,find,4,5.337499896995723e-05,10000,20011
HashTable,sorted,delete,4,2.6959000024362467e-05,10000,20011
HashTable,sorted,insert,5,0.007889499998782412,10000,20011
HashTable,sorted,find,5,5.549999877985101e-05,10000,20011
HashTable,sorted,delete,5,2.7749998480430804e-05,10000,20011
BST,shuffled,insert,1,0.011201125000297907,10000,
BST,shuffled,find,1,9.245900037058163e-05,10000,
BST,shuffled,delete,1,6.958300036785658e-05,10000,
BST,shuffled,insert,2,0.011337707999700797,10000,
BST,shuffled,find,2,9.545799912302755e-05,10000,
BST,shuffled,delete,2,7.141599962778855e-05,10000,
BST,shuffled,insert,3,0.01119999999900756,10000,
BST,shuffled,find,3,9.308299922849983e-05,10000,
BST,shuffled,delete,3,6.779199975426309e-05,10000,
BST,shuffled,insert,4,0.011189917000592686,10000,
BST,shuffled,find,4,9.675000001152512e-05,10000,
BST,shuffled,delete,4,6.624999878113158e-05,10000,
BST,shuffled,insert,5,0.01118529100131127,10000,
BST,shuffled,find,5,8.670799979881849e-05,10000,
BST,shuffled,delete,5,6.904200017743278e-05,10000,
BST,sorted,insert,1,2.2425066659998265,10000,
BST,sorted,find,1,0.018234625000332016,10000,
BST,sorted,delete,1,0.010230416999547742,10000,
BST,sorted,insert,2,2.26542979199985,10000,
BST,sorted,find,2,0.021546082998611382,10000,
BST,sorted,delete,2,0.011778292000599322,10000,
BST,sorted,insert,3,2.246992708000107,10000,
BST,sorted,find,3,0.01936033300080453,10000,
BST,sorted,delete,3,0.010003166000387864,10000,
BST,sorted,insert,4,2.2515108749994397,10000,
BST,sorted,find,4,0.021122417001606664,10000,
BST,sorted,delete,4,0.01173120800012839,10000,
BST,sorted,insert,5,2.2457697090012516,10000,
BST,sorted,find,5,0.01902170900029887,10000,
BST,sorted,delete,5,0.010273834001054638,10000,
1 structure mode operation repeat time_sec n bucket_count
2 LinkedList shuffled insert 1 1.5487497089998215 10000
3 LinkedList shuffled find 1 0.013355207998756669 10000
4 LinkedList shuffled delete 1 0.006138000000646571 10000
5 LinkedList shuffled insert 2 1.6062446670002828 10000
6 LinkedList shuffled find 2 0.014175791999150533 10000
7 LinkedList shuffled delete 2 0.007367083000644925 10000
8 LinkedList shuffled insert 3 1.5470056670001213 10000
9 LinkedList shuffled find 3 0.014115500000116299 10000
10 LinkedList shuffled delete 3 0.006011666999256704 10000
11 LinkedList shuffled insert 4 1.5362317910003185 10000
12 LinkedList shuffled find 4 0.01460650000080932 10000
13 LinkedList shuffled delete 4 0.006377084000632749 10000
14 LinkedList shuffled insert 5 1.541476624999632 10000
15 LinkedList shuffled find 5 0.014646625000750646 10000
16 LinkedList shuffled delete 5 0.005829540999911842 10000
17 LinkedList sorted insert 1 1.4639895000000251 10000
18 LinkedList sorted find 1 0.012882999999419553 10000
19 LinkedList sorted delete 1 0.005734124999435153 10000
20 LinkedList sorted insert 2 1.4757493329998397 10000
21 LinkedList sorted find 2 0.013435208000373677 10000
22 LinkedList sorted delete 2 0.006567624999661348 10000
23 LinkedList sorted insert 3 1.474924916999953 10000
24 LinkedList sorted find 3 0.012946166998517583 10000
25 LinkedList sorted delete 3 0.005636875001073349 10000
26 LinkedList sorted insert 4 1.6074728750008944 10000
27 LinkedList sorted find 4 0.012849667000409681 10000
28 LinkedList sorted delete 4 0.006610207999983686 10000
29 LinkedList sorted insert 5 1.5465652919992863 10000
30 LinkedList sorted find 5 0.012851292000050307 10000
31 LinkedList sorted delete 5 0.005656833000102779 10000
32 HashTable shuffled insert 1 0.005485583000336192 10000 20011
33 HashTable shuffled find 1 5.770799907622859e-05 10000 20011
34 HashTable shuffled delete 1 3.570800072338898e-05 10000 20011
35 HashTable shuffled insert 2 0.006064958999559167 10000 20011
36 HashTable shuffled find 2 5.854200026078615e-05 10000 20011
37 HashTable shuffled delete 2 3.495800046948716e-05 10000 20011
38 HashTable shuffled insert 3 0.005850707999343285 10000 20011
39 HashTable shuffled find 3 5.441699977382086e-05 10000 20011
40 HashTable shuffled delete 3 2.7292000595480204e-05 10000 20011
41 HashTable shuffled insert 4 0.005818375000671949 10000 20011
42 HashTable shuffled find 4 5.387499913922511e-05 10000 20011
43 HashTable shuffled delete 4 2.683300044736825e-05 10000 20011
44 HashTable shuffled insert 5 0.006451041999753215 10000 20011
45 HashTable shuffled find 5 5.6000000768108293e-05 10000 20011
46 HashTable shuffled delete 5 2.937499994004611e-05 10000 20011
47 HashTable sorted insert 1 0.005557000000408152 10000 20011
48 HashTable sorted find 1 5.608300125459209e-05 10000 20011
49 HashTable sorted delete 1 2.8624999686144292e-05 10000 20011
50 HashTable sorted insert 2 0.005895457999940845 10000 20011
51 HashTable sorted find 2 6.0874999689986e-05 10000 20011
52 HashTable sorted delete 2 3.199999991920777e-05 10000 20011
53 HashTable sorted insert 3 0.005766083999333205 10000 20011
54 HashTable sorted find 3 5.500000042957254e-05 10000 20011
55 HashTable sorted delete 3 2.7874999432242475e-05 10000 20011
56 HashTable sorted insert 4 0.005590124999798718 10000 20011
57 HashTable sorted find 4 5.337499896995723e-05 10000 20011
58 HashTable sorted delete 4 2.6959000024362467e-05 10000 20011
59 HashTable sorted insert 5 0.007889499998782412 10000 20011
60 HashTable sorted find 5 5.549999877985101e-05 10000 20011
61 HashTable sorted delete 5 2.7749998480430804e-05 10000 20011
62 BST shuffled insert 1 0.011201125000297907 10000
63 BST shuffled find 1 9.245900037058163e-05 10000
64 BST shuffled delete 1 6.958300036785658e-05 10000
65 BST shuffled insert 2 0.011337707999700797 10000
66 BST shuffled find 2 9.545799912302755e-05 10000
67 BST shuffled delete 2 7.141599962778855e-05 10000
68 BST shuffled insert 3 0.01119999999900756 10000
69 BST shuffled find 3 9.308299922849983e-05 10000
70 BST shuffled delete 3 6.779199975426309e-05 10000
71 BST shuffled insert 4 0.011189917000592686 10000
72 BST shuffled find 4 9.675000001152512e-05 10000
73 BST shuffled delete 4 6.624999878113158e-05 10000
74 BST shuffled insert 5 0.01118529100131127 10000
75 BST shuffled find 5 8.670799979881849e-05 10000
76 BST shuffled delete 5 6.904200017743278e-05 10000
77 BST sorted insert 1 2.2425066659998265 10000
78 BST sorted find 1 0.018234625000332016 10000
79 BST sorted delete 1 0.010230416999547742 10000
80 BST sorted insert 2 2.26542979199985 10000
81 BST sorted find 2 0.021546082998611382 10000
82 BST sorted delete 2 0.011778292000599322 10000
83 BST sorted insert 3 2.246992708000107 10000
84 BST sorted find 3 0.01936033300080453 10000
85 BST sorted delete 3 0.010003166000387864 10000
86 BST sorted insert 4 2.2515108749994397 10000
87 BST sorted find 4 0.021122417001606664 10000
88 BST sorted delete 4 0.01173120800012839 10000
89 BST sorted insert 5 2.2457697090012516 10000
90 BST sorted find 5 0.01902170900029887 10000
91 BST sorted delete 5 0.010273834001054638 10000

View File

@ -0,0 +1,19 @@
structure,mode,operation,average_time_sec,measurements_sec
LinkedList,shuffled,insert,1.5559416918000353,1.548749709;1.606244667;1.547005667;1.536231791;1.541476625
LinkedList,shuffled,find,0.014179924999916693,0.013355208;0.014175792;0.014115500;0.014606500;0.014646625
LinkedList,shuffled,delete,0.006344675000218558,0.006138000;0.007367083;0.006011667;0.006377084;0.005829541
LinkedList,sorted,insert,1.5137403833999996,1.463989500;1.475749333;1.474924917;1.607472875;1.546565292
LinkedList,sorted,find,0.01299306679975416,0.012883000;0.013435208;0.012946167;0.012849667;0.012851292
LinkedList,sorted,delete,0.006041133200051263,0.005734125;0.006567625;0.005636875;0.006610208;0.005656833
HashTable,shuffled,insert,0.005934133399932762,0.005485583;0.006064959;0.005850708;0.005818375;0.006451042
HashTable,shuffled,find,5.61083998036338e-05,0.000057708;0.000058542;0.000054417;0.000053875;0.000056000
HashTable,shuffled,delete,3.083320043515414e-05,0.000035708;0.000034958;0.000027292;0.000026833;0.000029375
HashTable,sorted,insert,0.006139633399652666,0.005557000;0.005895458;0.005766084;0.005590125;0.007889500
HashTable,sorted,find,5.6166599824791776e-05,0.000056083;0.000060875;0.000055000;0.000053375;0.000055500
HashTable,sorted,delete,2.8641799508477563e-05,0.000028625;0.000032000;0.000027875;0.000026959;0.000027750
BST,shuffled,insert,0.011222808200182044,0.011201125;0.011337708;0.011200000;0.011189917;0.011185291
BST,shuffled,find,9.289159970649052e-05,0.000092459;0.000095458;0.000093083;0.000096750;0.000086708
BST,shuffled,delete,6.881659974169451e-05,0.000069583;0.000071416;0.000067792;0.000066250;0.000069042
BST,sorted,insert,2.250441950000095,2.242506666;2.265429792;2.246992708;2.251510875;2.245769709
BST,sorted,find,0.019857033400330692,0.018234625;0.021546083;0.019360333;0.021122417;0.019021709
BST,sorted,delete,0.010803383400343591,0.010230417;0.011778292;0.010003166;0.011731208;0.010273834
1 structure mode operation average_time_sec measurements_sec
2 LinkedList shuffled insert 1.5559416918000353 1.548749709;1.606244667;1.547005667;1.536231791;1.541476625
3 LinkedList shuffled find 0.014179924999916693 0.013355208;0.014175792;0.014115500;0.014606500;0.014646625
4 LinkedList shuffled delete 0.006344675000218558 0.006138000;0.007367083;0.006011667;0.006377084;0.005829541
5 LinkedList sorted insert 1.5137403833999996 1.463989500;1.475749333;1.474924917;1.607472875;1.546565292
6 LinkedList sorted find 0.01299306679975416 0.012883000;0.013435208;0.012946167;0.012849667;0.012851292
7 LinkedList sorted delete 0.006041133200051263 0.005734125;0.006567625;0.005636875;0.006610208;0.005656833
8 HashTable shuffled insert 0.005934133399932762 0.005485583;0.006064959;0.005850708;0.005818375;0.006451042
9 HashTable shuffled find 5.61083998036338e-05 0.000057708;0.000058542;0.000054417;0.000053875;0.000056000
10 HashTable shuffled delete 3.083320043515414e-05 0.000035708;0.000034958;0.000027292;0.000026833;0.000029375
11 HashTable sorted insert 0.006139633399652666 0.005557000;0.005895458;0.005766084;0.005590125;0.007889500
12 HashTable sorted find 5.6166599824791776e-05 0.000056083;0.000060875;0.000055000;0.000053375;0.000055500
13 HashTable sorted delete 2.8641799508477563e-05 0.000028625;0.000032000;0.000027875;0.000026959;0.000027750
14 BST shuffled insert 0.011222808200182044 0.011201125;0.011337708;0.011200000;0.011189917;0.011185291
15 BST shuffled find 9.289159970649052e-05 0.000092459;0.000095458;0.000093083;0.000096750;0.000086708
16 BST shuffled delete 6.881659974169451e-05 0.000069583;0.000071416;0.000067792;0.000066250;0.000069042
17 BST sorted insert 2.250441950000095 2.242506666;2.265429792;2.246992708;2.251510875;2.245769709
18 BST sorted find 0.019857033400330692 0.018234625;0.021546083;0.019360333;0.021122417;0.019021709
19 BST sorted delete 0.010803383400343591 0.010230417;0.011778292;0.010003166;0.011731208;0.010273834

View File

@ -0,0 +1,112 @@
# Отчет по заданию 1: структуры данных
## Цель
Реализовать три структуры данных с нуля в процедурной парадигме и сравнить
скорость основных операций телефонного справочника:
- `insert(name, phone)` - добавить или обновить запись;
- `find(name)` - найти телефон по имени;
- `delete(name)` - удалить запись;
- `list_all()` - получить все записи, отсортированные по имени.
Классы не использовались. Узлы связного списка и дерева представлены
словарями, хеш-таблица представлена списком бакетов.
## Реализация
Код находится в файле `phonebook.py`.
Реализованы функции:
- связный список: `ll_insert`, `ll_find`, `ll_delete`, `ll_list_all`;
- хеш-таблица: `create_hash_table`, `ht_insert`, `ht_find`, `ht_delete`, `ht_list_all`;
- двоичное дерево поиска: `bst_insert`, `bst_find`, `bst_delete`, `bst_list_all`.
Для хеш-таблицы используется метод цепочек: каждый бакет хранит голову
связного списка. Хеш-функция написана вручную, чтобы результат не зависел от
рандомизации встроенной функции `hash()` в Python.
Для BST вставка, поиск, удаление и обход написаны без классов. Обход
`bst_list_all` реализован итеративно, чтобы отсортированный вход на 10000
элементов не приводил к переполнению стека рекурсии.
## Методика эксперимента
Скрипт эксперимента находится в файле `benchmark.py`.
Параметры запуска:
- количество записей: `N = 10000`;
- число повторов каждого эксперимента: `5`;
- имена: `User_00000`, `User_00001`, ..., `User_09999`;
- два режима входных данных: `shuffled` и `sorted`;
- поиск: 100 существующих имен и 10 отсутствующих;
- удаление: 50 случайных существующих имен;
- размер хеш-таблицы: `20011` бакетов.
После вставки структура не пересоздается: поиск и удаление выполняются на той
же заполненной структуре. Для каждого режима и каждой структуры создается новая
структура.
Файлы с результатами:
- `docs/data/results.csv` - все отдельные замеры;
- `docs/data/summary.csv` - среднее время и список всех пяти замеров;
- `docs/data/performance.svg` - столбчатая диаграмма средних значений.
![График производительности](data/performance.svg)
## Средние результаты
Время указано в секундах.
| Структура | Режим | Вставка | Поиск | Удаление |
|---|---:|---:|---:|---:|
| LinkedList | shuffled | 1.555942 | 0.014180 | 0.006345 |
| LinkedList | sorted | 1.513740 | 0.012993 | 0.006041 |
| HashTable | shuffled | 0.005934 | 0.000056 | 0.000031 |
| HashTable | sorted | 0.006140 | 0.000056 | 0.000029 |
| BST | shuffled | 0.011223 | 0.000093 | 0.000069 |
| BST | sorted | 2.250442 | 0.019857 | 0.010803 |
## Анализ
Связный список оказался самым медленным на вставке и поиске. Причина в том, что
для корректной операции `insert` нужно проверить, есть ли уже запись с таким
именем. При уникальных именах почти каждая вставка проходит по всему текущему
списку, поэтому суммарная сложность вставки всех записей становится `O(n^2)`.
Порядок входных данных почти не влияет на результат, потому что структура не
использует порядок ключей.
Хеш-таблица показала лучшие результаты почти во всех операциях. При хорошем
распределении по бакетам вставка, поиск и удаление близки к `O(1)`. Порядок
входных данных почти не влияет на время, так как индекс бакета определяется
хешем имени, а не расположением записи во входном списке.
BST хорошо работает на перемешанных данных: дерево получается сравнительно
сбалансированным, поэтому операции близки к `O(log n)`. На отсортированном
входе обычное двоичное дерево поиска вырождается в цепочку: каждый новый ключ
становится правым потомком предыдущего. Из-за этого вставка всех записей
становится `O(n^2)`, а поиск и удаление приближаются к поведению связного
списка.
Удаление у хеш-таблицы быстрое по той же причине, что и поиск: сначала
вычисляется бакет, затем просматривается короткая цепочка. В BST удаление
быстрое на перемешанном дереве, но на вырожденном дереве оно замедляется.
В связном списке удаление требует линейного поиска удаляемого элемента.
## Вывод
Для частого поиска, обновления и удаления по точному имени лучше выбирать
хеш-таблицу. Она быстрее всего в эксперименте и почти не зависит от порядка
вставки.
Если нужно часто получать данные в отсортированном порядке, дерево поиска дает
удобный `in-order` обход без отдельной сортировки. Но обычный BST чувствителен
к порядку входных данных, поэтому на практике лучше использовать
самобалансирующееся дерево или готовую структуру из библиотеки.
Связный список подходит только для маленьких наборов данных или учебных задач.
Для телефонного справочника с частым поиском он неудачен, потому что каждая
операция поиска требует последовательного прохода по элементам.

View File

@ -0,0 +1,255 @@
"""Procedural phone book data structures for assignment 1.
The task explicitly asks to avoid classes, so every structure is represented
with plain dictionaries, lists and functions.
"""
def _make_ll_node(name, phone, next_node=None):
return {"name": name, "phone": phone, "next": next_node}
def ll_insert(head, name, phone):
"""Insert or update a record in a linked list, returning the head."""
if head is None:
return _make_ll_node(name, phone)
current = head
while current is not None:
if current["name"] == name:
current["phone"] = phone
return head
if current["next"] is None:
break
current = current["next"]
current["next"] = _make_ll_node(name, phone)
return head
def ll_find(head, name):
"""Return a phone by name or None if there is no such record."""
current = head
while current is not None:
if current["name"] == name:
return current["phone"]
current = current["next"]
return None
def ll_delete(head, name):
"""Delete a record by name, returning the possibly changed head."""
previous = None
current = head
while current is not None:
if current["name"] == name:
if previous is None:
return current["next"]
previous["next"] = current["next"]
return head
previous = current
current = current["next"]
return head
def ll_list_all(head):
"""Return all linked-list records sorted by name."""
records = []
current = head
while current is not None:
records.append((current["name"], current["phone"]))
current = current["next"]
return sorted(records, key=lambda item: item[0])
def create_hash_table(size=20011):
"""Create a fixed-size hash table with separate chaining."""
return [None for _ in range(size)]
def _hash_name(name, bucket_count):
"""Stable polynomial hash, unlike Python's randomized built-in hash()."""
value = 0
for char in name:
value = (value * 31 + ord(char)) % bucket_count
return value
def ht_insert(buckets, name, phone):
"""Insert or update a record in the hash table."""
index = _hash_name(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
"""Return a phone by name or None if there is no such record."""
index = _hash_name(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
"""Delete a record by name if it exists."""
index = _hash_name(name, len(buckets))
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets):
"""Return all hash-table records sorted by name."""
records = []
for head in buckets:
current = head
while current is not None:
records.append((current["name"], current["phone"]))
current = current["next"]
return sorted(records, key=lambda item: item[0])
def _make_bst_node(name, phone):
return {"name": name, "phone": phone, "left": None, "right": None}
def bst_insert(root, name, phone):
"""Insert or update a record in a binary search tree."""
if root is None:
return _make_bst_node(name, phone)
current = root
while True:
if name == current["name"]:
current["phone"] = phone
return root
if name < current["name"]:
if current["left"] is None:
current["left"] = _make_bst_node(name, phone)
return root
current = current["left"]
else:
if current["right"] is None:
current["right"] = _make_bst_node(name, phone)
return root
current = current["right"]
def bst_find(root, name):
"""Return a phone by name or None if there is no such record."""
current = root
while current is not None:
if name == current["name"]:
return current["phone"]
if name < current["name"]:
current = current["left"]
else:
current = current["right"]
return None
def _detach_min(node):
"""Detach the minimal node from a subtree and return (new_subtree, min)."""
parent = None
current = node
while current["left"] is not None:
parent = current
current = current["left"]
if parent is None:
return current["right"], current
parent["left"] = current["right"]
current["right"] = None
return node, current
def bst_delete(root, name):
"""Delete a record from the tree, returning the possibly changed root."""
parent = None
current = root
while current is not None 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:
replacement = current["right"]
elif current["right"] is None:
replacement = current["left"]
else:
new_right, successor = _detach_min(current["right"])
successor["left"] = current["left"]
successor["right"] = new_right
replacement = successor
if parent is None:
return replacement
if parent["left"] is current:
parent["left"] = replacement
else:
parent["right"] = replacement
return root
def bst_list_all(root):
"""Return all BST records sorted by name using in-order traversal."""
records = []
stack = []
current = root
while current is not None or stack:
while current is not None:
stack.append(current)
current = current["left"]
current = stack.pop()
records.append((current["name"], current["phone"]))
current = current["right"]
return records
def _assert_basic_operations():
records = [("Boris", "222"), ("Anna", "111"), ("Denis", "444")]
expected_sorted = [("Anna", "111"), ("Boris", "222"), ("Denis", "444")]
head = None
for name, phone in records:
head = ll_insert(head, name, phone)
assert ll_find(head, "Anna") == "111"
head = ll_insert(head, "Anna", "333")
assert ll_find(head, "Anna") == "333"
head = ll_delete(head, "Anna")
assert ll_find(head, "Anna") is None
assert ll_list_all(head) == [("Boris", "222"), ("Denis", "444")]
table = create_hash_table(17)
for name, phone in records:
ht_insert(table, name, phone)
assert ht_find(table, "Denis") == "444"
ht_insert(table, "Denis", "555")
assert ht_find(table, "Denis") == "555"
ht_delete(table, "Missing")
assert ("Anna", "111") in ht_list_all(table)
root = None
for name, phone in records:
root = bst_insert(root, name, phone)
assert bst_list_all(root) == expected_sorted
root = bst_delete(root, "Boris")
assert bst_find(root, "Boris") is None
assert bst_list_all(root) == [("Anna", "111"), ("Denis", "444")]
if __name__ == "__main__":
_assert_basic_operations()
print("All phonebook checks passed.")

View File

@ -0,0 +1 @@
matplotlib>=3.8