diff --git a/BolonkinNM/.idea/.gitignore b/BolonkinNM/.idea/.gitignore deleted file mode 100644 index 26d3352..0000000 --- a/BolonkinNM/.idea/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml diff --git a/BolonkinNM/.idea/ds_project_archive.iml b/BolonkinNM/.idea/maze_project_submission.iml similarity index 100% rename from BolonkinNM/.idea/ds_project_archive.iml rename to BolonkinNM/.idea/maze_project_submission.iml diff --git a/BolonkinNM/.idea/misc.xml b/BolonkinNM/.idea/misc.xml index b56db6e..0ebfc91 100644 --- a/BolonkinNM/.idea/misc.xml +++ b/BolonkinNM/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/BolonkinNM/.idea/modules.xml b/BolonkinNM/.idea/modules.xml index 699d578..a636c96 100644 --- a/BolonkinNM/.idea/modules.xml +++ b/BolonkinNM/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/BolonkinNM/.idea/workspace.xml b/BolonkinNM/.idea/workspace.xml new file mode 100644 index 0000000..896a098 --- /dev/null +++ b/BolonkinNM/.idea/workspace.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + 1779637417749 + + + + \ No newline at end of file diff --git a/BolonkinNM/README.md b/BolonkinNM/README.md index 7e15dad..2e6e63f 100644 --- a/BolonkinNM/README.md +++ b/BolonkinNM/README.md @@ -1,18 +1,24 @@ -# Задание 1 — структуры данных +# Maze Solver Project -Процедурная реализация: -- linked_list.py -- hash_table.py -- bst.py +ООП-проект для поиска выхода из лабиринта с паттернами: +- Builder +- Strategy +- Observer +- Command -Эксперименты и отчёты: -- experiments.py -- plot_results.py -- results.csv -- docs/report.md -- docs/data/*.png - -Запуск: +## Запуск ```bash python main.py ``` + +## Эксперименты +```bash +python experiment.py +``` + +Результаты сохраняются в папку `experiment_results/`. + +## Требования +```bash +pip install -r requirements.txt +``` diff --git a/BolonkinNM/bst.py b/BolonkinNM/bst.py deleted file mode 100644 index 221fd67..0000000 --- a/BolonkinNM/bst.py +++ /dev/null @@ -1,118 +0,0 @@ -from typing import Any, Dict, List, Optional - - -Node = Dict[str, Any] - - -def _make_node(name: str, phone: str) -> Node: - return {"name": name, "phone": phone, "left": None, "right": None} - - -def bst_insert(root: Optional[Node], name: str, phone: str) -> Node: - new_node = _make_node(name, phone) - - if root is None: - return new_node - - current = root - parent = None - - while current is not None: - parent = current - if name < current["name"]: - current = current["left"] - elif name > current["name"]: - current = current["right"] - else: - current["phone"] = phone - return root - - if name < parent["name"]: - parent["left"] = new_node - else: - parent["right"] = new_node - - return root - - -def bst_find(root: Optional[Node], name: str) -> Optional[str]: - current = root - while current is not None: - if name < current["name"]: - current = current["left"] - elif name > current["name"]: - current = current["right"] - else: - return current["phone"] - return None - - -def _find_min_node(node: Node) -> Node: - current = node - while current["left"] is not None: - current = current["left"] - return current - - -def bst_delete(root: Optional[Node], name: str) -> Optional[Node]: - if root is None: - return None - - 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 or current["right"] is None: - child = current["left"] if current["left"] is not None else current["right"] - - if parent is None: - return child - - if parent["left"] is current: - parent["left"] = child - else: - parent["right"] = child - return root - - succ_parent = current - successor = current["right"] - while successor["left"] is not None: - succ_parent = successor - successor = successor["left"] - - current["name"] = successor["name"] - current["phone"] = successor["phone"] - - successor_child = successor["right"] - if succ_parent["left"] is successor: - succ_parent["left"] = successor_child - else: - succ_parent["right"] = successor_child - - return root - - -def bst_list_all(root: Optional[Node]) -> List[Dict[str, str]]: - result: List[Dict[str, str]] = [] - stack: List[Node] = [] - current = root - - while current is not None or stack: - while current is not None: - stack.append(current) - current = current["left"] - - current = stack.pop() - result.append({"name": current["name"], "phone": current["phone"]}) - current = current["right"] - - return result diff --git a/BolonkinNM/builders/__init__.py b/BolonkinNM/builders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/builders/maze_builder.py b/BolonkinNM/builders/maze_builder.py new file mode 100644 index 0000000..b055db8 --- /dev/null +++ b/BolonkinNM/builders/maze_builder.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class MazeBuilder(ABC): + @abstractmethod + def buildFromFile(self, filename): + raise NotImplementedError diff --git a/BolonkinNM/builders/text_file_maze_builder.py b/BolonkinNM/builders/text_file_maze_builder.py new file mode 100644 index 0000000..5e9ca03 --- /dev/null +++ b/BolonkinNM/builders/text_file_maze_builder.py @@ -0,0 +1,52 @@ +from core.cell import Cell +from core.maze import Maze +from builders.maze_builder import MazeBuilder + + +class TextFileMazeBuilder(MazeBuilder): + def buildFromFile(self, filename): + with open(filename, "r", encoding="utf-8") as f: + lines = [line.rstrip("\n") for line in f] + + if not lines: + raise ValueError("Maze file is empty") + + width = max(len(line) for line in lines) + height = len(lines) + + cells = [] + startCell = None + exitCell = None + + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + if startCell is not None: + raise ValueError("Multiple start cells found") + cell = Cell(x, y, isWall=False, isStart=True) + startCell = cell + elif ch == "E": + if exitCell is not None: + raise ValueError("Multiple exit cells found") + cell = Cell(x, y, isWall=False, isExit=True) + exitCell = cell + elif ch in (" ", "."): + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=max(1, int(ch))) + else: + raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})") + row.append(cell) + cells.append(row) + + if startCell is None: + raise ValueError("Start cell 'S' not found") + if exitCell is None: + raise ValueError("Exit cell 'E' not found") + + return Maze(cells, width, height, startCell, exitCell) diff --git a/BolonkinNM/commands/__init__.py b/BolonkinNM/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/commands/command.py b/BolonkinNM/commands/command.py new file mode 100644 index 0000000..71f2dc6 --- /dev/null +++ b/BolonkinNM/commands/command.py @@ -0,0 +1,11 @@ +from abc import ABC, abstractmethod + + +class Command(ABC): + @abstractmethod + def execute(self): + raise NotImplementedError + + @abstractmethod + def undo(self): + raise NotImplementedError diff --git a/BolonkinNM/commands/move_command.py b/BolonkinNM/commands/move_command.py new file mode 100644 index 0000000..e90b7f1 --- /dev/null +++ b/BolonkinNM/commands/move_command.py @@ -0,0 +1,37 @@ +from commands.command import Command + + +class MoveCommand(Command): + DIRECTION_TO_DELTA = { + "W": (0, -1), + "A": (-1, 0), + "S": (0, 1), + "D": (1, 0), + } + + def __init__(self, player, maze, direction): + self.player = player + self.maze = maze + self.direction = direction.upper() + self.previousCell = None + + def execute(self): + if self.direction not in self.DIRECTION_TO_DELTA: + return False + + dx, dy = self.DIRECTION_TO_DELTA[self.direction] + current = self.player.currentCell + new_cell = self.maze.getCell(current.x + dx, current.y + dy) + + if new_cell is None or not new_cell.isPassable(): + return False + + self.previousCell = current + self.player.setCell(new_cell) + return True + + def undo(self): + if self.previousCell is None: + return False + self.player.setCell(self.previousCell) + return True diff --git a/BolonkinNM/controller/__init__.py b/BolonkinNM/controller/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/controller/game_controller.py b/BolonkinNM/controller/game_controller.py new file mode 100644 index 0000000..0a4cb39 --- /dev/null +++ b/BolonkinNM/controller/game_controller.py @@ -0,0 +1,30 @@ +from commands.move_command import MoveCommand + + +class GameController: + def __init__(self, maze, player, view): + self.maze = maze + self.player = player + self.view = view + self.history = [] + + def move(self, direction): + command = MoveCommand(self.player, self.maze, direction) + if command.execute(): + self.history.append(command) + self.view.update({"type": "move", "direction": direction}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + print("Cannot move there") + return False + + def undo(self): + if not self.history: + print("Nothing to undo") + return False + command = self.history.pop() + if command.undo(): + self.view.update({"type": "undo"}) + self.view.render(self.maze, player_position=self.player.currentCell) + return True + return False diff --git a/BolonkinNM/core/__init__.py b/BolonkinNM/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/core/cell.py b/BolonkinNM/core/cell.py new file mode 100644 index 0000000..44e2d76 --- /dev/null +++ b/BolonkinNM/core/cell.py @@ -0,0 +1,26 @@ +from dataclasses import dataclass + + +@dataclass +class Cell: + x: int + y: int + isWall: bool = False + isStart: bool = False + isExit: bool = False + weight: int = 1 + + def isPassable(self): + return not self.isWall + + def __repr__(self): + parts = [f"Cell({self.x}, {self.y}"] + if self.isWall: + parts.append("WALL") + if self.isStart: + parts.append("START") + if self.isExit: + parts.append("EXIT") + if self.weight != 1: + parts.append(f"w={self.weight}") + return ", ".join(parts) + ")" diff --git a/BolonkinNM/core/maze.py b/BolonkinNM/core/maze.py new file mode 100644 index 0000000..59c86dd --- /dev/null +++ b/BolonkinNM/core/maze.py @@ -0,0 +1,49 @@ +class Maze: + def __init__(self, cells, width, height, startCell=None, exitCell=None): + self.cells = cells + self.width = width + self.height = height + self.startCell = startCell + self.exitCell = exitCell + + def getCell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def getNeighbors(self, cell): + neighbors = [] + for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)): + nx, ny = cell.x + dx, cell.y + dy + neighbor = self.getCell(nx, ny) + if neighbor is not None and neighbor.isPassable(): + neighbors.append(neighbor) + return neighbors + + def render_lines(self, player_position=None, path=None): + path_set = {(c.x, c.y) for c in path} if path else set() + player_pos = None if player_position is None else (player_position.x, player_position.y) + lines = [] + for y in range(self.height): + row = [] + for x in range(self.width): + cell = self.cells[y][x] + if player_pos == (x, y): + row.append("P") + elif cell.isStart: + row.append("S") + elif cell.isExit: + row.append("E") + elif cell.isWall: + row.append("#") + elif (x, y) in path_set: + row.append("*") + elif cell.weight > 1: + row.append(str(cell.weight)) + else: + row.append(" ") + lines.append("".join(row)) + return lines + + def render(self, player_position=None, path=None): + return "\n".join(self.render_lines(player_position=player_position, path=path)) diff --git a/BolonkinNM/core/player.py b/BolonkinNM/core/player.py new file mode 100644 index 0000000..b68a0ff --- /dev/null +++ b/BolonkinNM/core/player.py @@ -0,0 +1,6 @@ +class Player: + def __init__(self, currentCell): + self.currentCell = currentCell + + def setCell(self, cell): + self.currentCell = cell diff --git a/BolonkinNM/core/search_stats.py b/BolonkinNM/core/search_stats.py new file mode 100644 index 0000000..5548118 --- /dev/null +++ b/BolonkinNM/core/search_stats.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass, field + + +@dataclass +class SearchStats: + timeMs: float + visitedCells: int + pathLength: int + path: list = field(default_factory=list) + found: bool = False + algorithm: str = "" diff --git a/BolonkinNM/docs/README.txt b/BolonkinNM/docs/README.txt new file mode 100644 index 0000000..c760a90 --- /dev/null +++ b/BolonkinNM/docs/README.txt @@ -0,0 +1 @@ +Place report files and experiment outputs here. diff --git a/BolonkinNM/docs/data/delete.png b/BolonkinNM/docs/data/delete.png deleted file mode 100644 index b5307a7..0000000 Binary files a/BolonkinNM/docs/data/delete.png and /dev/null differ diff --git a/BolonkinNM/docs/data/find.png b/BolonkinNM/docs/data/find.png deleted file mode 100644 index d0d4e81..0000000 Binary files a/BolonkinNM/docs/data/find.png and /dev/null differ diff --git a/BolonkinNM/docs/data/insert.png b/BolonkinNM/docs/data/insert.png deleted file mode 100644 index b2832b9..0000000 Binary files a/BolonkinNM/docs/data/insert.png and /dev/null differ diff --git a/BolonkinNM/docs/data/results.csv b/BolonkinNM/docs/data/results.csv deleted file mode 100644 index 4ccfb69..0000000 --- a/BolonkinNM/docs/data/results.csv +++ /dev/null @@ -1,109 +0,0 @@ -Структура,Режим,Операция,Замер,Время (сек) -LinkedList,случайный,insert,1,4.2622492010 -LinkedList,случайный,find,1,0.0314994130 -LinkedList,случайный,delete,1,0.0149069000 -LinkedList,случайный,insert,2,4.0154580330 -LinkedList,случайный,find,2,0.0393284500 -LinkedList,случайный,delete,2,0.0210732100 -LinkedList,случайный,insert,3,4.0436019780 -LinkedList,случайный,find,3,0.0344933660 -LinkedList,случайный,delete,3,0.0152639850 -LinkedList,случайный,insert,4,3.7182993220 -LinkedList,случайный,find,4,0.0327698850 -LinkedList,случайный,delete,4,0.0149959540 -LinkedList,случайный,insert,5,3.7082228200 -LinkedList,случайный,find,5,0.0303762490 -LinkedList,случайный,delete,5,0.0141406560 -LinkedList,случайный,insert,среднее,3.9495662708 -LinkedList,случайный,find,среднее,0.0336934726 -LinkedList,случайный,delete,среднее,0.0160761410 -HashTable,случайный,insert,1,0.2059865770 -HashTable,случайный,find,1,0.0014966100 -HashTable,случайный,delete,1,0.0006891700 -HashTable,случайный,insert,2,0.2024331460 -HashTable,случайный,find,2,0.0015934880 -HashTable,случайный,delete,2,0.0007212620 -HashTable,случайный,insert,3,0.2126128040 -HashTable,случайный,find,3,0.0016566220 -HashTable,случайный,delete,3,0.0008358420 -HashTable,случайный,insert,4,0.2157934910 -HashTable,случайный,find,4,0.0015542810 -HashTable,случайный,delete,4,0.0007269120 -HashTable,случайный,insert,5,0.2079924580 -HashTable,случайный,find,5,0.0013696990 -HashTable,случайный,delete,5,0.0006616050 -HashTable,случайный,insert,среднее,0.2089636952 -HashTable,случайный,find,среднее,0.0015341400 -HashTable,случайный,delete,среднее,0.0007269582 -BST,случайный,insert,1,0.0166981280 -BST,случайный,find,1,0.0001569360 -BST,случайный,delete,1,0.0000917280 -BST,случайный,insert,2,0.0184119040 -BST,случайный,find,2,0.0001517110 -BST,случайный,delete,2,0.0001163770 -BST,случайный,insert,3,0.0174662270 -BST,случайный,find,3,0.0001582930 -BST,случайный,delete,3,0.0000892660 -BST,случайный,insert,4,0.0191369100 -BST,случайный,find,4,0.0002087170 -BST,случайный,delete,4,0.0001067050 -BST,случайный,insert,5,0.0184276900 -BST,случайный,find,5,0.0002767720 -BST,случайный,delete,5,0.0001067660 -BST,случайный,insert,среднее,0.0180281718 -BST,случайный,find,среднее,0.0001904858 -BST,случайный,delete,среднее,0.0001021684 -LinkedList,отсортированный,insert,1,2.9875078340 -LinkedList,отсортированный,find,1,0.0237300610 -LinkedList,отсортированный,delete,1,0.0111698260 -LinkedList,отсортированный,insert,2,3.0573987940 -LinkedList,отсортированный,find,2,0.0243270360 -LinkedList,отсортированный,delete,2,0.0115366030 -LinkedList,отсортированный,insert,3,2.9641987260 -LinkedList,отсортированный,find,3,0.0236313330 -LinkedList,отсортированный,delete,3,0.0112848510 -LinkedList,отсортированный,insert,4,3.0345914950 -LinkedList,отсортированный,find,4,0.0240271220 -LinkedList,отсортированный,delete,4,0.0112117310 -LinkedList,отсортированный,insert,5,2.9481954700 -LinkedList,отсортированный,find,5,0.0239006100 -LinkedList,отсортированный,delete,5,0.0110857710 -LinkedList,отсортированный,insert,среднее,2.9983784638 -LinkedList,отсортированный,find,среднее,0.0239232324 -LinkedList,отсортированный,delete,среднее,0.0112577564 -HashTable,отсортированный,insert,1,0.1997087560 -HashTable,отсортированный,find,1,0.0017550400 -HashTable,отсортированный,delete,1,0.0008407980 -HashTable,отсортированный,insert,2,0.1968675190 -HashTable,отсортированный,find,2,0.0019886760 -HashTable,отсортированный,delete,2,0.0008920910 -HashTable,отсортированный,insert,3,0.1907563580 -HashTable,отсортированный,find,3,0.0018447440 -HashTable,отсортированный,delete,3,0.0008684640 -HashTable,отсортированный,insert,4,0.2625327630 -HashTable,отсортированный,find,4,0.0016053140 -HashTable,отсортированный,delete,4,0.0008098670 -HashTable,отсортированный,insert,5,0.1936840590 -HashTable,отсортированный,find,5,0.0019015160 -HashTable,отсортированный,delete,5,0.0009053780 -HashTable,отсортированный,insert,среднее,0.2087098910 -HashTable,отсортированный,find,среднее,0.0018190580 -HashTable,отсортированный,delete,среднее,0.0008633196 -BST,отсортированный,insert,1,4.2195800190 -BST,отсортированный,find,1,0.0389314570 -BST,отсортированный,delete,1,0.0190308920 -BST,отсортированный,insert,2,4.1356184250 -BST,отсортированный,find,2,0.0383339310 -BST,отсортированный,delete,2,0.0194247740 -BST,отсортированный,insert,3,4.1204731890 -BST,отсортированный,find,3,0.0388593320 -BST,отсортированный,delete,3,0.0215428460 -BST,отсортированный,insert,4,4.2120902370 -BST,отсортированный,find,4,0.0378190250 -BST,отсортированный,delete,4,0.0188528460 -BST,отсортированный,insert,5,4.1304951260 -BST,отсортированный,find,5,0.0359927840 -BST,отсортированный,delete,5,0.0179617110 -BST,отсортированный,insert,среднее,4.1636513992 -BST,отсортированный,find,среднее,0.0379873058 -BST,отсортированный,delete,среднее,0.0193626138 diff --git a/BolonkinNM/docs/report.md b/BolonkinNM/docs/report.md index 0a757c7..8eb21e6 100644 --- a/BolonkinNM/docs/report.md +++ b/BolonkinNM/docs/report.md @@ -1,101 +1,249 @@ -# Отчёт по заданию 1 — структуры данных +# Отчёт по работе «Поиск выхода из лабиринта» -## Цель работы +## 1. Цель работы +Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов. -Реализовать три структуры данных с нуля в процедурном стиле: +## 2. Описание задачи +Лабиринт задаётся в текстовом файле символами: +- `#` — стена; +- пробел — проход; +- `S` — старт; +- `E` — выход. -- связный список; -- хеш-таблицу; -- двоичное дерево поиска. +Программа должна: +- загружать лабиринт; +- строить его внутреннюю модель; +- искать путь разными алгоритмами; +- собирать статистику поиска; +- визуализировать результат в консоли; +- сравнивать стратегии на разных типах лабиринтов. -Также были выполнены измерения времени для операций `insert`, `find`, `delete` и построены графики по результатам эксперимента. +## 3. Выбранные паттерны проектирования -## Реализованные структуры +### 3.1 Builder +Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`. -### Связный список +Преимущества: +- легко добавить новый формат загрузки; +- клиентский код не зависит от формата файла; +- создание лабиринта можно расширять без переписывания остальной программы. -Узел хранится как словарь: +### 3.2 Strategy +Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию. -```python -{"name": "Имя", "phone": "123", "next": None} +Преимущества: +- алгоритм можно менять во время выполнения; +- код оркестратора не зависит от конкретного метода поиска; +- новые алгоритмы добавляются без изменения существующего кода. + +### 3.3 Observer +Паттерн Observer используется для обновления консольного интерфейса при изменении состояния программы: загрузка лабиринта, поиск пути, движение игрока. + +Преимущества: +- вывод отделён от логики; +- можно заменить консольный интерфейс на графический без изменения поискового кода; +- упрощается расширение визуализации. + +### 3.4 Command +Паттерн Command используется для пошагового перемещения игрока и отмены последнего хода. + +Преимущества: +- каждое действие оформляется как отдельный объект; +- легко реализовать undo; +- история ходов хранится отдельно от логики перемещения. + +## 4. Диаграмма классов +Ниже приведена упрощённая диаграмма классов в формате Mermaid: + +```mermaid +classDiagram + class Cell { + +int x + +int y + +bool isWall + +bool isStart + +bool isExit + +isPassable() + } + + class Maze { + +cells + +width + +height + +startCell + +exitCell + +getCell(x, y) + +getNeighbors(cell) + } + + class MazeBuilder { + <> + +buildFromFile(filename) + } + + class TextFileMazeBuilder { + +buildFromFile(filename) + } + + class PathFindingStrategy { + <> + +findPath(maze, start, exitCell) + } + + class BFSStrategy { + +findPath(maze, start, exitCell) + } + + class DFSStrategy { + +findPath(maze, start, exitCell) + } + + class AStarStrategy { + +findPath(maze, start, exitCell) + } + + class SearchStats { + +timeMs + +visitedCells + +pathLength + +path + } + + class MazeSolver { + +maze + +strategy + +setStrategy(strategy) + +solve() + } + + class Observer { + <> + +update(event) + } + + class ConsoleView { + +update(event) + +render(maze, player_position, path) + } + + class Command { + <> + +execute() + +undo() + } + + class MoveCommand { + +execute() + +undo() + } + + class Player { + +currentCell + +setCell(cell) + } + + Maze <|-- TextFileMazeBuilder : creates + MazeBuilder <|.. TextFileMazeBuilder + PathFindingStrategy <|.. BFSStrategy + PathFindingStrategy <|.. DFSStrategy + PathFindingStrategy <|.. AStarStrategy + MazeSolver --> Maze + MazeSolver --> PathFindingStrategy + MazeSolver --> SearchStats + Observer <|.. ConsoleView + Command <|.. MoveCommand + MoveCommand --> Player + MoveCommand --> Maze + ConsoleView --> Maze + Maze --> Cell ``` -### Хеш-таблица +## 5. Ключевые классы и их роль -Хранится как список бакетов фиксированной длины, где каждый бакет — голова связного списка или `None`. +### Cell +Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой. -### Двоичное дерево поиска +### Maze +Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям. -Узел хранится как словарь: +### TextFileMazeBuilder +Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`. -```python -{"name": "Имя", "phone": "123", "left": None, "right": None} -``` +### BFSStrategy +Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости. -Для BST использованы итеративные операции, чтобы корректно работать и на отсортированных данных. +### DFSStrategy +Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм. -## Методика эксперимента +### AStarStrategy +Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели. -- Количество записей: `N = 10000` -- Режимы данных: - - случайный порядок; - - отсортированный порядок. -- Каждое измерение повторялось **5 раз**. -- В CSV сохранены: - - все отдельные замеры; - - среднее время для каждой операции, структуры и режима. +### MazeSolver +Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику. -Операции: +### SearchStats +Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути. -- вставка всех записей; -- поиск 100 существующих и 10 отсутствующих имён; -- удаление 50 случайных имён. +### ConsoleView +Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль. -## Графики +### MoveCommand +Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения. -![insert](data/insert.png) +## 6. Экспериментальная часть -![find](data/find.png) +### 6.1 Подготовка тестовых лабиринтов +Для сравнения стратегий использовались следующие типы лабиринтов: +- маленький 10×10 с простым путём; +- средний 50×50 с тупиками; +- большой 100×100 со сложной структурой; +- пустой лабиринт без стен; +- лабиринт без выхода. -![delete](data/delete.png) +### 6.2 Методика измерений +Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения: +- время поиска в миллисекундах; +- количество посещённых клеток; +- длина найденного пути. -## Средние результаты +Результаты сохранялись в CSV-файл в двух вариантах: +- сырой набор измерений; +- усреднённая таблица. -| Режим | Операция | LinkedList | HashTable | BST | Лучший результат | -|---|---:|---:|---:|---:|---| -| случайный | insert | 3.949566 | 0.208964 | 0.018028 | BST | -| случайный | find | 0.033693 | 0.001534 | 0.000190 | BST | -| случайный | delete | 0.016076 | 0.000727 | 0.000102 | BST | -| отсортированный | insert | 2.998378 | 0.208710 | 4.163651 | HashTable | -| отсортированный | find | 0.023923 | 0.001819 | 0.037987 | HashTable | -| отсортированный | delete | 0.011258 | 0.000863 | 0.019363 | HashTable | +## 7. Анализ эффективности -## Анализ результатов +### BFS +BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах. -### Влияние порядка входных данных на BST +### DFS +DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области. -На случайных данных BST работает значительно быстрее, чем на отсортированных. Это связано с тем, что при случайной вставке дерево остаётся ближе к сбалансированному состоянию. +### A* +A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода. -На отсортированных данных дерево вырождается в цепочку, поэтому вставка становится медленной, а поиск и удаление тоже деградируют по времени. +### Лабиринт без пути +Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения. -### Почему хеш-таблица почти не чувствительна к порядку +### Вывод по выбору алгоритма +- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы. +- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности. +- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток. +- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*. -Хеш-таблица распределяет элементы по бакетам через хеш-функцию, поэтому сам порядок входа почти не влияет на скорость. Влияние может появляться только из-за коллизий, но в целом поведение остаётся близким к постоянному времени. +## 8. Роль ООП и паттернов +ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому: +- можно заменить алгоритм поиска без переписывания логики программы; +- можно добавить новый формат загрузки лабиринта; +- можно поменять способ визуализации; +- можно расширить управление игроком и добавить отмену действий. -### Почему связный список всегда медленен при поиске +Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения. -Поиск в связном списке выполняется последовательным просмотром элементов. Поэтому при большом количестве записей приходится проходить много узлов, и операция остаётся линейной по времени. +## 9. Вывод +В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах. -### Как удаление работает в каждой структуре - -- В связном списке нужно сначала найти нужный узел, затем переназначить ссылку. -- В хеш-таблице сначала выбирается бакет, затем удаление выполняется внутри короткой цепочки. -- В BST удаление зависит от числа потомков: если потомок один или ноль, операция простая; если два — нужно найти преемника. - -## Вывод - -Для частых вставок и особенно частого поиска в реальной задаче чаще всего лучше подходит **хеш-таблица**. - -Если важно получать данные в отсортированном виде, удобнее использовать **BST**. - -**Связный список** подходит для маленьких объёмов данных или очень простых сценариев, но при большом числе записей он проигрывает по скорости поиска. +## 10. Приложения +- Листинги ключевых классов. +- CSV-файлы с результатами экспериментов. +- Графики сравнений. +- Файлы с тестовыми лабиринтами. diff --git a/BolonkinNM/experiment.py b/BolonkinNM/experiment.py new file mode 100644 index 0000000..588f377 --- /dev/null +++ b/BolonkinNM/experiment.py @@ -0,0 +1,225 @@ +from pathlib import Path +from statistics import mean +import csv +import random + +import matplotlib.pyplot as plt + +from core.cell import Cell +from core.maze import Maze +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from strategies.dijkstra_strategy import DijkstraStrategy + + +BASE_DIR = Path(__file__).resolve().parent +OUT_DIR = BASE_DIR / "experiment_results" + + +def build_maze_from_symbols(lines): + height = len(lines) + width = max(len(line) for line in lines) + cells = [] + start = None + exit_cell = None + for y, line in enumerate(lines): + row = [] + for x in range(width): + ch = line[x] if x < len(line) else "#" + if ch == "#": + cell = Cell(x, y, isWall=True) + elif ch == "S": + cell = Cell(x, y, isWall=False, isStart=True) + start = cell + elif ch == "E": + cell = Cell(x, y, isWall=False, isExit=True) + exit_cell = cell + elif ch == " " or ch == ".": + cell = Cell(x, y, isWall=False) + elif ch.isdigit(): + cell = Cell(x, y, isWall=False, weight=int(ch)) + else: + raise ValueError(f"Unknown symbol '{ch}' at {x},{y}") + row.append(cell) + cells.append(row) + return Maze(cells, width, height, start, exit_cell) + + +def generate_empty_maze(width, height): + lines = [" " * width for _ in range(height)] + lines = [list(row) for row in lines] + lines[1][1] = "S" + lines[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in lines]) + + +def generate_simple_maze(width, height): + grid = [["#" for _ in range(width)] for _ in range(height)] + for x in range(1, width - 1): + grid[1][x] = " " + for y in range(1, height - 1): + grid[y][width - 2] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_branching_maze(width, height, seed=42, wall_density=0.30): + rng = random.Random(seed) + grid = [["#" for _ in range(width)] for _ in range(height)] + x, y = 1, 1 + grid[y][x] = "S" + while (x, y) != (width - 2, height - 2): + candidates = [] + for dx, dy in [(1, 0), (0, 1)]: + nx, ny = x + dx, y + dy + if 1 <= nx < width - 1 and 1 <= ny < height - 1: + candidates.append((nx, ny)) + if not candidates: + break + x, y = rng.choice(candidates) + grid[y][x] = " " + grid[height - 2][width - 2] = "E" + + # carve extra corridors and dead ends + for yy in range(1, height - 1): + for xx in range(1, width - 1): + if grid[yy][xx] == "#" and rng.random() > wall_density: + grid[yy][xx] = " " + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_no_path_maze(width, height): + grid = [[" " for _ in range(width)] for _ in range(height)] + for x in range(width): + grid[height // 2][x] = "#" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def generate_weighted_maze(width, height, seed=123): + rng = random.Random(seed) + grid = [[" " for _ in range(width)] for _ in range(height)] + for y in range(height): + for x in range(width): + r = rng.random() + if r < 0.12: + grid[y][x] = "#" + elif r < 0.25: + grid[y][x] = "3" + elif r < 0.40: + grid[y][x] = "2" + else: + grid[y][x] = "1" + # ensure path-ish + for x in range(width): + grid[1][x] = "1" + for y in range(1, height): + grid[y][width - 2] = "1" + grid[1][1] = "S" + grid[height - 2][width - 2] = "E" + return build_maze_from_symbols(["".join(row) for row in grid]) + + +def bench_one_maze(maze_name, maze, strategies, repeats=5): + summary_rows = [] + raw_rows = [] + for strategy_name, strategy_factory in strategies: + times, visiteds, lengths = [], [], [] + for run in range(1, repeats + 1): + solver = MazeSolver(maze) + solver.setStrategy(strategy_factory()) + stats = solver.solve() + raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength]) + times.append(stats.timeMs) + visiteds.append(stats.visitedCells) + lengths.append(stats.pathLength) + summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats]) + return summary_rows, raw_rows + + +def save_csv(path, rows): + with open(path, "w", newline="", encoding="utf-8") as f: + csv.writer(f).writerows(rows) + + +def plot_summary(summary_rows): + by_maze = {} + for row in summary_rows[1:]: + maze_name, strategy, avg_time, avg_visited, avg_len, runs = row + by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len))) + + for maze_name, items in by_maze.items(): + items.sort(key=lambda t: t[0]) + strategies = [i[0] for i in items] + x = list(range(len(strategies))) + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[1] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("ms") + plt.title(f"{maze_name} — avg time") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[2] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — visited cells") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150) + plt.close() + + plt.figure(figsize=(8, 4)) + plt.bar(x, [i[3] for i in items]) + plt.xticks(x, strategies) + plt.ylabel("cells") + plt.title(f"{maze_name} — path length") + plt.tight_layout() + plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150) + plt.close() + + +def main(): + OUT_DIR.mkdir(exist_ok=True) + + strategies = [ + ("BFS", BFSStrategy), + ("DFS", DFSStrategy), + ("A*", AStarStrategy), + ("Dijkstra", DijkstraStrategy), + ] + + mazes = [ + ("small_10x10", generate_simple_maze(10, 10)), + ("medium_50x50", generate_branching_maze(50, 50)), + ("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)), + ("empty_30x30", generate_empty_maze(30, 30)), + ("no_path_30x30", generate_no_path_maze(30, 30)), + ("weighted_30x30", generate_weighted_maze(30, 30)), + ] + + summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]] + raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]] + + for maze_name, maze in mazes: + s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5) + summary.extend(s_rows) + raw.extend(r_rows) + + save_csv(OUT_DIR / "summary.csv", summary) + save_csv(OUT_DIR / "raw.csv", raw) + plot_summary(summary) + + print("Saved to", OUT_DIR.resolve()) + + +if __name__ == "__main__": + main() diff --git a/BolonkinNM/experiment_results/empty_30x30_length.png b/BolonkinNM/experiment_results/empty_30x30_length.png new file mode 100644 index 0000000..ba6a3b6 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/empty_30x30_time.png b/BolonkinNM/experiment_results/empty_30x30_time.png new file mode 100644 index 0000000..85aca79 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/empty_30x30_visited.png b/BolonkinNM/experiment_results/empty_30x30_visited.png new file mode 100644 index 0000000..8f7bac7 Binary files /dev/null and b/BolonkinNM/experiment_results/empty_30x30_visited.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_length.png b/BolonkinNM/experiment_results/large_100x100_length.png new file mode 100644 index 0000000..7f8c7e2 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_length.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_time.png b/BolonkinNM/experiment_results/large_100x100_time.png new file mode 100644 index 0000000..50bd2b5 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_time.png differ diff --git a/BolonkinNM/experiment_results/large_100x100_visited.png b/BolonkinNM/experiment_results/large_100x100_visited.png new file mode 100644 index 0000000..11bca38 Binary files /dev/null and b/BolonkinNM/experiment_results/large_100x100_visited.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_length.png b/BolonkinNM/experiment_results/medium_50x50_length.png new file mode 100644 index 0000000..146dedc Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_length.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_time.png b/BolonkinNM/experiment_results/medium_50x50_time.png new file mode 100644 index 0000000..e99ecfc Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_time.png differ diff --git a/BolonkinNM/experiment_results/medium_50x50_visited.png b/BolonkinNM/experiment_results/medium_50x50_visited.png new file mode 100644 index 0000000..a2b683d Binary files /dev/null and b/BolonkinNM/experiment_results/medium_50x50_visited.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_length.png b/BolonkinNM/experiment_results/no_path_30x30_length.png new file mode 100644 index 0000000..cbd8be8 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_time.png b/BolonkinNM/experiment_results/no_path_30x30_time.png new file mode 100644 index 0000000..68a92e3 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/no_path_30x30_visited.png b/BolonkinNM/experiment_results/no_path_30x30_visited.png new file mode 100644 index 0000000..1cc5a63 Binary files /dev/null and b/BolonkinNM/experiment_results/no_path_30x30_visited.png differ diff --git a/BolonkinNM/experiment_results/raw.csv b/BolonkinNM/experiment_results/raw.csv new file mode 100644 index 0000000..800dfef --- /dev/null +++ b/BolonkinNM/experiment_results/raw.csv @@ -0,0 +1,121 @@ +maze,strategy,run,time_ms,visited_cells,path_length +small_10x10,BFS,1,0.044300,15,15 +small_10x10,BFS,2,0.022800,15,15 +small_10x10,BFS,3,0.020400,15,15 +small_10x10,BFS,4,0.020300,15,15 +small_10x10,BFS,5,0.018700,15,15 +small_10x10,DFS,1,0.031200,15,15 +small_10x10,DFS,2,0.022000,15,15 +small_10x10,DFS,3,0.021200,15,15 +small_10x10,DFS,4,0.020800,15,15 +small_10x10,DFS,5,0.020500,15,15 +small_10x10,A*,1,0.048900,15,15 +small_10x10,A*,2,0.034700,15,15 +small_10x10,A*,3,0.029400,15,15 +small_10x10,A*,4,0.029100,15,15 +small_10x10,A*,5,0.029300,15,15 +small_10x10,Dijkstra,1,0.037900,15,15 +small_10x10,Dijkstra,2,0.028500,15,15 +small_10x10,Dijkstra,3,0.026800,15,15 +small_10x10,Dijkstra,4,0.026400,15,15 +small_10x10,Dijkstra,5,0.026700,15,15 +medium_50x50,BFS,1,2.105800,1579,95 +medium_50x50,BFS,2,1.928700,1579,95 +medium_50x50,BFS,3,1.969500,1579,95 +medium_50x50,BFS,4,1.938800,1579,95 +medium_50x50,BFS,5,1.943600,1579,95 +medium_50x50,DFS,1,1.927300,1277,647 +medium_50x50,DFS,2,1.856300,1277,647 +medium_50x50,DFS,3,1.890100,1277,647 +medium_50x50,DFS,4,1.868000,1277,647 +medium_50x50,DFS,5,1.865500,1277,647 +medium_50x50,A*,1,2.359000,927,95 +medium_50x50,A*,2,2.193700,927,95 +medium_50x50,A*,3,2.178400,927,95 +medium_50x50,A*,4,2.181800,927,95 +medium_50x50,A*,5,2.174500,927,95 +medium_50x50,Dijkstra,1,3.534700,1579,95 +medium_50x50,Dijkstra,2,3.435500,1579,95 +medium_50x50,Dijkstra,3,3.457600,1579,95 +medium_50x50,Dijkstra,4,3.417300,1579,95 +medium_50x50,Dijkstra,5,3.538000,1579,95 +large_100x100,BFS,1,8.624100,5566,195 +large_100x100,BFS,2,7.706900,5566,195 +large_100x100,BFS,3,9.723300,5566,195 +large_100x100,BFS,4,7.585700,5566,195 +large_100x100,BFS,5,8.031300,5566,195 +large_100x100,DFS,1,5.512400,3543,1531 +large_100x100,DFS,2,5.329300,3543,1531 +large_100x100,DFS,3,5.223300,3543,1531 +large_100x100,DFS,4,5.729900,3543,1531 +large_100x100,DFS,5,5.497400,3543,1531 +large_100x100,A*,1,2.101500,853,195 +large_100x100,A*,2,2.264500,853,195 +large_100x100,A*,3,2.064100,853,195 +large_100x100,A*,4,2.031700,853,195 +large_100x100,A*,5,2.046500,853,195 +large_100x100,Dijkstra,1,25.021300,5571,195 +large_100x100,Dijkstra,2,13.541100,5571,195 +large_100x100,Dijkstra,3,12.884100,5571,195 +large_100x100,Dijkstra,4,13.481800,5571,195 +large_100x100,Dijkstra,5,12.748000,5571,195 +empty_30x30,BFS,1,1.234300,896,55 +empty_30x30,BFS,2,1.163400,896,55 +empty_30x30,BFS,3,1.145700,896,55 +empty_30x30,BFS,4,1.177300,896,55 +empty_30x30,BFS,5,1.175100,896,55 +empty_30x30,DFS,1,1.338000,842,815 +empty_30x30,DFS,2,1.296500,842,815 +empty_30x30,DFS,3,1.296700,842,815 +empty_30x30,DFS,4,1.280100,842,815 +empty_30x30,DFS,5,1.290800,842,815 +empty_30x30,A*,1,2.183400,784,55 +empty_30x30,A*,2,2.522900,784,55 +empty_30x30,A*,3,1.985000,784,55 +empty_30x30,A*,4,1.972100,784,55 +empty_30x30,A*,5,2.088600,784,55 +empty_30x30,Dijkstra,1,2.080400,896,55 +empty_30x30,Dijkstra,2,2.100100,896,55 +empty_30x30,Dijkstra,3,2.130700,896,55 +empty_30x30,Dijkstra,4,2.073600,896,55 +empty_30x30,Dijkstra,5,2.095900,896,55 +no_path_30x30,BFS,1,0.645900,450,0 +no_path_30x30,BFS,2,0.566600,450,0 +no_path_30x30,BFS,3,0.566000,450,0 +no_path_30x30,BFS,4,0.583500,450,0 +no_path_30x30,BFS,5,0.568900,450,0 +no_path_30x30,DFS,1,0.692100,450,0 +no_path_30x30,DFS,2,0.676900,450,0 +no_path_30x30,DFS,3,0.703500,450,0 +no_path_30x30,DFS,4,0.722300,450,0 +no_path_30x30,DFS,5,0.672000,450,0 +no_path_30x30,A*,1,1.112700,450,0 +no_path_30x30,A*,2,1.130000,450,0 +no_path_30x30,A*,3,1.096100,450,0 +no_path_30x30,A*,4,1.111400,450,0 +no_path_30x30,A*,5,1.183500,450,0 +no_path_30x30,Dijkstra,1,1.023300,450,0 +no_path_30x30,Dijkstra,2,1.011700,450,0 +no_path_30x30,Dijkstra,3,1.127200,450,0 +no_path_30x30,Dijkstra,4,1.110200,450,0 +no_path_30x30,Dijkstra,5,1.043900,450,0 +weighted_30x30,BFS,1,1.074700,788,55 +weighted_30x30,BFS,2,0.997700,788,55 +weighted_30x30,BFS,3,0.992700,788,55 +weighted_30x30,BFS,4,1.010800,788,55 +weighted_30x30,BFS,5,1.035000,788,55 +weighted_30x30,DFS,1,1.130200,693,479 +weighted_30x30,DFS,2,1.057400,693,479 +weighted_30x30,DFS,3,1.049900,693,479 +weighted_30x30,DFS,4,1.051600,693,479 +weighted_30x30,DFS,5,1.059100,693,479 +weighted_30x30,A*,1,0.402200,126,55 +weighted_30x30,A*,2,0.384100,126,55 +weighted_30x30,A*,3,0.360000,126,55 +weighted_30x30,A*,4,0.360700,126,55 +weighted_30x30,A*,5,0.353500,126,55 +weighted_30x30,Dijkstra,1,1.834900,781,55 +weighted_30x30,Dijkstra,2,1.759000,781,55 +weighted_30x30,Dijkstra,3,1.786300,781,55 +weighted_30x30,Dijkstra,4,1.740500,781,55 +weighted_30x30,Dijkstra,5,1.807100,781,55 diff --git a/BolonkinNM/experiment_results/small_10x10_length.png b/BolonkinNM/experiment_results/small_10x10_length.png new file mode 100644 index 0000000..8dc2d78 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_length.png differ diff --git a/BolonkinNM/experiment_results/small_10x10_time.png b/BolonkinNM/experiment_results/small_10x10_time.png new file mode 100644 index 0000000..dcf10e1 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_time.png differ diff --git a/BolonkinNM/experiment_results/small_10x10_visited.png b/BolonkinNM/experiment_results/small_10x10_visited.png new file mode 100644 index 0000000..98fe889 Binary files /dev/null and b/BolonkinNM/experiment_results/small_10x10_visited.png differ diff --git a/BolonkinNM/experiment_results/summary.csv b/BolonkinNM/experiment_results/summary.csv new file mode 100644 index 0000000..46a0412 --- /dev/null +++ b/BolonkinNM/experiment_results/summary.csv @@ -0,0 +1,25 @@ +maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs +small_10x10,BFS,0.025300,15.00,15.00,5 +small_10x10,DFS,0.023140,15.00,15.00,5 +small_10x10,A*,0.034280,15.00,15.00,5 +small_10x10,Dijkstra,0.029260,15.00,15.00,5 +medium_50x50,BFS,1.977280,1579.00,95.00,5 +medium_50x50,DFS,1.881440,1277.00,647.00,5 +medium_50x50,A*,2.217480,927.00,95.00,5 +medium_50x50,Dijkstra,3.476620,1579.00,95.00,5 +large_100x100,BFS,8.334260,5566.00,195.00,5 +large_100x100,DFS,5.458460,3543.00,1531.00,5 +large_100x100,A*,2.101660,853.00,195.00,5 +large_100x100,Dijkstra,15.535260,5571.00,195.00,5 +empty_30x30,BFS,1.179160,896.00,55.00,5 +empty_30x30,DFS,1.300420,842.00,815.00,5 +empty_30x30,A*,2.150400,784.00,55.00,5 +empty_30x30,Dijkstra,2.096140,896.00,55.00,5 +no_path_30x30,BFS,0.586180,450.00,0.00,5 +no_path_30x30,DFS,0.693360,450.00,0.00,5 +no_path_30x30,A*,1.126740,450.00,0.00,5 +no_path_30x30,Dijkstra,1.063260,450.00,0.00,5 +weighted_30x30,BFS,1.022180,788.00,55.00,5 +weighted_30x30,DFS,1.069640,693.00,479.00,5 +weighted_30x30,A*,0.372100,126.00,55.00,5 +weighted_30x30,Dijkstra,1.785560,781.00,55.00,5 diff --git a/BolonkinNM/experiment_results/weighted_30x30_length.png b/BolonkinNM/experiment_results/weighted_30x30_length.png new file mode 100644 index 0000000..7c7e3b1 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_length.png differ diff --git a/BolonkinNM/experiment_results/weighted_30x30_time.png b/BolonkinNM/experiment_results/weighted_30x30_time.png new file mode 100644 index 0000000..45196c3 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_time.png differ diff --git a/BolonkinNM/experiment_results/weighted_30x30_visited.png b/BolonkinNM/experiment_results/weighted_30x30_visited.png new file mode 100644 index 0000000..3b02d70 Binary files /dev/null and b/BolonkinNM/experiment_results/weighted_30x30_visited.png differ diff --git a/BolonkinNM/experiments.py b/BolonkinNM/experiments.py deleted file mode 100644 index f5face1..0000000 --- a/BolonkinNM/experiments.py +++ /dev/null @@ -1,172 +0,0 @@ -from __future__ import annotations - -import csv -import random -import time -from pathlib import Path -from typing import Dict, List, Tuple - -from linked_list import ll_insert, ll_find, ll_delete -from hash_table import ht_insert, ht_find, ht_delete -from bst import bst_insert, bst_find, bst_delete -from utils import generate_records, prepare_records_variants - - -Record = Tuple[str, str] - - -def make_missing_names(count: int = 10) -> List[str]: - return [f"None_{i}" for i in range(count)] - - -def pick_existing_names(records: List[Record], count: int, seed: int = 42) -> List[str]: - rng = random.Random(seed) - unique_names = list(dict.fromkeys(name for name, _ in records)) - if len(unique_names) < count: - raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}") - return rng.sample(unique_names, count) - - -def pick_delete_names(records: List[Record], count: int = 50, seed: int = 43) -> List[str]: - rng = random.Random(seed) - unique_names = list(dict.fromkeys(name for name, _ in records)) - if len(unique_names) < count: - raise ValueError(f"Not enough unique names: need {count}, got {len(unique_names)}") - return rng.sample(unique_names, count) - - -def build_structure(structure_name: str, records: List[Record], buckets_count: int = 2048): - if structure_name == "linked_list": - structure = None - for name, phone in records: - structure = ll_insert(structure, name, phone) - return structure - - if structure_name == "hash_table": - buckets = [None] * buckets_count - for name, phone in records: - buckets = 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 do_find(structure_name: str, structure: object, existing_names: List[str], missing_names: List[str]) -> None: - if structure_name == "linked_list": - for name in existing_names: - ll_find(structure, name) - for name in missing_names: - ll_find(structure, name) - return - - if structure_name == "hash_table": - for name in existing_names: - ht_find(structure, name) - for name in missing_names: - ht_find(structure, name) - return - - if structure_name == "bst": - for name in existing_names: - bst_find(structure, name) - for name in missing_names: - bst_find(structure, name) - return - - raise ValueError(f"Unknown structure: {structure_name}") - - -def do_delete(structure_name: str, structure: object, delete_names: List[str]): - if structure_name == "linked_list": - for name in delete_names: - structure = ll_delete(structure, name) - return structure - - if structure_name == "hash_table": - for name in delete_names: - structure = ht_delete(structure, name) - return structure - - if structure_name == "bst": - for name in delete_names: - structure = bst_delete(structure, name) - return structure - - raise ValueError(f"Unknown structure: {structure_name}") - - -def measure_once(structure_name: str, records: List[Record], buckets_count: int = 2048) -> Dict[str, float]: - existing_names = pick_existing_names(records, 100, seed=42) - missing_names = make_missing_names(10) - delete_names = pick_delete_names(records, 50, seed=43) - - start = time.perf_counter() - structure = build_structure(structure_name, records, buckets_count=buckets_count) - insert_time = time.perf_counter() - start - - start = time.perf_counter() - do_find(structure_name, structure, existing_names, missing_names) - find_time = time.perf_counter() - start - - start = time.perf_counter() - structure = do_delete(structure_name, structure, delete_names) - delete_time = time.perf_counter() - start - - return {"insert": insert_time, "find": find_time, "delete": delete_time} - - -def run_experiments(n: int = 10000, buckets_count: int = 2048, repeats: int = 5): - records = generate_records(n, repeat_names=False) - records_shuffled, records_sorted = prepare_records_variants(records) - - datasets = [ - ("случайный", records_shuffled), - ("отсортированный", records_sorted), - ] - structures = [ - ("LinkedList", "linked_list"), - ("HashTable", "hash_table"), - ("BST", "bst"), - ] - operations = ("insert", "find", "delete") - - rows = [["Структура", "Режим", "Операция", "Замер", "Время (сек)"]] - - for mode_name, dataset_records in datasets: - for human_name, structure_name in structures: - times_by_op = {op: [] for op in operations} - - for attempt in range(1, repeats + 1): - result = measure_once(structure_name, dataset_records, buckets_count=buckets_count) - for op_name in operations: - elapsed = result[op_name] - times_by_op[op_name].append(elapsed) - rows.append([human_name, mode_name, op_name, attempt, f"{elapsed:.10f}"]) - - for op_name in operations: - avg_time = sum(times_by_op[op_name]) / len(times_by_op[op_name]) - rows.append([human_name, mode_name, op_name, "среднее", f"{avg_time:.10f}"]) - - return rows - - -def save_results_csv(rows, filename: str = "results.csv"): - with open(filename, "w", newline="", encoding="utf-8") as f: - writer = csv.writer(f) - writer.writerows(rows) - - -def main(): - rows = run_experiments(n=10000, buckets_count=2048, repeats=5) - save_results_csv(rows, "results.csv") - print("Saved results.csv") - - -if __name__ == "__main__": - main() diff --git a/BolonkinNM/hash_table.py b/BolonkinNM/hash_table.py deleted file mode 100644 index 9aa720d..0000000 --- a/BolonkinNM/hash_table.py +++ /dev/null @@ -1,44 +0,0 @@ - - -from typing import Any, Dict, List, Optional - -from linked_list import ll_insert, ll_find, ll_delete, ll_list_all - - -Bucket = Optional[Dict[str, Any]] - - -def _hash_name(name: str, buckets_count: int) -> int: - if buckets_count <= 0: - return 0 - return sum(ord(ch) for ch in name) % buckets_count - - -def ht_insert(buckets: List[Bucket], name: str, phone: str) -> List[Bucket]: - if not buckets: - return buckets - index = _hash_name(name, len(buckets)) - buckets[index] = ll_insert(buckets[index], name, phone) - return buckets - - -def ht_find(buckets: List[Bucket], name: str) -> Optional[str]: - if not buckets: - return None - index = _hash_name(name, len(buckets)) - return ll_find(buckets[index], name) - - -def ht_delete(buckets: List[Bucket], name: str) -> List[Bucket]: - if not buckets: - return buckets - index = _hash_name(name, len(buckets)) - buckets[index] = ll_delete(buckets[index], name) - return buckets - - -def ht_list_all(buckets: List[Bucket]) -> List[Dict[str, str]]: - records: List[Dict[str, str]] = [] - for head in buckets: - records.extend(ll_list_all(head)) - return sorted(records, key=lambda x: x["name"]) diff --git a/BolonkinNM/linked_list.py b/BolonkinNM/linked_list.py deleted file mode 100644 index 0260036..0000000 --- a/BolonkinNM/linked_list.py +++ /dev/null @@ -1,73 +0,0 @@ - - -from typing import Any, Dict, List, Optional - - -Node = Dict[str, Any] - - -def _make_node(name: str, phone: str) -> Node: - return {"name": name, "phone": phone, "next": None} - - -def sort_records(records: List[Dict[str, str]]) -> List[Dict[str, str]]: - - return sorted(records, key=lambda x: x["name"]) - - -def ll_insert(head: Optional[Node], name: str, phone: str) -> Node: - - new_node = _make_node(name, phone) - - if head is None: - return new_node - - current = head - while current is not None: - if current["name"] == name: - current["phone"] = phone - return head - if current["next"] is None: - current["next"] = new_node - return head - current = current["next"] - - return head - - -def ll_find(head: Optional[Node], name: str) -> Optional[str]: - current = head - while current is not None: - if current["name"] == name: - return current["phone"] - current = current["next"] - return None - - -def ll_delete(head: Optional[Node], name: str) -> Optional[Node]: - if head is None: - return None - - if head["name"] == name: - return head["next"] - - prev = head - current = head["next"] - - while current is not None: - if current["name"] == name: - prev["next"] = current["next"] - return head - prev = current - current = current["next"] - - return head - - -def ll_list_all(head: Optional[Node]) -> List[Dict[str, str]]: - records: List[Dict[str, str]] = [] - current = head - while current is not None: - records.append({"name": current["name"], "phone": current["phone"]}) - current = current["next"] - return sort_records(records) diff --git a/BolonkinNM/main.py b/BolonkinNM/main.py index 70de618..08f22c7 100644 --- a/BolonkinNM/main.py +++ b/BolonkinNM/main.py @@ -1,21 +1,59 @@ +from builders.text_file_maze_builder import TextFileMazeBuilder +from core.player import Player +from observer.console_view import ConsoleView +from solver.maze_solver import MazeSolver +from strategies.astar_strategy import AStarStrategy +from strategies.bfs_strategy import BFSStrategy +from strategies.dfs_strategy import DFSStrategy +from controller.game_controller import GameController -from __future__ import annotations - -import csv from pathlib import Path -from experiments import run_experiments, save_results_csv -from plot_results import build_graphs, load_average_results +BASE_DIR = Path(__file__).resolve().parent -def main(): - rows = run_experiments(n=10000, buckets_count=2048, repeats=5) - save_results_csv(rows, "results.csv") - averaged = load_average_results("results.csv") - build_graphs(averaged, output_dir="docs/data") - print("Done.") +def run_demo(): + builder = TextFileMazeBuilder() + maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt")) + + view = ConsoleView() + view.update({"type": "maze_loaded", "message": "Maze loaded"}) + view.render(maze) + + solver = MazeSolver(maze) + solver.addObserver(view) + + for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()): + solver.setStrategy(strategy) + stats = solver.solve() + + print() + print(f"=== {strategy.name} ===") + print(f"Time: {stats.timeMs:.3f} ms") + print(f"Visited cells: {stats.visitedCells}") + print(f"Path length: {stats.pathLength}") + print(f"Path found: {'yes' if stats.found else 'no'}") + + view.render(maze, path=stats.path) + + player = Player(maze.startCell) + controller = GameController(maze, player, view) + + print("Manual mode: W/A/S/D move, Z undo, Q quit") + view.render(maze, player_position=player.currentCell) + + while True: + cmd = input("Command: ").strip().upper() + if cmd == "Q": + break + if cmd == "Z": + controller.undo() + elif cmd in {"W", "A", "S", "D"}: + controller.move(cmd) + else: + print("Unknown command") if __name__ == "__main__": - main() + run_demo() diff --git a/BolonkinNM/mazes/maze_empty.txt b/BolonkinNM/mazes/maze_empty.txt new file mode 100644 index 0000000..8267fd0 --- /dev/null +++ b/BolonkinNM/mazes/maze_empty.txt @@ -0,0 +1,9 @@ +S + + + + + + + + E diff --git a/BolonkinNM/mazes/maze_large.txt b/BolonkinNM/mazes/maze_large.txt new file mode 100644 index 0000000..eb03326 --- /dev/null +++ b/BolonkinNM/mazes/maze_large.txt @@ -0,0 +1,11 @@ +#################################################################################################### +#S # # # # # # # # # # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ## +# # # # # # # # # # # # # # # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ## +# # # # # # # # # # # # # # # # # # # # # +### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### #### +# # # # # # # # # # # # # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## # +# # # # # # # # # +#################################################################################################### diff --git a/BolonkinNM/mazes/maze_medium.txt b/BolonkinNM/mazes/maze_medium.txt new file mode 100644 index 0000000..67ecd65 --- /dev/null +++ b/BolonkinNM/mazes/maze_medium.txt @@ -0,0 +1,11 @@ +################################################## +#S # # # # # # E# +# # ### ### # ###### # ### # ## # #### # ####### ## +# # # # # # # # # # # # # # +# ##### # ######## # ### # ## # #### # ####### ## # +# # # # # # # # # # +### # # ###### # ########### ########### ### ###### +# # # # # # # # # # # +# ### ###### # ##### # ### # ####### # ### ### ## # +# # # # # +################################################## diff --git a/BolonkinNM/mazes/maze_no_path.txt b/BolonkinNM/mazes/maze_no_path.txt new file mode 100644 index 0000000..9633160 --- /dev/null +++ b/BolonkinNM/mazes/maze_no_path.txt @@ -0,0 +1,9 @@ +########## +#S # +# ###### # +# # # +########## +# #E# +# ###### # +# # +########## diff --git a/BolonkinNM/mazes/maze_small.txt b/BolonkinNM/mazes/maze_small.txt new file mode 100644 index 0000000..e829a58 --- /dev/null +++ b/BolonkinNM/mazes/maze_small.txt @@ -0,0 +1,7 @@ +########## +#S #E# +# ## # # ## +# # # +# #### # # +# # # +########## diff --git a/BolonkinNM/mazes/maze_weighted.txt b/BolonkinNM/mazes/maze_weighted.txt new file mode 100644 index 0000000..be8718d --- /dev/null +++ b/BolonkinNM/mazes/maze_weighted.txt @@ -0,0 +1,10 @@ +1111111111111111111111111111 +1S11111111111111111111111111 +1111111111111111111111111111 +1111111111111111111111111111 +1111111111111222222222222111 +1111111111111222222222222111 +1111111111111333333333333111 +1111111111111333333333333111 +111111111111111111111111111E +1111111111111111111111111111 diff --git a/BolonkinNM/observer/__init__.py b/BolonkinNM/observer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/observer/console_view.py b/BolonkinNM/observer/console_view.py new file mode 100644 index 0000000..77248a5 --- /dev/null +++ b/BolonkinNM/observer/console_view.py @@ -0,0 +1,26 @@ +import os +from observer.observer import Observer + + +class ConsoleView(Observer): + def update(self, event): + if isinstance(event, str): + print(f"[EVENT] {event}") + elif isinstance(event, dict): + event_type = event.get("type", "unknown") + if event_type == "search_finished": + stats = event.get("stats") + print(f"[EVENT] search finished: {stats}") + else: + print(f"[EVENT] {event_type}: {event}") + else: + print("[EVENT] unknown") + + def clear(self): + os.system("cls" if os.name == "nt" else "clear") + + def render(self, maze, player_position=None, path=None, clear_screen=False): + if clear_screen: + self.clear() + print(maze.render(player_position=player_position, path=path)) + print() diff --git a/BolonkinNM/observer/observer.py b/BolonkinNM/observer/observer.py new file mode 100644 index 0000000..0ccca59 --- /dev/null +++ b/BolonkinNM/observer/observer.py @@ -0,0 +1,7 @@ +from abc import ABC, abstractmethod + + +class Observer(ABC): + @abstractmethod + def update(self, event): + raise NotImplementedError diff --git a/BolonkinNM/plot_results.py b/BolonkinNM/plot_results.py deleted file mode 100644 index f4f3b6c..0000000 --- a/BolonkinNM/plot_results.py +++ /dev/null @@ -1,60 +0,0 @@ - - -from __future__ import annotations - -import csv -from collections import defaultdict -from pathlib import Path - -import matplotlib.pyplot as plt - - -def load_average_results(csv_file: str): - results = [] - with open(csv_file, "r", encoding="utf-8") as f: - reader = csv.DictReader(f) - for row in reader: - if row["Замер"] != "среднее": - continue - results.append({ - "structure": row["Структура"], - "mode": row["Режим"], - "operation": row["Операция"], - "time": float(row["Время (сек)"]), - }) - return results - - -def build_graphs(results, output_dir: str = "docs/data"): - output = Path(output_dir) - output.mkdir(parents=True, exist_ok=True) - - grouped = defaultdict(list) - for row in results: - grouped[row["operation"]].append(row) - - for operation in ("insert", "find", "delete"): - rows = grouped[operation] - labels = [f"{r['structure']}\n{r['mode']}" for r in rows] - values = [r["time"] for r in rows] - - plt.figure(figsize=(11, 6)) - plt.bar(labels, values) - plt.title(f"{operation.capitalize()} comparison") - plt.xlabel("Structure / data order") - plt.ylabel("Time, seconds") - plt.xticks(rotation=20) - plt.tight_layout() - filename = output / f"{operation}.png" - plt.savefig(filename, dpi=160) - plt.close() - print(f"Saved {filename}") - - -def main(): - results = load_average_results("results.csv") - build_graphs(results) - - -if __name__ == "__main__": - main() diff --git a/BolonkinNM/requirements.txt b/BolonkinNM/requirements.txt index a9006fd..6ccafc3 100644 --- a/BolonkinNM/requirements.txt +++ b/BolonkinNM/requirements.txt @@ -1 +1 @@ -matplotlib>=3.8 +matplotlib diff --git a/BolonkinNM/results.csv b/BolonkinNM/results.csv deleted file mode 100644 index cc67b71..0000000 --- a/BolonkinNM/results.csv +++ /dev/null @@ -1,109 +0,0 @@ -Структура,Режим,Операция,Замер,Время (сек) -LinkedList,случайный,insert,1,2.4210275000 -LinkedList,случайный,find,1,0.0214394000 -LinkedList,случайный,delete,1,0.0108667000 -LinkedList,случайный,insert,2,2.4208055000 -LinkedList,случайный,find,2,0.0216110000 -LinkedList,случайный,delete,2,0.0106216000 -LinkedList,случайный,insert,3,2.4210881000 -LinkedList,случайный,find,3,0.0216503000 -LinkedList,случайный,delete,3,0.0106497000 -LinkedList,случайный,insert,4,2.4530798000 -LinkedList,случайный,find,4,0.0222764000 -LinkedList,случайный,delete,4,0.0108350000 -LinkedList,случайный,insert,5,2.4567773000 -LinkedList,случайный,find,5,0.0219400000 -LinkedList,случайный,delete,5,0.0108697000 -LinkedList,случайный,insert,среднее,2.4345556400 -LinkedList,случайный,find,среднее,0.0217834200 -LinkedList,случайный,delete,среднее,0.0107685400 -HashTable,случайный,insert,1,0.1621210000 -HashTable,случайный,find,1,0.0011201000 -HashTable,случайный,delete,1,0.0005854000 -HashTable,случайный,insert,2,0.1732676000 -HashTable,случайный,find,2,0.0011247000 -HashTable,случайный,delete,2,0.0005818000 -HashTable,случайный,insert,3,0.1638609000 -HashTable,случайный,find,3,0.0011355000 -HashTable,случайный,delete,3,0.0005814000 -HashTable,случайный,insert,4,0.1642886000 -HashTable,случайный,find,4,0.0011268000 -HashTable,случайный,delete,4,0.0005785000 -HashTable,случайный,insert,5,0.1640916000 -HashTable,случайный,find,5,0.0011287000 -HashTable,случайный,delete,5,0.0005787000 -HashTable,случайный,insert,среднее,0.1655259400 -HashTable,случайный,find,среднее,0.0011271600 -HashTable,случайный,delete,среднее,0.0005811600 -BST,случайный,insert,1,0.0153754000 -BST,случайный,find,1,0.0001491000 -BST,случайный,delete,1,0.0000786000 -BST,случайный,insert,2,0.0155821000 -BST,случайный,find,2,0.0001453000 -BST,случайный,delete,2,0.0000724000 -BST,случайный,insert,3,0.0151360000 -BST,случайный,find,3,0.0001437000 -BST,случайный,delete,3,0.0000741000 -BST,случайный,insert,4,0.0153703000 -BST,случайный,find,4,0.0001425000 -BST,случайный,delete,4,0.0000715000 -BST,случайный,insert,5,0.0153753000 -BST,случайный,find,5,0.0001455000 -BST,случайный,delete,5,0.0000723000 -BST,случайный,insert,среднее,0.0153678200 -BST,случайный,find,среднее,0.0001452200 -BST,случайный,delete,среднее,0.0000737800 -LinkedList,отсортированный,insert,1,2.5884851000 -LinkedList,отсортированный,find,1,0.0227221000 -LinkedList,отсортированный,delete,1,0.0111309000 -LinkedList,отсортированный,insert,2,2.5095731000 -LinkedList,отсортированный,find,2,0.0217208000 -LinkedList,отсортированный,delete,2,0.0107773000 -LinkedList,отсортированный,insert,3,2.5642096000 -LinkedList,отсортированный,find,3,0.0228242000 -LinkedList,отсортированный,delete,3,0.0115945000 -LinkedList,отсортированный,insert,4,2.7163021000 -LinkedList,отсортированный,find,4,0.0431456000 -LinkedList,отсортированный,delete,4,0.0136020000 -LinkedList,отсортированный,insert,5,2.6891794000 -LinkedList,отсортированный,find,5,0.0217679000 -LinkedList,отсортированный,delete,5,0.0106384000 -LinkedList,отсортированный,insert,среднее,2.6135498600 -LinkedList,отсортированный,find,среднее,0.0264361200 -LinkedList,отсортированный,delete,среднее,0.0115486200 -HashTable,отсортированный,insert,1,0.1524640000 -HashTable,отсортированный,find,1,0.0014973000 -HashTable,отсортированный,delete,1,0.0006991000 -HashTable,отсортированный,insert,2,0.1537592000 -HashTable,отсортированный,find,2,0.0012225000 -HashTable,отсортированный,delete,2,0.0006561000 -HashTable,отсортированный,insert,3,0.1555816000 -HashTable,отсортированный,find,3,0.0012080000 -HashTable,отсортированный,delete,3,0.0006472000 -HashTable,отсортированный,insert,4,0.1546417000 -HashTable,отсортированный,find,4,0.0015017000 -HashTable,отсортированный,delete,4,0.0007512000 -HashTable,отсортированный,insert,5,0.1531659000 -HashTable,отсортированный,find,5,0.0012219000 -HashTable,отсортированный,delete,5,0.0006493000 -HashTable,отсортированный,insert,среднее,0.1539224800 -HashTable,отсортированный,find,среднее,0.0013302800 -HashTable,отсортированный,delete,среднее,0.0006805800 -BST,отсортированный,insert,1,4.5025059000 -BST,отсортированный,find,1,0.0387267000 -BST,отсортированный,delete,1,0.0162161000 -BST,отсортированный,insert,2,4.6704081000 -BST,отсортированный,find,2,0.0435012000 -BST,отсортированный,delete,2,0.0203211000 -BST,отсортированный,insert,3,6.2192950000 -BST,отсортированный,find,3,0.0578654000 -BST,отсортированный,delete,3,0.0327529000 -BST,отсортированный,insert,4,4.7844525000 -BST,отсортированный,find,4,0.0380228000 -BST,отсортированный,delete,4,0.0159740000 -BST,отсортированный,insert,5,4.4861403000 -BST,отсортированный,find,5,0.0382484000 -BST,отсортированный,delete,5,0.0159402000 -BST,отсортированный,insert,среднее,4.9325603600 -BST,отсортированный,find,среднее,0.0432729000 -BST,отсортированный,delete,среднее,0.0202408600 diff --git a/BolonkinNM/solver/__init__.py b/BolonkinNM/solver/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/solver/maze_solver.py b/BolonkinNM/solver/maze_solver.py new file mode 100644 index 0000000..7894661 --- /dev/null +++ b/BolonkinNM/solver/maze_solver.py @@ -0,0 +1,50 @@ +import time +from core.search_stats import SearchStats + + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def setStrategy(self, strategy): + self.strategy = strategy + + def addObserver(self, observer): + if observer not in self.observers: + self.observers.append(observer) + + def removeObserver(self, observer): + if observer in self.observers: + self.observers.remove(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + if self.strategy is None: + raise ValueError("Strategy is not set") + self.notify({"type": "search_started", "strategy": self.strategy.name}) + + start_time = time.perf_counter() + path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell) + end_time = time.perf_counter() + + stats = SearchStats( + timeMs=(end_time - start_time) * 1000.0, + visitedCells=getattr(self.strategy, "visitedCount", 0), + pathLength=len(path), + path=path, + found=bool(path), + algorithm=getattr(self.strategy, "name", "") + ) + + if stats.found: + self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength}) + else: + self.notify({"type": "path_not_found", "strategy": stats.algorithm}) + + self.notify({"type": "search_finished", "stats": stats}) + return stats diff --git a/BolonkinNM/strategies/__init__.py b/BolonkinNM/strategies/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BolonkinNM/strategies/astar_strategy.py b/BolonkinNM/strategies/astar_strategy.py new file mode 100644 index 0000000..4da5535 --- /dev/null +++ b/BolonkinNM/strategies/astar_strategy.py @@ -0,0 +1,45 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class AStarStrategy(PathFindingStrategy): + name = "A*" + + def heuristic(self, cell, exitCell): + return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y) + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + open_set = [] + heapq.heappush(open_set, (0, 0, start.x, start.y, start)) + parent = {} + g_score = {(start.x, start.y): 0} + closed = set() + + while open_set: + f_score, current_g, _, _, current = heapq.heappop(open_set) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + tentative_g = current_g + getattr(neighbor, "weight", 1) + + if tentative_g < g_score.get(npos, float("inf")): + g_score[npos] = tentative_g + parent[npos] = current + new_f = tentative_g + self.heuristic(neighbor, exitCell) + heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/BolonkinNM/strategies/bfs_strategy.py b/BolonkinNM/strategies/bfs_strategy.py new file mode 100644 index 0000000..7a98b50 --- /dev/null +++ b/BolonkinNM/strategies/bfs_strategy.py @@ -0,0 +1,31 @@ +from collections import deque +from strategies.pathfinding_strategy import PathFindingStrategy + + +class BFSStrategy(PathFindingStrategy): + name = "BFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + queue = deque([start]) + visited = {(start.x, start.y)} + parent = {} + + while queue: + current = queue.popleft() + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + pos = (neighbor.x, neighbor.y) + if pos not in visited: + visited.add(pos) + parent[pos] = current + queue.append(neighbor) + + return [] diff --git a/BolonkinNM/strategies/dfs_strategy.py b/BolonkinNM/strategies/dfs_strategy.py new file mode 100644 index 0000000..36451b3 --- /dev/null +++ b/BolonkinNM/strategies/dfs_strategy.py @@ -0,0 +1,35 @@ +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DFSStrategy(PathFindingStrategy): + name = "DFS" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + stack = [start] + visited = set() + parent = {} + + while stack: + current = stack.pop() + pos = (current.x, current.y) + if pos in visited: + continue + + visited.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + neighbors = maze.getNeighbors(current) + for neighbor in reversed(neighbors): + npos = (neighbor.x, neighbor.y) + if npos not in visited: + parent[npos] = current + stack.append(neighbor) + + return [] diff --git a/BolonkinNM/strategies/dijkstra_strategy.py b/BolonkinNM/strategies/dijkstra_strategy.py new file mode 100644 index 0000000..fd3163f --- /dev/null +++ b/BolonkinNM/strategies/dijkstra_strategy.py @@ -0,0 +1,41 @@ +import heapq +from strategies.pathfinding_strategy import PathFindingStrategy + + +class DijkstraStrategy(PathFindingStrategy): + name = "Dijkstra" + + def findPath(self, maze, start, exitCell): + self.visitedCount = 0 + if start is None or exitCell is None: + return [] + + pq = [(0, start.x, start.y, start)] + dist = {(start.x, start.y): 0} + parent = {} + closed = set() + + while pq: + current_cost, _, _, current = heapq.heappop(pq) + pos = (current.x, current.y) + + if pos in closed: + continue + + closed.add(pos) + self.visitedCount += 1 + + if current.x == exitCell.x and current.y == exitCell.y: + return self._restore_path(parent, start, exitCell) + + for neighbor in maze.getNeighbors(current): + npos = (neighbor.x, neighbor.y) + step_cost = getattr(neighbor, "weight", 1) + new_cost = current_cost + step_cost + + if new_cost < dist.get(npos, float("inf")): + dist[npos] = new_cost + parent[npos] = current + heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor)) + + return [] diff --git a/BolonkinNM/strategies/pathfinding_strategy.py b/BolonkinNM/strategies/pathfinding_strategy.py new file mode 100644 index 0000000..17b3ee4 --- /dev/null +++ b/BolonkinNM/strategies/pathfinding_strategy.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod + + +class PathFindingStrategy(ABC): + name = "Base" + + def __init__(self): + self.visitedCount = 0 + + @abstractmethod + def findPath(self, maze, start, exitCell): + raise NotImplementedError + + def _restore_path(self, parent, start, exitCell): + if exitCell is None or start is None: + return [] + + path = [] + current = exitCell + + while True: + path.append(current) + if current.x == start.x and current.y == start.y: + break + current = parent.get((current.x, current.y)) + if current is None: + return [] + + path.reverse() + return path diff --git a/BolonkinNM/utils.py b/BolonkinNM/utils.py deleted file mode 100644 index 0fcb993..0000000 --- a/BolonkinNM/utils.py +++ /dev/null @@ -1,35 +0,0 @@ -import random -from typing import List, Tuple - - -Record = Tuple[str, str] - - -def generate_records(n: int, repeat_names: bool = False, seed: int = 42) -> List[Record]: - rng = random.Random(seed) - records: List[Record] = [] - - if repeat_names: - name_pool = [ - "User_Alex", "User_Bob", "User_Cat", "User_Dan", "User_Eva", - "User_Fox", "User_Geo", "User_Hen", "User_Ira", "User_Leo", - ] - for _ in range(n): - name = rng.choice(name_pool) - phone = f"{rng.randint(1000000000, 9999999999)}" - records.append((name, phone)) - else: - for i in range(n): - name = f"User_{i:05d}" - phone = f"{1000000000 + i}" - records.append((name, phone)) - - return records - - -def prepare_records_variants(records: List[Record], seed: int = 42): - rng = random.Random(seed) - records_shuffled = list(records) - rng.shuffle(records_shuffled) - records_sorted = sorted(records, key=lambda x: x[0]) - return records_shuffled, records_sorted