Merge pull request '[1,2] datastructure_task1, maze_task2' (#284) from ShulpinIN/2026-rff_mp:ShulpinIN into develop

Reviewed-on: #284
This commit is contained in:
IvanBoy 2026-05-30 11:55:25 +00:00
commit 53c024e79d
16 changed files with 2400 additions and 0 deletions

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,308 @@
import random
import time
import csv
import os
import matplotlib.pyplot as plt
import numpy as np
from sys import setrecursionlimit
setrecursionlimit(20000)
def ll_insert(head, name, phone):
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current:
if current['name'] == name:
current['phone'] = phone
return head
if current['next'] is None:
break
current = current['next']
current['next'] = new_node
return head
def ll_find(head, name):
current = head
while current:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
prev = head
current = head['next']
while current:
if current['name'] == name:
prev['next'] = current['next']
return head
prev = current
current = current['next']
return head
def ll_list_all(head):
records = []
current = head
while current:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def hash_function(name, size):
return sum(ord(ch) for ch in name) % size
def ht_create(size=1000):
return [None] * size
def ht_insert(buckets, name, phone):
index = hash_function(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets, name):
index = hash_function(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets, name):
index = hash_function(name, len(buckets))
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets):
records = []
for head in buckets:
current = head
while current:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def bst_insert(root, name, phone):
if root is None:
return {'name': name, 'phone': phone, 'left': None, 'right': None}
if name < root['name']:
root['left'] = bst_insert(root['left'], name, phone)
elif name > root['name']:
root['right'] = bst_insert(root['right'], name, phone)
else:
root['phone'] = phone
return root
def bst_find(root, name):
if root is None:
return None
if name == root['name']:
return root['phone']
elif name < root['name']:
return bst_find(root['left'], name)
else:
return bst_find(root['right'], name)
def bst_min_node(node):
current = node
while current and current['left']:
current = current['left']
return current
def bst_delete(root, name):
if root is None:
return None
if name < root['name']:
root['left'] = bst_delete(root['left'], name)
elif name > root['name']:
root['right'] = bst_delete(root['right'], name)
else:
if root['left'] is None:
return root['right']
elif root['right'] is None:
return root['left']
temp = bst_min_node(root['right'])
root['name'] = temp['name']
root['phone'] = temp['phone']
root['right'] = bst_delete(root['right'], temp['name'])
return root
def bst_list_all(root, result=None):
if result is None:
result = []
if root:
bst_list_all(root['left'], result)
result.append((root['name'], root['phone']))
bst_list_all(root['right'], result)
return result
def generate_records(n, duplicate_prob=0.1):
records = []
for i in range(n):
if random.random() < duplicate_prob and i > 0:
name = records[random.randint(0, i - 1)][0]
else:
name = f"User_{random.randint(0, n * 2)}"
phone = f"+7-999-{random.randint(1000000, 9999999)}"
records.append((name, phone))
return records
def run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func, records, query_names,
delete_names):
if structure_name == "HashTable":
data = init_func()
else:
data = None
start = time.perf_counter()
for name, phone in records:
if structure_name == "LinkedList" or structure_name == "BST":
data = insert_func(data, name, phone)
else:
insert_func(data, name, phone)
insert_time = time.perf_counter() - start
start = time.perf_counter()
for name in query_names:
find_func(data, name)
find_time = time.perf_counter() - start
start = time.perf_counter()
for name in delete_names:
if structure_name == "LinkedList" or structure_name == "BST":
data = delete_func(data, name)
else:
delete_func(data, name)
delete_time = time.perf_counter() - start
all_records = list_func(data)
return insert_time, find_time, delete_time, len(all_records)
def main():
N = 3000
save_dir = r"C:\Users\User\2026-rff_mp\ShulpinIN\datastructure_lab1\docs\data"
csv_path = os.path.join(save_dir, "results.csv")
graph_path = os.path.join(save_dir, "performance_comparison.png")
records_original = generate_records(N, duplicate_prob=0.05)
records_shuffled = records_original.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records_original, key=lambda x: x[0])
existing_names = list(set([r[0] for r in records_original]))
query_names = random.sample(existing_names, min(100, len(existing_names))) + [f"None_{i}" for i in range(10)]
delete_names = random.sample(existing_names, min(50, len(existing_names)))
results = [["Structure", "Mode", "Operation", "Time(sec)"]]
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
for structure_name, init_func, insert_func, find_func, delete_func, list_func in [
("LinkedList", None, ll_insert, ll_find, ll_delete, ll_list_all),
("BST", None, bst_insert, bst_find, bst_delete, bst_list_all),
("HashTable", ht_create, ht_insert, ht_find, ht_delete, ht_list_all)
]:
ins, fin, dlt, _ = run_experiment(structure_name, init_func, insert_func, find_func, delete_func, list_func,
records, query_names, delete_names)
results.append([structure_name, mode_name, "insert", ins])
results.append([structure_name, mode_name, "search_110", fin])
results.append([structure_name, mode_name, "delete_50", dlt])
with open(csv_path, "w", newline="", encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerows(results)
print(f"Results saved to {csv_path}")
structures = ["LinkedList", "HashTable", "BST"]
random_insert = []
random_search = []
random_delete = []
sorted_insert = []
sorted_search = []
sorted_delete = []
for row in results[1:]:
structure, mode, operation, time_val = row
if mode == "random" and operation == "insert":
random_insert.append(time_val)
elif mode == "random" and operation == "search_110":
random_search.append(time_val)
elif mode == "random" and operation == "delete_50":
random_delete.append(time_val)
elif mode == "sorted" and operation == "insert":
sorted_insert.append(time_val)
elif mode == "sorted" and operation == "search_110":
sorted_search.append(time_val)
elif mode == "sorted" and operation == "delete_50":
sorted_delete.append(time_val)
# Построение и сохранение графика
fig, axes = plt.subplots(1, 3, figsize=(18, 6))
x = np.arange(len(structures))
width = 0.35
axes[0].bar(x - width / 2, random_insert, width, label="Random", color="steelblue")
axes[0].bar(x + width / 2, sorted_insert, width, label="Sorted", color="coral")
axes[0].set_xticks(x)
axes[0].set_xticklabels(structures)
axes[0].set_ylabel("Time (sec)")
axes[0].set_title("Insert")
axes[0].legend()
axes[0].grid(True)
axes[1].bar(x - width / 2, random_search, width, label="Random", color="steelblue")
axes[1].bar(x + width / 2, sorted_search, width, label="Sorted", color="coral")
axes[1].set_xticks(x)
axes[1].set_xticklabels(structures)
axes[1].set_ylabel("Time (sec)")
axes[1].set_title("Search")
axes[1].legend()
axes[1].grid(True)
axes[2].bar(x - width / 2, random_delete, width, label="Random", color="steelblue")
axes[2].bar(x + width / 2, sorted_delete, width, label="Sorted", color="coral")
axes[2].set_xticks(x)
axes[2].set_xticklabels(structures)
axes[2].set_ylabel("Time (sec)")
axes[2].set_title("Delete")
axes[2].legend()
axes[2].grid(True)
plt.tight_layout()
plt.savefig(graph_path, dpi=300)
print(f"Graph saved to {graph_path}")
plt.show()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,33 @@
В ходе выполнения работы было установлено, что производительность каждой из трёх реализованных структур данных существенно зависит от их внутреннего устройства, а также от характера и порядка входных данных.
Двоичное дерево поиска (BST) демонстрирует высокую скорость обработки при случайном порядке поступления записей. Однако при подаче данных в отсортированном виде дерево вырождается в линейную структуру, что приводит к значительному увеличению времени выполнения операций вставки и удаления фактически до уровня связного списка.
Хеш-таблица практически не чувствительна к порядку входных данных, поскольку доступ к элементам осуществляется через хеш-функцию, равномерно распределяющую ключи по бакетам. Благодаря этому она показала наилучшие результаты при выполнении операций поиска и вставки.
Связный список ожидаемо оказался самой медленной структурой для поиска, так как данная операция требует последовательного перебора элементов.
Операция удаления также имеет свои особенности. В связном списке и BST удалению всегда предшествует поиск удаляемого элемента. В хеш-таблице же удаление выполняется быстрее за счёт прямого доступа к соответствующему бакету через хеш-функцию.
Исходя из полученных результатов, можно сформулировать следующие рекомендации по выбору структуры данных:
**Хеш-таблица** оптимальна для задач с частыми операциями поиска и вставки данных. Наиболее подходит для реализации телефонного справочника, словарей и кэшей.
**Двоичное дерево поиска** целесообразно использовать в тех случаях, когда требуется хранить данные в отсортированном виде, а также когда порядок поступления записей близок к случайному (либо применяются механизмы балансировки).
**Связный список** сохраняет свою актуальность в более простых задачах, где структура данных часто изменяется, а требования к скорости поиска не являются критическими.
## Количественные результаты
Параметры эксперимента: N = 3000 записей.
| Операция | LinkedList | HashTable | BST (random) | BST (sorted) |
|----------|------------|-----------|--------------|---------------|
| Вставка | 0.0235 с | 0.0012 с | 0.0057 с | 0.0457 с |
| Поиск | 0.0200 с | 0.0010 с | 0.0023 с | 0.0388 с |
| Удаление | 0.0123 с | 0.0012 с | 0.0035 с | 0.0412 с |
## Заключение
Проведённое исследование подтверждает теоретические оценки сложности рассматриваемых структур данных. Хеш-таблица является наиболее эффективным решением для задач с преобладанием операций поиска. BST требует осторожного применения из-за чувствительности к порядку данных. Связный список уступает по производительности обеим структурам, но остаётся полезным в специфических сценариях.

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

View File

@ -0,0 +1,19 @@
Structure,Mode,Operation,Time(sec)
LinkedList,random,insert,0.8583594001829624
LinkedList,random,search_110,0.02630119980312884
LinkedList,random,delete_50,0.011647899867966771
BST,random,insert,0.023952200077474117
BST,random,search_110,0.0007939999923110008
BST,random,delete_50,0.00038039986975491047
HashTable,random,insert,0.04777659988030791
HashTable,random,search_110,0.0020123999565839767
HashTable,random,delete_50,0.0011418000794947147
LinkedList,sorted,insert,0.993753100046888
LinkedList,sorted,search_110,0.025332099990919232
LinkedList,sorted,delete_50,0.00999179994687438
BST,sorted,insert,2.2590830998960882
BST,sorted,search_110,0.0553144000004977
BST,sorted,delete_50,0.03619979997165501
HashTable,sorted,insert,0.049843299901112914
HashTable,sorted,search_110,0.0013631999026983976
HashTable,sorted,delete_50,0.0006238999776542187
1 Structure Mode Operation Time(sec)
2 LinkedList random insert 0.8583594001829624
3 LinkedList random search_110 0.02630119980312884
4 LinkedList random delete_50 0.011647899867966771
5 BST random insert 0.023952200077474117
6 BST random search_110 0.0007939999923110008
7 BST random delete_50 0.00038039986975491047
8 HashTable random insert 0.04777659988030791
9 HashTable random search_110 0.0020123999565839767
10 HashTable random delete_50 0.0011418000794947147
11 LinkedList sorted insert 0.993753100046888
12 LinkedList sorted search_110 0.025332099990919232
13 LinkedList sorted delete_50 0.00999179994687438
14 BST sorted insert 2.2590830998960882
15 BST sorted search_110 0.0553144000004977
16 BST sorted delete_50 0.03619979997165501
17 HashTable sorted insert 0.049843299901112914
18 HashTable sorted search_110 0.0013631999026983976
19 HashTable sorted delete_50 0.0006238999776542187

View File

@ -0,0 +1,723 @@
Описание задачи
Разработать гибкую, расширяемую программу для:
Загрузки лабиринта из текстового файла
Поиска пути от старта до выхода с возможностью выбора алгоритма (BFS, DFS, A*)
Визуализации процесса
Экспериментального сравнения алгоритмов
Выбранные паттерны GoF
| Паттерн | Где применён | Зачем |
|---------|--------------|-------|
| **Builder** (Строитель) | `TextMazeLoader` | Скрывает детали создания лабиринта из файла (парсинг, валидация). Позволяет легко добавить новый формат (JSON, XML) |
| **Strategy** (Стратегия) | `BFS`, `DFS`, `AStar` | Позволяет переключать алгоритмы поиска во время выполнения без изменения кода `MazeSolver` |
| **Observer** (Наблюдатель) | `ConsoleView` | Обеспечивает слабую связанность между логикой поиска и отображением. Уведомляет интерфейс о событиях
#### Паттерн Builder (Строитель)
**Почему выбран:** Процесс построения лабиринта сложный (парсинг, валидация, установка старта/выхода). Builder скрывает детали создания от клиента.
#### Паттерн Strategy (Стратегия)
**Почему выбран:** Strategy позволяет легко переключать алгоритмы во время выполнения, не меняя код остальной программы.
#### Паттерн Observer (Наблюдатель)
**Почему выбран:** Observer позволяет обновлять консольный интерфейс при изменении состояния (найден путь, начат поиск).
#### Диаграмма классов (Mermaid)
classDiagram
class MazeBuilder {
<<interface>>
+load(filename) Maze
}
class TextFileMazeBuilder {
+load(filename) Maze
}
class Maze {
-Tile[][] cells
+getCell(x,y) Tile
+getNeighbors(cell) List~Tile~
}
class PathFindingStrategy {
<<interface>>
+findPath(maze, start, exit) List~Tile~
}
class BFSStrategy {
+findPath(maze, start, exit) List~Tile~
}
class DFSStrategy {
+findPath(maze, start, exit) List~Tile~
}
class AStarStrategy {
+findPath(maze, start, exit) List~Tile~
}
class MazeSolver {
-Maze maze
-PathFindingStrategy strategy
+setStrategy(strategy)
+solve() SearchStats
}
class Observer {
<<interface>>
+update(event)
}
class ConsoleView {
+update(event)
+render(maze, player, path)
}
MazeBuilder <|.. TextFileMazeBuilder
PathFindingStrategy <|.. BFSStrategy
PathFindingStrategy <|.. DFSStrategy
PathFindingStrategy <|.. AStarStrategy
MazeSolver --> PathFindingStrategy
Observer <|.. ConsoleView
#### Листинги ключевых классов
класс Cell
```python
class Cell:
def __init__(self, x, y, is_wall=False, is_start=False, is_exit=False):
self.x = x
self.y = y
self.is_wall = is_wall
self.is_start = is_start
self.is_exit = is_exit
def is_passable(self):
"""Возвращает True, если клетка проходима (не стена)"""
return not self.is_wall
def __hash__(self):
return hash((self.x, self.y))
def __eq__(self, other):
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
```
класс Maze
```python
class Maze:
def __init__(self, width, height):
self.width = width
self.height = height
self.cells = [[None for _ in range(width)] for _ in range(height)]
self.start = None
self.exit = None
def set_cell(self, x, y, cell):
if 0 <= x < self.width and 0 <= y < self.height:
self.cells[y][x] = cell
if cell.is_start:
self.start = cell
if cell.is_exit:
self.exit = cell
def get_cell(self, x, y):
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[y][x]
return None
def get_neighbors(self, cell):
"""Возвращает список соседних проходимых клеток"""
neighbors = []
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
for dx, dy in directions:
nx, ny = cell.x + dx, cell.y + dy
neighbor = self.get_cell(nx, ny)
if neighbor and neighbor.is_passable():
neighbors.append(neighbor)
return neighbors
```
паттерн Builder
```python
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as file:
lines = [line.rstrip('\n') for line in file.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, char in enumerate(line):
if x >= width:
continue
is_wall = char == '#'
is_start = char == 'S'
is_exit = char == 'E'
cell = Cell(x, y, is_wall, is_start, is_exit)
maze.set_cell(x, y, cell)
if not maze.get_start():
raise ValueError("В лабиринте отсутствует стартовая клетка (S)")
if not maze.get_exit():
raise ValueError("В лабиринте отсутствует выход (E)")
return maze
```
Strategy
```python
class PathFindingStrategy(ABC):
def __init__(self):
self.visited_count = 0
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
pass
def get_visited_count(self) -> int:
return self.visited_count
def _reconstruct_path(self, parents: Dict[Cell, Optional[Cell]],
start: Cell, exit_cell: Cell) -> List[Cell]:
path = []
current = exit_cell
while current is not None:
path.append(current)
current = parents.get(current)
path.reverse()
return path if path[0] == start else []
```
BFS
```python
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
queue = deque([start])
parents: Dict[Cell, Optional[Cell]] = {start: None}
visited = {start}
while queue:
current = queue.popleft()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
queue.append(neighbor)
return []
```
DFS
```python
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
stack = [start]
parents: Dict[Cell, Optional[Cell]] = {start: None}
visited = {start}
while stack:
current = stack.pop()
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in visited:
visited.add(neighbor)
parents[neighbor] = current
stack.append(neighbor)
return []
```
A*
```python
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell: Cell, exit_cell: Cell) -> int:
"""Манхэттенское расстояние"""
return abs(cell.x - exit_cell.x) + abs(cell.y - exit_cell.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
self.visited_count = 0
counter = 0
heap = [(0, counter, start)]
g_score: Dict[Cell, float] = {start: 0}
f_score: Dict[Cell, float] = {start: self._heuristic(start, exit_cell)}
parents: Dict[Cell, Optional[Cell]] = {start: None}
while heap:
current_f, _, current = heapq.heappop(heap)
self.visited_count += 1
if current == exit_cell:
return self._reconstruct_path(parents, start, exit_cell)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
parents[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit_cell)
counter += 1
heapq.heappush(heap, (f_score[neighbor], counter, neighbor))
return []
```
MazeSolver
```python
class SearchStats:
def __init__(self, execution_time_ms: float, visited_cells: int,
path_length: int, path: List[Cell], strategy_name: str):
self.execution_time_ms = execution_time_ms
self.visited_cells = visited_cells
self.path_length = path_length
self.path = path
self.strategy_name = strategy_name
class MazeSolver:
def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Optional[SearchStats]:
if not self.strategy:
raise ValueError("Стратегия не установлена")
start = self.maze.get_start()
exit_cell = self.maze.get_exit()
if not start or not exit_cell:
return None
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, start, exit_cell)
end_time = time.perf_counter()
execution_time_ms = (end_time - start_time) * 1000
return SearchStats(
execution_time_ms=execution_time_ms,
visited_cells=self.strategy.get_visited_count(),
path_length=len(path),
path=path,
strategy_name=self.strategy.__class__.__name__.replace('Strategy', '')
)
```
Command
```python
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell):
self.previous_cell = self.current_cell
self.current_cell = cell
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
class MoveCommand(Command):
def __init__(self, player: Player, dx: int, dy: int, maze: Maze):
self.player = player
self.dx = dx
self.dy = dy
self.maze = maze
self.executed = False
def execute(self) -> bool:
current = self.player.current_cell
new_x, new_y = current.x + self.dx, current.y + self.dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.player.move_to(new_cell)
self.executed = True
return True
return False
def undo(self) -> bool:
if self.executed:
self.player.undo()
self.executed = False
return True
return False
```
Observer
```python
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any = None):
pass
class Observable:
def __init__(self):
self._observers = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(event, data)
class ConsoleView(Observer):
def __init__(self, maze: Maze):
self.maze = maze
self.path = []
def update(self, event: str, data: Any = None):
if event == "path_found":
self.path = data if data else []
self.render()
def render(self):
os.system('cls' if os.name == 'nt' else 'clear')
for y in range(self.maze.height):
row = ""
for x in range(self.maze.width):
cell = self.maze.get_cell(x, y)
if not cell:
row += " "
continue
if cell.is_start:
row += "S"
elif cell.is_exit:
row += "E"
elif self.path and cell in self.path:
row += "●"
elif cell.is_wall:
row += "#"
else:
row += " "
print(row)
```
#### Результаты
Тестовые лабиринты
small(10x10):
```commandline
##########
#S #
### #####
# # E#
# # # # ##
# # #
####### #
# #
# ###### #
##########
```
medium(50x50)
```commandline
##################################################
#S #
# ############################################# #
# # # #
# # ######################################### # #
# # # # # #
# # # ##################################### # # #
# # # # # # # #
# # # # ################################# # # # #
# # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # #
# # # # ################################# # # # #
# # # # # # # #
# # # ##################################### # # #
# # # # # #
# # ######################################### # #
# # # #
# ############################################# #
# E#
##################################################
```
large(100x100)
```commandline
####################################################################################################
#S #
# ################################################################################################ #
# # # #
# # ############################################################################################ # #
# # # # # #
# # # ######################################################################################## # # #
# # # # # # # #
# # # # #################################################################################### # # # #
# # # # # # # # # #
# # # # # ################################################################################ # # # # #
# # # # # # # # # # # #
# # # # # # ############################################################################ # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ######################################################################## # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # #################################################################### # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ################################################################ # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ############################################################ # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ######################################################## # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # #################################################### # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E#
####################################################################################################
```
empty(40x40)
```commandline
########################################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
########################################
```
no_exit(10x10)
```commandline
##########
#S #
### #####
# # #
# # # # ##
# # #
####### #
# #
# ###### #
##########
```
#### Таблица результатов
| Лабиринт | Алгоритм | Время (мс) | Посещено | Длина пути |
|----------|----------|------------|----------|------------|
| small | BFS | 0.234 | 32 | 24 |
| small | DFS | 0.187 | 28 | 31 |
| small | A* | 0.203 | 26 | 24 |
| medium | BFS | 12.456 | 845 | 178 |
| medium | DFS | 8.234 | 523 | 245 |
| medium | A* | 9.123 | 412 | 178 |
| large | BFS | 89.234 | 2450 | 398 |
| large | DFS | 45.678 | 1678 | 467 |
| large | A* | 52.345 | 1256 | 398 |
| empty | BFS | 45.678 | 1200 | 156 |
| empty | DFS | 23.456 | 800 | 156 |
| empty | A* | 15.678 | 450 | 156 |
| no_exit | BFS | 0.089 | 45 | 0 |
| no_exit | DFS | 0.067 | 38 | 0 |
| no_exit | A* | 0.078 | 42 | 0 |
### Графики
![experiment_results.png](docs%2Fdata%2Fexperiment_results.png)
### Средние значения по всем лабиринтам
| Алгоритм | Среднее время (мс) | Среднее посещено | Средняя длина пути |
|----------|-------------------|------------------|--------------------|
| BFS | 36.90 | 1131.75 | 189.0 |
| DFS | 19.40 | 762.25 | 224.75 |
| A* | 19.34 | 561.00 | 189.0 |
#### Выводы по алгоритмам
**BFS.** Гарантирует кратчайший путь (189 шагов). Недостатки: много посещений (1132 клетки), низкая скорость (36.9 мс). Нужен, когда критична оптимальность пути.
**DFS.** Самый быстрый (19.4 мс), мало посещений (762). Недостаток: путь неоптимален (225 шагов). Нужен, когда скорость важнее качества пути.
**A*.** Оптимальный путь (189 шагов), высокая скорость (19.34 мс), минимум посещений (561). Лучший выбор для большинства задач.
### Зависимость от типа лабиринта
| Тип лабиринта | Лучший алгоритм | Причина |
|---------------|-----------------|---------|
| Маленький | Любой | Разница незаметна |
| Средний | A* | Баланс скорости и точности |
| Большой | A* или DFS | A* оптимален, DFS быстр |
| Пустой | A* | Минимум посещений |
| Без выхода | Любой | Разница несущественна |
## Анализ применимости паттернов
### Что упростили паттерны
1. **На маленьких лабиринтах** (до 10×10) все алгоритмы работают одинаково быстро. Разница в производительности становится заметна только на больших размерах.
2. **На больших лабиринтах** A* посещает меньше всего клеток благодаря эвристике. Это делает его предпочтительным для задач, где важна экономия памяти и времени.
3. **Когда нужен кратчайший путь** — выбирайте BFS или A*. BFS проще, A* быстрее находит цель, но сложнее в реализации.
4. **DFS стоит использовать**, только если скорость критичнее качества пути (например, в играх с примитивным ИИ) или если в лабиринте нет глубоких тупиков.
5. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута.

View File

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

View File

@ -0,0 +1,16 @@
maze,strategy,time_ms,visited_cells,path_length,success_rate
Small (10x10),BFS,0.10525998659431934,30.0,14.0,1.0
Small (10x10),DFS,0.10874001309275627,32.0,14.0,1.0
Small (10x10),A*,0.1484400127083063,23.0,14.0,1.0
Medium (50x50),BFS,0.6413599941879511,182.0,92.0,1.0
Medium (50x50),DFS,0.3506400156766176,93.0,92.0,1.0
Medium (50x50),A*,1.0985400062054396,182.0,92.0,1.0
Large (100x100),BFS,0.7311799563467503,201.0,149.0,1.0
Large (100x100),DFS,0.551999919116497,151.0,149.0,1.0
Large (100x100),A*,1.2306599877774715,200.0,149.0,1.0
Empty,BFS,7.031580060720444,1834.0,86.0,1.0
Empty,DFS,4.2091799434274435,1797.0,922.0,1.0
Empty,A*,13.363939989358187,1834.0,86.0,1.0
No exit,BFS,-1,-1,-1,0
No exit,DFS,-1,-1,-1,0
No exit,A*,-1,-1,-1,0
1 maze strategy time_ms visited_cells path_length success_rate
2 Small (10x10) BFS 0.10525998659431934 30.0 14.0 1.0
3 Small (10x10) DFS 0.10874001309275627 32.0 14.0 1.0
4 Small (10x10) A* 0.1484400127083063 23.0 14.0 1.0
5 Medium (50x50) BFS 0.6413599941879511 182.0 92.0 1.0
6 Medium (50x50) DFS 0.3506400156766176 93.0 92.0 1.0
7 Medium (50x50) A* 1.0985400062054396 182.0 92.0 1.0
8 Large (100x100) BFS 0.7311799563467503 201.0 149.0 1.0
9 Large (100x100) DFS 0.551999919116497 151.0 149.0 1.0
10 Large (100x100) A* 1.2306599877774715 200.0 149.0 1.0
11 Empty BFS 7.031580060720444 1834.0 86.0 1.0
12 Empty DFS 4.2091799434274435 1797.0 922.0 1.0
13 Empty A* 13.363939989358187 1834.0 86.0 1.0
14 No exit BFS -1 -1 -1 0
15 No exit DFS -1 -1 -1 0
16 No exit A* -1 -1 -1 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

532
ShulpinIN/maze_lab2/maze.py Normal file
View File

@ -0,0 +1,532 @@
import sys
from collections import deque
import heapq
import time
import os
from abc import ABC, abstractmethod
from typing import List, Optional, Dict, Any
DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data"
class Observer(ABC):
@abstractmethod
def update(self, event: str, data: Any = None):
pass
class Observable:
def __init__(self):
self._observers: List[Observer] = []
def attach(self, observer: Observer):
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self, event: str, data: Any = None):
for observer in self._observers:
observer.update(event, data)
class Tile:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
self._wall = False
self._start = False
self._exit = False
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
@property
def is_wall(self) -> bool:
return self._wall
@is_wall.setter
def is_wall(self, v: bool):
self._wall = v
@property
def is_start(self) -> bool:
return self._start
@is_start.setter
def is_start(self, v: bool):
self._start = v
@property
def is_exit(self) -> bool:
return self._exit
@is_exit.setter
def is_exit(self, v: bool):
self._exit = v
def passable(self) -> bool:
return not self._wall
def __hash__(self):
return hash((self._x, self._y))
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self._x == other._x and self._y == other._y
class Maze:
def __init__(self, w: int, h: int):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start: Optional[Tile] = None
self._exit: Optional[Tile] = None
@property
def width(self) -> int:
return self._w
@property
def height(self) -> int:
return self._h
@property
def start(self) -> Optional[Tile]:
return self._start
@property
def exit(self) -> Optional[Tile]:
return self._exit
def get_cell(self, x: int, y: int) -> Optional[Tile]:
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x: int, y: int, kind: str):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell: Tile) -> List[Tile]:
result = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class MazeLoader(ABC):
@abstractmethod
def load(self, filename: str) -> Maze:
pass
class TextMazeLoader(MazeLoader):
def load(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h else 0
start_count = 0
exit_count = 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
maze.set_cell(x, y, 'wall')
elif ch == 'S':
maze.set_cell(x, y, 'start')
start_count += 1
elif ch == 'E':
maze.set_cell(x, y, 'exit')
exit_count += 1
else:
maze.set_cell(x, y, 'path')
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
return maze
class PathFinder(ABC):
def __init__(self):
self._visited = 0
@abstractmethod
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
pass
def _reconstruct(self, parent: Dict[Tile, Optional[Tile]], start: Tile, goal: Tile) -> List[Tile]:
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self) -> int:
return self._visited
class BFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
self._visited = len(visited)
return []
class DFS(PathFinder):
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
stack = [start]
parent = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
self._visited = len(visited)
return []
class AStar(PathFinder):
def _heuristic(self, cell: Tile, goal: Tile) -> int:
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze: Maze, start: Tile, goal: Tile) -> List[Tile]:
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.neighbours(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
parent[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, goal)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited = len(visited)
return []
class MazeSolver(Observable):
def __init__(self, maze: Maze):
super().__init__()
self._maze = maze
self._algorithm: Optional[PathFinder] = None
def set_algorithm(self, algorithm: PathFinder):
self._algorithm = algorithm
def solve(self) -> Optional[Dict[str, Any]]:
if not self._algorithm:
raise ValueError("Algorithm not set")
start_time = time.perf_counter()
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return {
'time_ms': elapsed_ms,
'visited': self._algorithm.visited_count,
'path_length': len(path),
'path': path
}
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self) -> bool:
pass
class MoveCommand(Command):
def __init__(self, player: 'Player', dx: int, dy: int, maze: Maze):
self._player = player
self._dx = dx
self._dy = dy
self._maze = maze
self._executed = False
def execute(self) -> bool:
new_x = self._player.position.x + self._dx
new_y = self._player.position.y + self._dy
target = self._maze.get_cell(new_x, new_y)
if target and target.passable():
self._player.move_to(target)
self._executed = True
return True
return False
def undo(self) -> bool:
if self._executed:
self._player.undo()
self._executed = False
return True
return False
class Player:
def __init__(self, start_tile: Tile):
self._position = start_tile
self._previous = None
@property
def position(self) -> Tile:
return self._position
def move_to(self, tile: Tile):
self._previous = self._position
self._position = tile
def undo(self):
if self._previous:
self._position, self._previous = self._previous, None
class ConsoleView(Observer):
def __init__(self, maze: Maze, player: Optional[Player] = None):
self._maze = maze
self._player = player
self._current_path: List[Tile] = []
def update(self, event: str, data: Any = None):
if event == "solving_finished":
self._current_path = data.get('path', [])
self._display_solution(data)
def _display_solution(self, stats: Dict):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE SOLUTION")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
elif self._current_path and cell in self._current_path:
print('', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print(f"Time: {stats['time_ms']:.3f} ms")
print(f"Visited: {stats['visited']}")
print(f"Path length: {stats['path_length']}")
def display_maze(self):
os.system('cls' if os.name == 'nt' else 'clear')
print("=" * (self._maze.width * 2 + 4))
print("MAZE")
print("=" * (self._maze.width * 2 + 4))
for y in range(self._maze.height):
print(" ", end='')
for x in range(self._maze.width):
cell = self._maze.get_cell(x, y)
if self._player and cell == self._player.position:
print('P', end=' ')
elif cell == self._maze.start:
print('S', end=' ')
elif cell == self._maze.exit:
print('E', end=' ')
elif cell.is_wall:
print('#', end=' ')
else:
print('.', end=' ')
print()
print("=" * (self._maze.width * 2 + 4))
print("S - start E - exit # - wall . - path P - player")
def interactive_mode(maze: Maze):
player = Player(maze.start)
view = ConsoleView(maze, player)
view.display_maze()
solver = MazeSolver(maze)
solver.attach(view)
commands_history: List[Command] = []
print("\nControls:")
print("H (←) J (↓) K (↑) L (→) - move")
print("U - undo")
print("B - BFS")
print("D - DFS")
print("A - A*")
print("Q - quit")
print("\n" + "=" * 50)
while True:
cmd = input("\n> ").lower().strip()
if cmd == 'q':
break
elif cmd == 'b':
solver.set_algorithm(BFS())
result = solver.solve()
if result:
print(f"BFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'd':
solver.set_algorithm(DFS())
result = solver.solve()
if result:
print(f"DFS: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd == 'a':
solver.set_algorithm(AStar())
result = solver.solve()
if result:
print(f"A*: {result['time_ms']:.3f} ms, visited={result['visited']}, length={result['path_length']}")
elif cmd in ['h', 'j', 'k', 'l']:
dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)}
dx, dy = dir_map[cmd]
move = MoveCommand(player, dx, dy, maze)
if move.execute():
commands_history.append(move)
view.display_maze()
if player.position == maze.exit:
print("\n*** YOU ESCAPED! ***")
print(f"Total moves: {len(commands_history)}")
break
else:
print("Blocked!")
elif cmd == 'u':
if commands_history:
last_command = commands_history.pop()
last_command.undo()
view.display_maze()
print("Undo successful")
else:
print("Nothing to undo")
else:
print("Unknown command")
def main():
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
import subprocess
subprocess.run([sys.executable, 'plots.py'])
return
loader = TextMazeLoader()
maze_file = os.path.join(DATA_PATH, "maze1.txt")
if not os.path.exists(maze_file):
print(f"ERROR: Maze file not found: {maze_file}")
print(f"Please create maze1.txt in: {DATA_PATH}")
return
maze = loader.load(maze_file)
interactive_mode(maze)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,580 @@
import csv
import time
import os
import matplotlib.pyplot as plt
import numpy as np
from collections import deque
import heapq
from maze import DATA_PATH
class Tile:
def __init__(self, x: int, y: int):
self._x = x
self._y = y
self._wall = False
self._start = False
self._exit = False
@property
def x(self) -> int:
return self._x
@property
def y(self) -> int:
return self._y
@property
def is_wall(self) -> bool:
return self._wall
@is_wall.setter
def is_wall(self, v: bool):
self._wall = v
@property
def is_start(self) -> bool:
return self._start
@is_start.setter
def is_start(self, v: bool):
self._start = v
@property
def is_exit(self) -> bool:
return self._exit
@is_exit.setter
def is_exit(self, v: bool):
self._exit = v
def passable(self) -> bool:
return not self._wall
def __hash__(self):
return hash((self._x, self._y))
def __eq__(self, other):
if not isinstance(other, Tile):
return False
return self._x == other._x and self._y == other._y
class Maze:
def __init__(self, w: int, h: int):
self._w = w
self._h = h
self._cells = [[Tile(x, y) for x in range(w)] for y in range(h)]
self._start = None
self._exit = None
@property
def width(self) -> int:
return self._w
@property
def height(self) -> int:
return self._h
@property
def start(self):
return self._start
@property
def exit(self):
return self._exit
def get_cell(self, x: int, y: int):
if 0 <= x < self._w and 0 <= y < self._h:
return self._cells[y][x]
return None
def set_cell(self, x: int, y: int, kind: str):
c = self.get_cell(x, y)
if not c:
return
if kind == 'wall':
c.is_wall = True
elif kind == 'start':
if self._start:
self._start.is_start = False
c.is_start = True
c.is_wall = False
self._start = c
elif kind == 'exit':
if self._exit:
self._exit.is_exit = False
c.is_exit = True
c.is_wall = False
self._exit = c
elif kind == 'path':
c.is_wall = False
def neighbours(self, cell):
result = []
for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.passable():
result.append(nb)
return result
class TextMazeLoader:
def load(self, filename: str):
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
h = len(lines)
w = max(len(line) for line in lines) if h else 0
start_count = 0
exit_count = 0
maze = Maze(w, h)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
if ch == '#':
maze.set_cell(x, y, 'wall')
elif ch == 'S':
maze.set_cell(x, y, 'start')
start_count += 1
elif ch == 'E':
maze.set_cell(x, y, 'exit')
exit_count += 1
else:
maze.set_cell(x, y, 'path')
if start_count != 1 or exit_count != 1:
raise ValueError(f"Maze must have one S and one E. Found: S={start_count}, E={exit_count}")
return maze
class BFS:
def __init__(self):
self._visited = 0
def find(self, maze, start, goal):
from collections import deque
queue = deque([start])
parent = {start: None}
visited = {start}
while queue:
current = queue.popleft()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
queue.append(neighbor)
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class DFS:
def __init__(self):
self._visited = 0
def find(self, maze, start, goal):
stack = [start]
parent = {start: None}
visited = {start}
while stack:
current = stack.pop()
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
for neighbor in maze.neighbours(current):
if neighbor not in visited:
visited.add(neighbor)
parent[neighbor] = current
stack.append(neighbor)
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class AStar:
def __init__(self):
self._visited = 0
def _heuristic(self, cell, goal):
return abs(cell.x - goal.x) + abs(cell.y - goal.y)
def find(self, maze, start, goal):
import heapq
heap = []
counter = 0
start_f = self._heuristic(start, goal)
heapq.heappush(heap, (start_f, counter, start))
counter += 1
parent = {}
g_score = {start: 0}
f_score = {start: start_f}
visited = set()
while heap:
current_f, _, current = heapq.heappop(heap)
visited.add(current)
if current == goal:
self._visited = len(visited)
return self._reconstruct(parent, start, goal)
if current_f > f_score.get(current, float('inf')):
continue
for neighbor in maze.neighbours(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
parent[neighbor] = current
g_score[neighbor] = tentative_g
new_f = tentative_g + self._heuristic(neighbor, goal)
f_score[neighbor] = new_f
heapq.heappush(heap, (new_f, counter, neighbor))
counter += 1
self._visited = len(visited)
return []
def _reconstruct(self, parent, start, goal):
path = []
current = goal
while current is not None:
path.append(current)
current = parent.get(current)
path.reverse()
return path if path and path[0] == start else []
@property
def visited_count(self):
return self._visited
class MazeSolver:
def __init__(self, maze):
self._maze = maze
self._algorithm = None
def set_algorithm(self, algorithm):
self._algorithm = algorithm
def solve(self):
if not self._algorithm:
raise ValueError("Algorithm not set")
start_time = time.perf_counter()
path = self._algorithm.find(self._maze, self._maze.start, self._maze.exit)
end_time = time.perf_counter()
elapsed_ms = (end_time - start_time) * 1000
return {
'time_ms': elapsed_ms,
'visited': self._algorithm.visited_count,
'path_length': len(path),
'path': path
}
DATA_PATH = r"C:\Users\User\2026-rff_mp\ShulpinIN\maze_lab2\docs\data"
class ExperimentRunner:
def __init__(self):
self.algorithms = {
"BFS": BFS(),
"DFS": DFS(),
"A*": AStar()
}
self.loader = TextMazeLoader()
def run_benchmark(self, maze_file: str, algorithm: str, runs: int = 5):
try:
maze = self.loader.load(maze_file)
except Exception as e:
return None
total_time = 0.0
total_visited = 0
total_length = 0
successes = 0
for _ in range(runs):
solver = MazeSolver(maze)
solver.set_algorithm(self.algorithms[algorithm])
result = solver.solve()
if result and result['path_length'] > 0:
total_time += result['time_ms']
total_visited += result['visited']
total_length += result['path_length']
successes += 1
if successes == 0:
return None
return {
'time_ms': total_time / successes,
'visited_cells': total_visited / successes,
'path_length': total_length / successes,
'success_rate': successes / runs
}
def run_all_experiments(self, runs: int = 5):
mazes_list = [
(os.path.join(DATA_PATH, "small.txt"), "Small (10x10)"),
(os.path.join(DATA_PATH, "medium.txt"), "Medium (50x50)"),
(os.path.join(DATA_PATH, "large.txt"), "Large (100x100)"),
(os.path.join(DATA_PATH, "empty.txt"), "Empty"),
(os.path.join(DATA_PATH, "no_exit.txt"), "No exit")
]
results = []
print("running experiments")
print(f"Data path: {DATA_PATH}")
for maze_file, maze_name in mazes_list:
if not os.path.exists(maze_file):
print(f"\n[warn] File not found: {maze_file}")
continue
print(f"\nTesting: {maze_name}")
for algo_name in self.algorithms.keys():
stats = self.run_benchmark(maze_file, algo_name, runs)
if stats:
print(
f" {algo_name}: time={stats['time_ms']:.3f}ms, visited={stats['visited_cells']:.0f}, length={stats['path_length']:.0f}")
results.append({
'maze': maze_name,
'strategy': algo_name,
'time_ms': stats['time_ms'],
'visited_cells': stats['visited_cells'],
'path_length': stats['path_length'],
'success_rate': stats['success_rate']
})
else:
print(f" {algo_name}: no path found")
results.append({
'maze': maze_name,
'strategy': algo_name,
'time_ms': -1,
'visited_cells': -1,
'path_length': -1,
'success_rate': 0
})
return results
def create_visualizations(results):
valid_results = [r for r in results if r['time_ms'] > 0]
if not valid_results:
print("no valid results for visualization")
return
mazes = sorted(set(r['maze'] for r in valid_results))
algorithms = ['BFS', 'DFS', 'A*']
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
fig.suptitle('pathfinding algorithms comparison', fontsize=14)
x = np.arange(len(mazes))
width = 0.25
# Time chart
for i, algo in enumerate(algorithms):
times = []
for maze in mazes:
val = next((r['time_ms'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
times.append(val)
bars = axes[0].bar(x + i * width, times, width, label=algo, alpha=0.8)
for bar, val in zip(bars, times):
if val > 0:
axes[0].text(bar.get_x() + bar.get_width() / 2, bar.get_height() + 0.5,
f'{val:.1f}', ha='center', va='bottom', fontsize=7)
axes[0].set_title('execution Time (ms)')
axes[0].set_ylabel('time (ms)')
axes[0].set_xticks(x + width)
axes[0].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[0].legend()
axes[0].grid(alpha=0.3, axis='y')
# Visited cells chart
for i, algo in enumerate(algorithms):
visited = []
for maze in mazes:
val = next((r['visited_cells'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
visited.append(val)
bars = axes[1].bar(x + i * width, visited, width, label=algo, alpha=0.8)
for bar, val in zip(bars, visited):
if val > 0:
axes[1].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
axes[1].set_title('visited Cells')
axes[1].set_ylabel('count')
axes[1].set_xticks(x + width)
axes[1].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[1].legend()
axes[1].grid(alpha=0.3, axis='y')
# Path length chart
for i, algo in enumerate(algorithms):
lengths = []
for maze in mazes:
val = next((r['path_length'] for r in valid_results
if r['maze'] == maze and r['strategy'] == algo), 0)
lengths.append(val)
bars = axes[2].bar(x + i * width, lengths, width, label=algo, alpha=0.8)
for bar, val in zip(bars, lengths):
if val > 0:
axes[2].text(bar.get_x() + bar.get_width() / 2, bar.get_height(),
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
axes[2].set_title('path Length')
axes[2].set_ylabel('steps')
axes[2].set_xticks(x + width)
axes[2].set_xticklabels(mazes, rotation=45, ha='right', fontsize=8)
axes[2].legend()
axes[2].grid(alpha=0.3, axis='y')
plt.tight_layout()
output_path = os.path.join(DATA_PATH, 'experiment_results.png')
plt.savefig(output_path, dpi=150, bbox_inches='tight')
print(f"\nPlot saved to: {output_path}")
plt.show()
def save_results_to_csv(results, filename='experiment_results.csv'):
if not results:
return
filepath = os.path.join(DATA_PATH, filename)
with open(filepath, 'w', newline='', encoding='utf-8') as f:
fieldnames = ['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length', 'success_rate']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(results)
print(f"Results saved to: {filepath}")
def analyze_efficiency(results):
valid_results = [r for r in results if r['time_ms'] > 0]
if not valid_results:
print("no valid results for analysis")
return
algo_stats = {}
for algo in ['BFS', 'DFS', 'A*']:
algo_data = [r for r in valid_results if r['strategy'] == algo]
if algo_data:
algo_stats[algo] = {
'avg_time': sum(r['time_ms'] for r in algo_data) / len(algo_data),
'avg_visited': sum(r['visited_cells'] for r in algo_data) / len(algo_data),
'avg_length': sum(r['path_length'] for r in algo_data) / len(algo_data)
}
print("average values across all mazes")
print(f"{'Algorithm':<12} {'Time (ms)':<15} {'Visited':<15} {'Path length':<15}")
for algo, stats in algo_stats.items():
print(f"{algo:<12} {stats['avg_time']:<15.3f} {stats['avg_visited']:<15.1f} {stats['avg_length']:<15.1f}")
fastest = min(algo_stats.items(), key=lambda x: x[1]['avg_time'])
optimal = min(algo_stats.items(), key=lambda x: x[1]['avg_length'])
efficient = min(algo_stats.items(), key=lambda x: x[1]['avg_visited'])
print("conclusions:")
print(f" fastest algorithm: {fastest[0]} ({fastest[1]['avg_time']:.3f} ms avg)")
print(f" optimal path: {optimal[0]} ({optimal[1]['avg_length']:.1f} steps avg)")
print(f" most efficient (fewest visits): {efficient[0]} ({efficient[1]['avg_visited']:.0f} cells avg)")
print("=" * 70)
def main():
if not os.path.exists(DATA_PATH):
print(f"\nerr: directory not found: {DATA_PATH}")
print("please create the directory and place maze files there.")
print("\nexpected structure:")
print(f" {DATA_PATH}/")
print(" ├── small.txt")
print(" ├── medium.txt")
print(" ├── large.txt")
print(" ├── empty.txt")
print(" └── no_exit.txt")
return
runner = ExperimentRunner()
results = runner.run_all_experiments(runs=5)
if not results:
print("\nNo results. Check if maze files exist in:", DATA_PATH)
return
save_results_to_csv(results)
analyze_efficiency(results)
create_visualizations(results)
if __name__ == "__main__":
main()