Задание 2
3
BolonkinNM/.idea/.gitignore
vendored
|
|
@ -1,3 +0,0 @@
|
||||||
# Default ignored files
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (ds_project_archive)" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (maze_project_submission) (2)" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/ds_project_archive.iml" filepath="$PROJECT_DIR$/.idea/ds_project_archive.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/maze_project_submission.iml" filepath="$PROJECT_DIR$/.idea/maze_project_submission.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
42
BolonkinNM/.idea/workspace.xml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<list default="true" id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="MarkdownSettingsMigration">
|
||||||
|
<option name="stateVersion" value="1" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo"><![CDATA[{
|
||||||
|
"associatedIndex": 2
|
||||||
|
}]]></component>
|
||||||
|
<component name="ProjectId" id="3EB20Mq0B865MSq8Kkl2evaRIZW" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"last_opened_file_path": "C:/Users/vaz21/Downloads/Task 2 GLOBAL/maze_project_submission"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
|
||||||
|
<created>1779637417749</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1779637417749</updated>
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
|
|
@ -1,18 +1,24 @@
|
||||||
# Задание 1 — структуры данных
|
# Maze Solver Project
|
||||||
|
|
||||||
Процедурная реализация:
|
ООП-проект для поиска выхода из лабиринта с паттернами:
|
||||||
- linked_list.py
|
- Builder
|
||||||
- hash_table.py
|
- Strategy
|
||||||
- bst.py
|
- Observer
|
||||||
|
- Command
|
||||||
|
|
||||||
Эксперименты и отчёты:
|
## Запуск
|
||||||
- experiments.py
|
|
||||||
- plot_results.py
|
|
||||||
- results.csv
|
|
||||||
- docs/report.md
|
|
||||||
- docs/data/*.png
|
|
||||||
|
|
||||||
Запуск:
|
|
||||||
```bash
|
```bash
|
||||||
python main.py
|
python main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Эксперименты
|
||||||
|
```bash
|
||||||
|
python experiment.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результаты сохраняются в папку `experiment_results/`.
|
||||||
|
|
||||||
|
## Требования
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
0
BolonkinNM/builders/__init__.py
Normal file
7
BolonkinNM/builders/maze_builder.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def buildFromFile(self, filename):
|
||||||
|
raise NotImplementedError
|
||||||
52
BolonkinNM/builders/text_file_maze_builder.py
Normal file
|
|
@ -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)
|
||||||
0
BolonkinNM/commands/__init__.py
Normal file
11
BolonkinNM/commands/command.py
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self):
|
||||||
|
raise NotImplementedError
|
||||||
37
BolonkinNM/commands/move_command.py
Normal file
|
|
@ -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
|
||||||
0
BolonkinNM/controller/__init__.py
Normal file
30
BolonkinNM/controller/game_controller.py
Normal file
|
|
@ -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
|
||||||
0
BolonkinNM/core/__init__.py
Normal file
26
BolonkinNM/core/cell.py
Normal file
|
|
@ -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) + ")"
|
||||||
49
BolonkinNM/core/maze.py
Normal file
|
|
@ -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))
|
||||||
6
BolonkinNM/core/player.py
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
class Player:
|
||||||
|
def __init__(self, currentCell):
|
||||||
|
self.currentCell = currentCell
|
||||||
|
|
||||||
|
def setCell(self, cell):
|
||||||
|
self.currentCell = cell
|
||||||
11
BolonkinNM/core/search_stats.py
Normal file
|
|
@ -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 = ""
|
||||||
1
BolonkinNM/docs/README.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Place report files and experiment outputs here.
|
||||||
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 51 KiB |
|
Before Width: | Height: | Size: 48 KiB |
|
|
@ -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
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
<<interface>>
|
||||||
|
+buildFromFile(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextFileMazeBuilder {
|
||||||
|
+buildFromFile(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
class PathFindingStrategy {
|
||||||
|
<<interface>>
|
||||||
|
+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 {
|
||||||
|
<<interface>>
|
||||||
|
+update(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConsoleView {
|
||||||
|
+update(event)
|
||||||
|
+render(maze, player_position, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Command {
|
||||||
|
<<interface>>
|
||||||
|
+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
|
### BFSStrategy
|
||||||
{"name": "Имя", "phone": "123", "left": None, "right": None}
|
Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости.
|
||||||
```
|
|
||||||
|
|
||||||
Для BST использованы итеративные операции, чтобы корректно работать и на отсортированных данных.
|
### DFSStrategy
|
||||||
|
Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм.
|
||||||
|
|
||||||
## Методика эксперимента
|
### AStarStrategy
|
||||||
|
Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели.
|
||||||
|
|
||||||
- Количество записей: `N = 10000`
|
### MazeSolver
|
||||||
- Режимы данных:
|
Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику.
|
||||||
- случайный порядок;
|
|
||||||
- отсортированный порядок.
|
|
||||||
- Каждое измерение повторялось **5 раз**.
|
|
||||||
- В CSV сохранены:
|
|
||||||
- все отдельные замеры;
|
|
||||||
- среднее время для каждой операции, структуры и режима.
|
|
||||||
|
|
||||||
Операции:
|
### SearchStats
|
||||||
|
Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути.
|
||||||
|
|
||||||
- вставка всех записей;
|
### ConsoleView
|
||||||
- поиск 100 существующих и 10 отсутствующих имён;
|
Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль.
|
||||||
- удаление 50 случайных имён.
|
|
||||||
|
|
||||||
## Графики
|
### MoveCommand
|
||||||
|
Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения.
|
||||||
|
|
||||||

|
## 6. Экспериментальная часть
|
||||||
|
|
||||||

|
### 6.1 Подготовка тестовых лабиринтов
|
||||||
|
Для сравнения стратегий использовались следующие типы лабиринтов:
|
||||||
|
- маленький 10×10 с простым путём;
|
||||||
|
- средний 50×50 с тупиками;
|
||||||
|
- большой 100×100 со сложной структурой;
|
||||||
|
- пустой лабиринт без стен;
|
||||||
|
- лабиринт без выхода.
|
||||||
|
|
||||||

|
### 6.2 Методика измерений
|
||||||
|
Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения:
|
||||||
|
- время поиска в миллисекундах;
|
||||||
|
- количество посещённых клеток;
|
||||||
|
- длина найденного пути.
|
||||||
|
|
||||||
## Средние результаты
|
Результаты сохранялись в CSV-файл в двух вариантах:
|
||||||
|
- сырой набор измерений;
|
||||||
|
- усреднённая таблица.
|
||||||
|
|
||||||
| Режим | Операция | LinkedList | HashTable | BST | Лучший результат |
|
## 7. Анализ эффективности
|
||||||
|---|---:|---:|---:|---:|---|
|
|
||||||
| случайный | 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 |
|
|
||||||
|
|
||||||
## Анализ результатов
|
### BFS
|
||||||
|
BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах.
|
||||||
|
|
||||||
### Влияние порядка входных данных на BST
|
### DFS
|
||||||
|
DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области.
|
||||||
|
|
||||||
На случайных данных BST работает значительно быстрее, чем на отсортированных. Это связано с тем, что при случайной вставке дерево остаётся ближе к сбалансированному состоянию.
|
### A*
|
||||||
|
A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода.
|
||||||
|
|
||||||
На отсортированных данных дерево вырождается в цепочку, поэтому вставка становится медленной, а поиск и удаление тоже деградируют по времени.
|
### Лабиринт без пути
|
||||||
|
Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения.
|
||||||
|
|
||||||
### Почему хеш-таблица почти не чувствительна к порядку
|
### Вывод по выбору алгоритма
|
||||||
|
- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы.
|
||||||
|
- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности.
|
||||||
|
- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток.
|
||||||
|
- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*.
|
||||||
|
|
||||||
Хеш-таблица распределяет элементы по бакетам через хеш-функцию, поэтому сам порядок входа почти не влияет на скорость. Влияние может появляться только из-за коллизий, но в целом поведение остаётся близким к постоянному времени.
|
## 8. Роль ООП и паттернов
|
||||||
|
ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому:
|
||||||
|
- можно заменить алгоритм поиска без переписывания логики программы;
|
||||||
|
- можно добавить новый формат загрузки лабиринта;
|
||||||
|
- можно поменять способ визуализации;
|
||||||
|
- можно расширить управление игроком и добавить отмену действий.
|
||||||
|
|
||||||
### Почему связный список всегда медленен при поиске
|
Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения.
|
||||||
|
|
||||||
Поиск в связном списке выполняется последовательным просмотром элементов. Поэтому при большом количестве записей приходится проходить много узлов, и операция остаётся линейной по времени.
|
## 9. Вывод
|
||||||
|
В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах.
|
||||||
|
|
||||||
### Как удаление работает в каждой структуре
|
## 10. Приложения
|
||||||
|
- Листинги ключевых классов.
|
||||||
- В связном списке нужно сначала найти нужный узел, затем переназначить ссылку.
|
- CSV-файлы с результатами экспериментов.
|
||||||
- В хеш-таблице сначала выбирается бакет, затем удаление выполняется внутри короткой цепочки.
|
- Графики сравнений.
|
||||||
- В BST удаление зависит от числа потомков: если потомок один или ноль, операция простая; если два — нужно найти преемника.
|
- Файлы с тестовыми лабиринтами.
|
||||||
|
|
||||||
## Вывод
|
|
||||||
|
|
||||||
Для частых вставок и особенно частого поиска в реальной задаче чаще всего лучше подходит **хеш-таблица**.
|
|
||||||
|
|
||||||
Если важно получать данные в отсортированном виде, удобнее использовать **BST**.
|
|
||||||
|
|
||||||
**Связный список** подходит для маленьких объёмов данных или очень простых сценариев, но при большом числе записей он проигрывает по скорости поиска.
|
|
||||||
|
|
|
||||||
225
BolonkinNM/experiment.py
Normal file
|
|
@ -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()
|
||||||
BIN
BolonkinNM/experiment_results/empty_30x30_length.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
BolonkinNM/experiment_results/empty_30x30_time.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
BolonkinNM/experiment_results/empty_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
BolonkinNM/experiment_results/large_100x100_length.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
BolonkinNM/experiment_results/large_100x100_time.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
BolonkinNM/experiment_results/large_100x100_visited.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
BolonkinNM/experiment_results/medium_50x50_length.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
BolonkinNM/experiment_results/medium_50x50_time.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
BolonkinNM/experiment_results/medium_50x50_visited.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
BolonkinNM/experiment_results/no_path_30x30_length.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
BolonkinNM/experiment_results/no_path_30x30_time.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
BolonkinNM/experiment_results/no_path_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
121
BolonkinNM/experiment_results/raw.csv
Normal file
|
|
@ -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
|
||||||
|
BIN
BolonkinNM/experiment_results/small_10x10_length.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
BolonkinNM/experiment_results/small_10x10_time.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
BolonkinNM/experiment_results/small_10x10_visited.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
25
BolonkinNM/experiment_results/summary.csv
Normal file
|
|
@ -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
|
||||||
|
BIN
BolonkinNM/experiment_results/weighted_30x30_length.png
Normal file
|
After Width: | Height: | Size: 21 KiB |
BIN
BolonkinNM/experiment_results/weighted_30x30_time.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
BolonkinNM/experiment_results/weighted_30x30_visited.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
|
|
@ -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()
|
|
||||||
|
|
@ -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"])
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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 pathlib import Path
|
||||||
|
|
||||||
from experiments import run_experiments, save_results_csv
|
BASE_DIR = Path(__file__).resolve().parent
|
||||||
from plot_results import build_graphs, load_average_results
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def run_demo():
|
||||||
rows = run_experiments(n=10000, buckets_count=2048, repeats=5)
|
builder = TextFileMazeBuilder()
|
||||||
save_results_csv(rows, "results.csv")
|
maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt"))
|
||||||
averaged = load_average_results("results.csv")
|
|
||||||
build_graphs(averaged, output_dir="docs/data")
|
view = ConsoleView()
|
||||||
print("Done.")
|
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
run_demo()
|
||||||
|
|
|
||||||
9
BolonkinNM/mazes/maze_empty.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
S
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
E
|
||||||
11
BolonkinNM/mazes/maze_large.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
####################################################################################################
|
||||||
|
#S # # # # # # # # # # # # # # # E#
|
||||||
|
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
|
||||||
|
# # # # # # # # # # # # # # # # # # # # #
|
||||||
|
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
|
||||||
|
# # # # # # # # # # # # # # # # # # # # # #
|
||||||
|
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
|
||||||
|
# # # # # # # # #
|
||||||
|
####################################################################################################
|
||||||
11
BolonkinNM/mazes/maze_medium.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
##################################################
|
||||||
|
#S # # # # # # E#
|
||||||
|
# # ### ### # ###### # ### # ## # #### # ####### ##
|
||||||
|
# # # # # # # # # # # # # #
|
||||||
|
# ##### # ######## # ### # ## # #### # ####### ## #
|
||||||
|
# # # # # # # # # #
|
||||||
|
### # # ###### # ########### ########### ### ######
|
||||||
|
# # # # # # # # # # #
|
||||||
|
# ### ###### # ##### # ### # ####### # ### ### ## #
|
||||||
|
# # # # #
|
||||||
|
##################################################
|
||||||
9
BolonkinNM/mazes/maze_no_path.txt
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
##########
|
||||||
|
#S #
|
||||||
|
# ###### #
|
||||||
|
# # #
|
||||||
|
##########
|
||||||
|
# #E#
|
||||||
|
# ###### #
|
||||||
|
# #
|
||||||
|
##########
|
||||||
7
BolonkinNM/mazes/maze_small.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
##########
|
||||||
|
#S #E#
|
||||||
|
# ## # # ##
|
||||||
|
# # #
|
||||||
|
# #### # #
|
||||||
|
# # #
|
||||||
|
##########
|
||||||
10
BolonkinNM/mazes/maze_weighted.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
1111111111111111111111111111
|
||||||
|
1S11111111111111111111111111
|
||||||
|
1111111111111111111111111111
|
||||||
|
1111111111111111111111111111
|
||||||
|
1111111111111222222222222111
|
||||||
|
1111111111111222222222222111
|
||||||
|
1111111111111333333333333111
|
||||||
|
1111111111111333333333333111
|
||||||
|
111111111111111111111111111E
|
||||||
|
1111111111111111111111111111
|
||||||
0
BolonkinNM/observer/__init__.py
Normal file
26
BolonkinNM/observer/console_view.py
Normal file
|
|
@ -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()
|
||||||
7
BolonkinNM/observer/observer.py
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
|
||||||
|
class Observer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
matplotlib>=3.8
|
matplotlib
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
0
BolonkinNM/solver/__init__.py
Normal file
50
BolonkinNM/solver/maze_solver.py
Normal file
|
|
@ -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
|
||||||
0
BolonkinNM/strategies/__init__.py
Normal file
45
BolonkinNM/strategies/astar_strategy.py
Normal file
|
|
@ -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 []
|
||||||
31
BolonkinNM/strategies/bfs_strategy.py
Normal file
|
|
@ -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 []
|
||||||
35
BolonkinNM/strategies/dfs_strategy.py
Normal file
|
|
@ -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 []
|
||||||
41
BolonkinNM/strategies/dijkstra_strategy.py
Normal file
|
|
@ -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 []
|
||||||
30
BolonkinNM/strategies/pathfinding_strategy.py
Normal file
|
|
@ -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
|
||||||
|
|
@ -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
|
|
||||||