diff --git a/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml b/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/ShulpinIN/datastructure_lab1/.idea/datastructure_lab1.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/ShulpinIN/datastructure_lab1/datastruct.py b/ShulpinIN/datastructure_lab1/datastruct.py new file mode 100644 index 0000000..09a05bf --- /dev/null +++ b/ShulpinIN/datastructure_lab1/datastruct.py @@ -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() \ No newline at end of file diff --git a/ShulpinIN/datastructure_lab1/docs/README.md b/ShulpinIN/datastructure_lab1/docs/README.md new file mode 100644 index 0000000..45a0a50 --- /dev/null +++ b/ShulpinIN/datastructure_lab1/docs/README.md @@ -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 требует осторожного применения из-за чувствительности к порядку данных. Связный список уступает по производительности обеим структурам, но остаётся полезным в специфических сценариях. + diff --git a/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png b/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png new file mode 100644 index 0000000..28a6cca Binary files /dev/null and b/ShulpinIN/datastructure_lab1/docs/data/performance_comparison.png differ diff --git a/ShulpinIN/datastructure_lab1/docs/data/results.csv b/ShulpinIN/datastructure_lab1/docs/data/results.csv new file mode 100644 index 0000000..389f296 --- /dev/null +++ b/ShulpinIN/datastructure_lab1/docs/data/results.csv @@ -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 diff --git a/ShulpinIN/maze_lab2/README.md b/ShulpinIN/maze_lab2/README.md new file mode 100644 index 0000000..2b572bc --- /dev/null +++ b/ShulpinIN/maze_lab2/README.md @@ -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 { + <> + +load(filename) Maze + } + + class TextFileMazeBuilder { + +load(filename) Maze + } + + class Maze { + -Tile[][] cells + +getCell(x,y) Tile + +getNeighbors(cell) List~Tile~ + } + + class PathFindingStrategy { + <> + +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 { + <> + +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. **Программа корректно определяет отсутствие пути.** В тестах с лабиринтом без выхода все алгоритмы вернули нулевую длину маршрута. diff --git a/ShulpinIN/maze_lab2/docs/data/empty.txt b/ShulpinIN/maze_lab2/docs/data/empty.txt new file mode 100644 index 0000000..6d0a249 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/empty.txt @@ -0,0 +1,49 @@ +######################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +######################################## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.csv b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv new file mode 100644 index 0000000..855bf62 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/experiment_results.csv @@ -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 diff --git a/ShulpinIN/maze_lab2/docs/data/experiment_results.png b/ShulpinIN/maze_lab2/docs/data/experiment_results.png new file mode 100644 index 0000000..e5c6cdb Binary files /dev/null and b/ShulpinIN/maze_lab2/docs/data/experiment_results.png differ diff --git a/ShulpinIN/maze_lab2/docs/data/large.txt b/ShulpinIN/maze_lab2/docs/data/large.txt new file mode 100644 index 0000000..90a84ad --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/large.txt @@ -0,0 +1,54 @@ +#################################################################################################### +#S # +# ################################################################################################ # +# # # # +# # ############################################################################################ # # +# # # # # # +# # # ######################################################################################## # # # +# # # # # # # # +# # # # #################################################################################### # # # # +# # # # # # # # # # +# # # # # ################################################################################ # # # # # +# # # # # # # # # # # # +# # # # # # ############################################################################ # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ######################################################################## # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # #################################################################### # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ################################################################ # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ############################################################ # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ######################################################## # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # #################################################### # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # ################################################ # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # ############################################ # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # ######################################## # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # #################################### # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # ################################ # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # ############################ # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # ######################## # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # #################### # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # ################ # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # ############ # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # ######## # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # #### # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #E# +#################################################################################################### \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/maze1.txt b/ShulpinIN/maze_lab2/docs/data/maze1.txt new file mode 100644 index 0000000..07a3ed5 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/maze1.txt @@ -0,0 +1,10 @@ +########## +#S # +### ##### +# # E# +# # # # ## +# # # +####### # +# # +# ###### # +########## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/medium.txt b/ShulpinIN/maze_lab2/docs/data/medium.txt new file mode 100644 index 0000000..c8df775 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/medium.txt @@ -0,0 +1,48 @@ +################################################## +#S # +# ############################################# # +# # # # +# # ######################################### # # +# # # # # # +# # # ##################################### # # # +# # # # # # # # +# # # # ################################# # # # # +# # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # +# # # # ################################# # # # # +# # # # # # # # +# # # ##################################### # # # +# # # # # # +# # ######################################### # # +# # # # +# ############################################# # +# E# +################################################## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/no_exit.txt b/ShulpinIN/maze_lab2/docs/data/no_exit.txt new file mode 100644 index 0000000..4697881 --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/no_exit.txt @@ -0,0 +1,10 @@ +########## +#S # +### ##### +# # # +# # # # ## +# # # +####### # +# # +# ###### # +########## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/docs/data/small.txt b/ShulpinIN/maze_lab2/docs/data/small.txt new file mode 100644 index 0000000..e21dcdf --- /dev/null +++ b/ShulpinIN/maze_lab2/docs/data/small.txt @@ -0,0 +1,10 @@ +########## +#S # +### ##### +# # E# +# # # # ## +# # # +####### # +# # +# ###### # +########## \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/maze.py b/ShulpinIN/maze_lab2/maze.py new file mode 100644 index 0000000..40c1d36 --- /dev/null +++ b/ShulpinIN/maze_lab2/maze.py @@ -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() \ No newline at end of file diff --git a/ShulpinIN/maze_lab2/plots.py b/ShulpinIN/maze_lab2/plots.py new file mode 100644 index 0000000..c4f4dfa --- /dev/null +++ b/ShulpinIN/maze_lab2/plots.py @@ -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() \ No newline at end of file