diff --git a/ivanchenkoam/laba1.txt b/ivanchenkoam/laba1.txt new file mode 100644 index 0000000..43ffaa5 --- /dev/null +++ b/ivanchenkoam/laba1.txt @@ -0,0 +1,537 @@ +import time +import csv +import random +import sys +from typing import List, Tuple, Optional, Any, Dict + +#лимит рекурсии +sys.setrecursionlimit(20000) +def ll_insert(head: Optional[Dict], name: str, phone: str) -> Dict: + """Вставка в конец связного списка""" + new_node = {'name': name, 'phone': phone, 'next': None} + + if head is None: + return new_node + + current = head + while current['next'] is not None: + # Обновляем, если уже есть + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + + if current['name'] == name: + current['phone'] = phone + else: + current['next'] = new_node + + return head + + +def ll_find(head: Optional[Dict], name: str) -> Optional[str]: + """Поиск в связном списке""" + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + + +def ll_delete(head: Optional[Dict], name: str) -> Optional[Dict]: + """Удаление из связного списка""" + if head is None: + return None + + if head['name'] == name: + return head['next'] + + current = head + while current['next'] is not None: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + current = current['next'] + + return head + + +def ll_list_all(head: Optional[Dict]) -> List[Tuple[str, str]]: + """Сбор всех записей из связного списка с сортировкой""" + records = [] + current = head + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] +def hash_function(name: str, size: int) -> int: + """Простая хеш-функция""" + return sum(ord(c) for c in name) % size + + +def ht_create(size: int = 1000) -> List[Optional[Dict]]: + """Создание хеш-таблицы""" + return [None] * size + + +def ht_insert(buckets: List[Optional[Dict]], name: str, phone: str) -> None: + """Вставка в хеш-таблицу""" + index = hash_function(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + + +def ht_find(buckets: List[Optional[Dict]], name: str) -> Optional[str]: + """Поиск в хеш-таблице""" + index = hash_function(name, len(buckets)) + return ll_find(buckets[index], name) + + +def ht_delete(buckets: List[Optional[Dict]], name: str) -> None: + """Удаление из хеш-таблицы""" + index = hash_function(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + + +def ht_list_all(buckets: List[Optional[Dict]]) -> List[Tuple[str, str]]: + """Сбор всех записей из хеш-таблицы с сортировкой""" + records = [] + for head in buckets: + current = head + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + + records.sort(key=lambda x: x[0]) + return records +def bst_insert(root: Optional[Dict], name: str, phone: str) -> Dict: + """Вставка в BST (итеративная)""" + new_node = {'name': name, 'phone': phone, 'left': None, 'right': None} + + if root is None: + return new_node + + current = root + while True: + if name < current['name']: + if current['left'] is None: + current['left'] = new_node + break + else: + current = current['left'] + elif name > current['name']: + if current['right'] is None: + current['right'] = new_node + break + else: + current = current['right'] + else: + # Обновляем существующую запись + current['phone'] = phone + break + + return root + + +def bst_find(root: Optional[Dict], name: str) -> Optional[str]: + """Поиск в BST (итеративный)""" + current = root + while current is not None: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + + +def bst_min_node(node: Dict) -> Dict: + """Поиск узла с минимальным значением""" + current = node + while current['left'] is not None: + current = current['left'] + return current + + +def bst_delete(root: Optional[Dict], name: str) -> Optional[Dict]: + """Удаление из BST (итеративная версия)""" + if root is None: + return None + + # Поиск узла для удаления и его родителя + parent = None + current = root + + while current is not None and current['name'] != name: + parent = current + if name < current['name']: + current = current['left'] + else: + current = current['right'] + + if current is None: + return root + + # Случай 1: узел не имеет детей + if current['left'] is None and current['right'] is None: + if parent is None: + return None + if parent['left'] == current: + parent['left'] = None + else: + parent['right'] = None + return root + + # Случай 2: узел имеет одного ребёнка + if current['left'] is None: + child = current['right'] + elif current['right'] is None: + child = current['left'] + else: + # Случай 3: узел имеет двух детей + # Находим минимальный узел в правом поддереве + successor_parent = current + successor = current['right'] + while successor['left'] is not None: + successor_parent = successor + successor = successor['left'] + + # Копируем данные из successor в current + current['name'] = successor['name'] + current['phone'] = successor['phone'] + + # Удаляем successor + if successor_parent['left'] == successor: + successor_parent['left'] = successor['right'] + else: + successor_parent['right'] = successor['right'] + + return root + + # Присоединяем ребёнка к родителю + if parent is None: + return child + if parent['left'] == current: + parent['left'] = child + else: + parent['right'] = child + + return root + + +def bst_inorder(root: Optional[Dict], records: List[Tuple[str, str]]) -> None: + """Центрированный обход BST (рекурсивный)""" + if root is not None: + bst_inorder(root['left'], records) + records.append((root['name'], root['phone'])) + bst_inorder(root['right'], records) + + +def bst_list_all(root: Optional[Dict]) -> List[Tuple[str, str]]: + """Сбор всех записей из BST (уже отсортированы)""" + records = [] + bst_inorder(root, records) + return records + + + + records.sort(key=lambda x: x[0]) + return records + +def generate_records(n: int) -> List[Tuple[str, str]]: + """Генерация записей""" + records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)] + return records +def measure_insertion(structure_type: str, data: List[Tuple[str, str]], + ht_size: int = 1000) -> float: + """Замер времени вставки""" + start = time.perf_counter() + + if structure_type == "LinkedList": + head = None + for name, phone in data: + head = ll_insert(head, name, phone) + + elif structure_type == "HashTable": + buckets = ht_create(ht_size) + for name, phone in data: + ht_insert(buckets, name, phone) + + elif structure_type == "BST": + root = None + for name, phone in data: + root = bst_insert(root, name, phone) + + end = time.perf_counter() + return end - start + + +def measure_find(structure_type: str, data: List[Tuple[str, str]], + existing_names: List[str], missing_names: List[str], + ht_size: int = 1000) -> Tuple[float, Any]: + """Замер времени поиска (возвращает время и структуру для удаления)""" + # Сначала создаём структуру + if structure_type == "LinkedList": + head = None + for name, phone in data: + head = ll_insert(head, name, phone) + + start = time.perf_counter() + for name in existing_names + missing_names: + ll_find(head, name) + end = time.perf_counter() + return end - start, head + + elif structure_type == "HashTable": + buckets = ht_create(ht_size) + for name, phone in data: + ht_insert(buckets, name, phone) + + start = time.perf_counter() + for name in existing_names + missing_names: + ht_find(buckets, name) + end = time.perf_counter() + return end - start, buckets + + elif structure_type == "BST": + root = None + for name, phone in data: + root = bst_insert(root, name, phone) + + start = time.perf_counter() + for name in existing_names + missing_names: + bst_find(root, name) + end = time.perf_counter() + return end - start, root + + +def measure_delete(structure_type: str, structure: Any, + names_to_delete: List[str]) -> float: + """Замер времени удаления""" + start = time.perf_counter() + + if structure_type == "LinkedList": + head = structure + for name in names_to_delete: + head = ll_delete(head, name) + + elif structure_type == "HashTable": + buckets = structure + for name in names_to_delete: + ht_delete(buckets, name) + + elif structure_type == "BST": + root = structure + for name in names_to_delete: + root = bst_delete(root, name) + + end = time.perf_counter() + return end - start +def run_experiment(n_records: int = 10000, n_find: int = 110, + n_delete: int = 50, n_runs: int = 5) -> List[List]: + """Запуск всех замеров""" + + # Генерация данных + all_records = generate_records(n_records) + records_shuffled = all_records.copy() + random.shuffle(records_shuffled) + records_sorted = sorted(all_records, key=lambda x: x[0]) + + # Имена для поиска + all_names = [name for name, _ in all_records] + existing_names = random.sample(all_names, n_find - 10) + missing_names = [f"None_{i}" for i in range(10)] + + # Имена для удаления + names_to_delete = random.sample(all_names, n_delete) + + structures = ["LinkedList", "HashTable", "BST"] + modes = {"shuffled": records_shuffled, "sorted": records_sorted} + + # Заголовок CSV + results = [["Структура", "Режим", "Операция", + "Замер1", "Замер2", "Замер3", "Замер4", "Замер5", "Среднее"]] + + for structure in structures: + for mode_name, mode_data in modes.items(): + print(f"\nТестирование: {structure}, режим: {mode_name}") + + # Вставка + insertion_times = [] + for run in range(n_runs): + print(f" Вставка, run {run+1}/{n_runs}...") + t = measure_insertion(structure, mode_data) + insertion_times.append(t) + + avg_insertion = sum(insertion_times) / n_runs + results.append([structure, mode_name, "вставка"] + + [f"{t:.6f}" for t in insertion_times] + + [f"{avg_insertion:.6f}"]) + print(f" Замеры: {[f'{t:.6f}' for t in insertion_times]}") + print(f" Среднее: {avg_insertion:.6f} сек") + + # Поиск + find_times = [] + for run in range(n_runs): + print(f" Поиск, run {run+1}/{n_runs}...") + t, _ = measure_find(structure, mode_data, + existing_names, missing_names) + find_times.append(t) + + avg_find = sum(find_times) / n_runs + results.append([structure, mode_name, "поиск"] + + [f"{t:.6f}" for t in find_times] + + [f"{avg_find:.6f}"]) + print(f" Замеры: {[f'{t:.6f}' for t in find_times]}") + print(f" Среднее: {avg_find:.6f} сек") + + # Удаление + delete_times = [] + for run in range(n_runs): + print(f" Удаление, run {run+1}/{n_runs}...") + # Создаём свежую структуру для каждого замера удаления + if structure == "LinkedList": + head = None + for name, phone in mode_data: + head = ll_insert(head, name, phone) + t = measure_delete(structure, head, names_to_delete) + elif structure == "HashTable": + buckets = ht_create() + for name, phone in mode_data: + ht_insert(buckets, name, phone) + t = measure_delete(structure, buckets, names_to_delete) + elif structure == "BST": + root = None + for name, phone in mode_data: + root = bst_insert(root, name, phone) + t = measure_delete(structure, root, names_to_delete) + + delete_times.append(t) + + avg_delete = sum(delete_times) / n_runs + results.append([structure, mode_name, "удаление"] + + [f"{t:.6f}" for t in delete_times] + + [f"{avg_delete:.6f}"]) + print(f" Замеры: {[f'{t:.6f}' for t in delete_times]}") + print(f" Среднее: {avg_delete:.6f} сек") + + return results + +def save_results(results: List[List], filename: str = "results.csv"): + """Сохранение результатов в CSV""" + with open(filename, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerows(results) + print(f"\nРезультаты сохранены в {filename}") +def plot_results(results_file: str = "results.csv"): + """Построение графика сравнения производительности""" + import matplotlib.pyplot as plt + import numpy as np + + # Чтение результатов из CSV + data = {} + with open(results_file, 'r', encoding='utf-8') as f: + reader = csv.reader(f) + header = next(reader) # пропускаем заголовок + + for row in reader: + structure = row[0] + mode = row[1] + operation = row[2] + # Берём последнюю колонку (Среднее) + avg_time = float(row[-1]) + + if structure not in data: + data[structure] = {} + if mode not in data[structure]: + data[structure][mode] = {} + + data[structure][mode][operation] = avg_time + + # Настройка стиля + plt.style.use('seaborn-v0_8-darkgrid') + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + colors = ['#FF6B6B', '#4ECDC4'] + + structures = ["LinkedList", "HashTable", "BST"] + modes = ["shuffled", "sorted"] + operations = ["вставка", "поиск", "удаление"] + op_titles = ["ВСТАВКИ", "ПОИСКА (110 запросов)", "УДАЛЕНИЯ (50 записей)"] + + for idx, (op, op_title) in enumerate(zip(operations, op_titles)): + ax = axes[idx] + x = np.arange(len(structures)) + width = 0.35 + + shuffled_vals = [data[s]['shuffled'][op] for s in structures] + sorted_vals = [data[s]['sorted'][op] for s in structures] + + bars1 = ax.bar(x - width/2, shuffled_vals, width, + label='Случайный порядок', color=colors[0]) + bars2 = ax.bar(x + width/2, sorted_vals, width, + label='Отсортированный порядок', color=colors[1]) + + ax.set_xlabel('Структура данных') + ax.set_ylabel('Время (секунды)') + ax.set_title(f'Сравнение времени {op_title}') + ax.set_xticks(x) + ax.set_xticklabels(['Связный\nсписок', 'Хеш-\nтаблица', 'Двоичное\nдерево']) + ax.legend() + + # Добавляем значения на столбцы + for bars in [bars1, bars2]: + for bar in bars: + height = bar.get_height() + fmt = '{:.4f}'.format(height) if op == 'вставка' else '{:.6f}'.format(height) + ax.text(bar.get_x() + bar.get_width()/2., height, + fmt, ha='center', va='bottom', fontsize=8) + + plt.tight_layout() + plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight') + plt.show() + + # Вывод сводной таблицы в консоль + print("\n" + "=" * 90) + print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ (среднее время в секундах)") + print("=" * 90) + print(f"{'Структура':<15} {'Режим':<12} {'Вставка':<14} {'Поиск':<14} {'Удаление':<14}") + print("-" * 90) + + for structure in structures: + for mode in modes: + print(f"{structure:<15} {mode:<12} " + f"{data[structure][mode]['вставка']:<14.6f} " + f"{data[structure][mode]['поиск']:<14.6f} " + f"{data[structure][mode]['удаление']:<14.6f}") + + print("=" * 90) +def main(): + print("=" * 60) + print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ") + print("Связный список | Хеш-таблица | Двоичное дерево поиска") + print("=" * 60) + + # Запуск эксперимента (5 прогонов) + results = run_experiment(n_records=10000, n_runs=5) + + # Сохранение результатов + save_results(results) + + # Построение графика + try: + import matplotlib.pyplot as plt + print("\nПостроение графика...") + plot_results("results.csv") + + except ImportError: + print("\nВНИМАНИЕ: Библиотека matplotlib не установлена.") + print("Для построения графика выполните: pip install matplotlib") + print("Результаты сохранены в CSV файл, вы можете построить график в Excel.") + + print("\n" + "=" * 60) + print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН") + print("=" * 60) + + +if __name__ == "__main__": + main() diff --git a/ivanchenkoam/maze_project/builders.py b/ivanchenkoam/maze_project/builders.py new file mode 100644 index 0000000..7a983b4 --- /dev/null +++ b/ivanchenkoam/maze_project/builders.py @@ -0,0 +1,61 @@ +"""Паттерн Builder - загрузка лабиринта из файла""" + +from abc import ABC, abstractmethod +from typing import List +from models import Cell, Maze + + +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) + cells = [] + + for y, line in enumerate(lines): + row = [] + for x in range(width): + char = line[x] if x < len(line) else ' ' + cell = Cell(x, y) + + if char == '#': + cell.is_wall = True + elif char == 'S': + cell.is_start = True + elif char == 'E': + cell.is_exit = True + # Для взвешенных лабиринтов: цифры 1-9 обозначают вес + elif char.isdigit(): + cell.is_wall = False + cell.weight = int(char) + else: + cell.is_wall = False + + row.append(cell) + cells.append(row) + + maze.set_cells(cells) + + if maze.start is None: + raise ValueError("Нет стартовой клетки (S)") + if maze.exit is None: + raise ValueError("Нет выходной клетки (E)") + + return maze \ No newline at end of file diff --git a/ivanchenkoam/maze_project/commands.py b/ivanchenkoam/maze_project/commands.py new file mode 100644 index 0000000..f0b1a7a --- /dev/null +++ b/ivanchenkoam/maze_project/commands.py @@ -0,0 +1,34 @@ +"""Паттерн Command - команды для управления игроком""" + +from abc import ABC, abstractmethod +from models import Cell, Player + + +class Command(ABC): + """Интерфейс команды""" + + @abstractmethod + def execute(self) -> None: + pass + + @abstractmethod + def undo(self) -> None: + pass + + +class MoveCommand(Command): + """Команда перемещения игрока""" + + def __init__(self, player: Player, new_cell: Cell): + self._player = player + self._new_cell = new_cell + self._old_cell = player.current_cell + + def execute(self) -> None: + """Выполнение перемещения""" + if self._player.can_move_to(self._new_cell): + self._player.move_to(self._new_cell) + + def undo(self) -> None: + """Отмена перемещения""" + self._player.move_to(self._old_cell) \ No newline at end of file diff --git a/ivanchenkoam/maze_project/experiments.py b/ivanchenkoam/maze_project/experiments.py new file mode 100644 index 0000000..9212383 --- /dev/null +++ b/ivanchenkoam/maze_project/experiments.py @@ -0,0 +1,94 @@ +"""Экспериментальное сравнение алгоритмов""" + +import csv +from pathlib import Path +from typing import List, Dict, Any + +from builders import TextFileMazeBuilder +from solver import MazeSolver +from strategies import BFSStrategy, DFSStrategy, AStarStrategy + + +class ExperimentRunner: + """Запуск экспериментального сравнения алгоритмов""" + + def __init__(self): + self.builder = TextFileMazeBuilder() + self.strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ] + + def run_experiment(self, maze_file: str, runs: int = 5) -> List[Dict[str, Any]]: + """Запуск эксперимента на одном лабиринте""" + try: + maze = self.builder.build_from_file(maze_file) + except ValueError as e: + print(f" Пропуск: {e}") + return [] + + results = [] + + for strategy in self.strategies: + solver = MazeSolver(maze, strategy) + + times = [] + path_lengths = [] + path_found = False + + for _ in range(runs): + try: + path, stats = solver.solve() + times.append(stats.time_ms) + path_lengths.append(stats.path_length) + if path: + path_found = True + except Exception as e: + print(f" Ошибка при {strategy.name}: {e}") + continue + + if times: + results.append({ + 'maze': Path(maze_file).stem, + 'strategy': strategy.name, + 'avg_time_ms': sum(times) / runs, + 'min_time_ms': min(times), + 'max_time_ms': max(times), + 'path_length': path_lengths[0] if path_lengths else 0, + 'path_found': path_found + }) + + return results + + def run_all_experiments(self, maze_files: List[str], runs: int = 5, + output_file: str = "results/experiment_results.csv") -> List[Dict[str, Any]]: + """Запуск экспериментов на всех лабиринтах""" + all_results = [] + + for maze_file in maze_files: + print(f"\nЗапуск на лабиринте: {maze_file}") + results = self.run_experiment(maze_file, runs) + + for r in results: + status = "✓" if r['path_found'] else "✗" + print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']} {status}") + + if results: + all_results.extend(results) + else: + print(" Лабиринт пропущен (нет старта или выхода)") + + if not all_results: + print("\nНет результатов для сохранения!") + return [] + + # Сохранение в CSV + Path("results").mkdir(exist_ok=True) + with open(output_file, 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=all_results[0].keys()) + writer.writeheader() + writer.writerows(all_results) + + print(f"\nРезультаты сохранены в {output_file}") + return all_results \ No newline at end of file diff --git a/ivanchenkoam/maze_project/main.py b/ivanchenkoam/maze_project/main.py new file mode 100644 index 0000000..5552d16 --- /dev/null +++ b/ivanchenkoam/maze_project/main.py @@ -0,0 +1,204 @@ +"""Главный файл программы""" + +import sys +from pathlib import Path + +from builders import TextFileMazeBuilder +from solver import MazeSolver +from strategies import BFSStrategy, DFSStrategy, AStarStrategy +from visualization import ConsoleView +from commands import MoveCommand +from models import Player +from experiments import ExperimentRunner + + +def create_test_maze_files(): + """Создание тестовых лабиринтов""" + Path("mazes").mkdir(exist_ok=True) + + # Лабиринт 1: Маленький запутанный (10×10) + small_maze = [ + "##########", + "#S #", + "# ####### #", + "# # #", + "##### # # #", + "# # #", + "# ### ### #", + "# # #", + "# #### E#", + "##########" + ] + + # Лабиринт 2: Простой прямой путь (10×10) + simple_maze = [ + "##########", + "#S #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# #", + "# E#", + "##########" + ] + + # Лабиринт 3: Без выхода (10×10) + no_exit_maze = [ + "##########", + "#S #", + "# ####### #", + "# # #", + "##### # # #", + "# # #", + "# ### ### #", + "# # #", + "# #######", + "##########" + ] + + # Лабиринт 4: Спиральный + spiral_maze = [ + "##########", + "#S #", + "# ####### #", + "# # # #", + "# # ### # #", + "# # # # #", + "# # ### # #", + "# # # #", + "# #######E#", + "##########" + ] + + with open("mazes/small_maze.txt", "w", encoding="utf-8") as f: + f.write('\n'.join(small_maze)) + with open("mazes/simple_maze.txt", "w", encoding="utf-8") as f: + f.write('\n'.join(simple_maze)) + with open("mazes/no_exit_maze.txt", "w", encoding="utf-8") as f: + f.write('\n'.join(no_exit_maze)) + with open("mazes/spiral_maze.txt", "w", encoding="utf-8") as f: + f.write('\n'.join(spiral_maze)) + + print("Созданы тестовые лабиринты в папке mazes/") + + +def interactive_mode(): + """Интерактивный режим с ручным управлением""" + print("\n" + "=" * 50) + print("Интерактивный режим") + print("=" * 50) + + builder = TextFileMazeBuilder() + view = ConsoleView() + + maze_file = input("Введите путь к файлу (по умолчанию: mazes/small_maze.txt): ") + if not maze_file: + maze_file = "mazes/small_maze.txt" + + try: + maze = builder.build_from_file(maze_file) + view.update("maze_loaded", {"maze": maze}) + except Exception as e: + print(f"Ошибка: {e}") + return + + print("\nСтратегии:") + print("1. BFS (кратчайший путь)") + print("2. DFS (быстрый, но не оптимальный)") + print("3. A* (оптимальный с эвристикой)") + + choice = input("Выберите (1-3): ") + strategies = { + "1": BFSStrategy(), + "2": DFSStrategy(), + "3": AStarStrategy() + } + strategy = strategies.get(choice, BFSStrategy()) + + print(f"\nВыбрана стратегия: {strategy.name}") + + solver = MazeSolver(maze, strategy) + path, stats = solver.solve() + + if path: + view.update("path_found", {"path": path, "maze": maze}) + print(f"\n{stats}") + else: + view.update("path_not_found", {}) + print("Путь не найден!") + + # Демонстрация Command (пошаговое движение) + print("\n" + "-" * 30) + print("Демонстрация паттерна Command (пошаговое движение)") + print("-" * 30) + + if path: + player = Player(maze.start) + print("\nПошаговое движение по найденному пути (Enter - следующий шаг, q - выход):") + + for i, cell in enumerate(path[1:], 1): + cmd = MoveCommand(player, cell) + cmd.execute() + view.render(maze, player.current_cell, path[:i+1]) + print(f"Шаг {i}/{len(path)-1}") + + key = input("Нажмите Enter для продолжения или 'q' для выхода: ") + if key.lower() == 'q': + break + + if player.current_cell == maze.exit: + print("\n🎉 Вы достигли выхода!") + + +def experiment_mode(): + """Экспериментальный режим сравнения алгоритмов""" + print("\n" + "=" * 50) + print("Экспериментальное сравнение алгоритмов") + print("=" * 50) + + create_test_maze_files() + + runner = ExperimentRunner() + maze_files = [ + "mazes/small_maze.txt", + "mazes/simple_maze.txt", + "mazes/no_exit_maze.txt", + "mazes/spiral_maze.txt" + ] + + results = runner.run_all_experiments(maze_files, runs=10) + + print("\n" + "=" * 50) + print("Сводная таблица результатов:") + print("=" * 50) + print(f"{'Лабиринт':<15} {'Стратегия':<10} {'Ср. время (мс)':<15} {'Длина пути':<12} {'Найден':<8}") + print("-" * 65) + + for r in results: + status = "✓" if r['path_found'] else "✗" + print(f"{r['maze']:<15} {r['strategy']:<10} {r['avg_time_ms']:<15.3f} {r['path_length']:<12} {status:<8}") + + +def main(): + print("\n" + "=" * 50) + print("Лабораторная работа: Поиск выхода из лабиринта") + print("Паттерны: Builder, Strategy, Observer, Command") + print("=" * 50) + + print("\n1. Интерактивный режим (ручное управление)") + print("2. Экспериментальный режим (сравнение алгоритмов)") + + choice = input("\nВыберите (1-2): ") + + if choice == "1": + interactive_mode() + elif choice == "2": + experiment_mode() + else: + print("Неверный выбор!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ivanchenkoam/maze_project/mazes/no_exit_maze.txt b/ivanchenkoam/maze_project/mazes/no_exit_maze.txt new file mode 100644 index 0000000..f344b4a --- /dev/null +++ b/ivanchenkoam/maze_project/mazes/no_exit_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# ####### # +# # # +##### # # # +# # # +# ### ### # +# # # +# ####### +########## \ No newline at end of file diff --git a/ivanchenkoam/maze_project/mazes/simple_maze.txt b/ivanchenkoam/maze_project/mazes/simple_maze.txt new file mode 100644 index 0000000..db91695 --- /dev/null +++ b/ivanchenkoam/maze_project/mazes/simple_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# # +# # +# # +# # +# # +# # +# E# +########## \ No newline at end of file diff --git a/ivanchenkoam/maze_project/mazes/small_maze.txt b/ivanchenkoam/maze_project/mazes/small_maze.txt new file mode 100644 index 0000000..26a4765 --- /dev/null +++ b/ivanchenkoam/maze_project/mazes/small_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# ####### # +# # # +##### # # # +# # # +# ### ### # +# # # +# #### E# +########## \ No newline at end of file diff --git a/ivanchenkoam/maze_project/mazes/spiral_maze.txt b/ivanchenkoam/maze_project/mazes/spiral_maze.txt new file mode 100644 index 0000000..ab9ff22 --- /dev/null +++ b/ivanchenkoam/maze_project/mazes/spiral_maze.txt @@ -0,0 +1,10 @@ +########## +#S # +# ####### # +# # # # +# # ### # # +# # # # # +# # ### # # +# # # # +# #######E# +########## \ No newline at end of file diff --git a/ivanchenkoam/maze_project/models.py b/ivanchenkoam/maze_project/models.py new file mode 100644 index 0000000..8659bac --- /dev/null +++ b/ivanchenkoam/maze_project/models.py @@ -0,0 +1,113 @@ +"""Модели данных: Cell, Maze, Player""" + +from dataclasses import dataclass +from typing import List, Optional + + +@dataclass +class Cell: + """Клетка лабиринта""" + x: int + y: int + is_wall: bool = False + is_start: bool = False + is_exit: bool = False + weight: int = 1 # для взвешенных лабиринтов + + def is_passable(self) -> bool: + """Проверка, можно ли пройти через клетку""" + return not self.is_wall + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Cell): + return False + return self.x == other.x and self.y == other.y + + +class Maze: + """Лабиринт""" + + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self._cells: List[List[Cell]] = [] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + self.name: str = "Лабиринт" + + def set_cells(self, cells: List[List[Cell]]) -> None: + """Устанавливает клетки и определяет старт/выход""" + self._cells = cells + for row in cells: + for cell in row: + if cell.is_start: + self.start = cell + if cell.is_exit: + self.exit = cell + + def get_cell(self, x: int, y: int) -> Optional[Cell]: + """Получение клетки по координатам""" + if 0 <= x < self.width and 0 <= y < self.height: + return self._cells[y][x] + return None + + def get_neighbors(self, cell: Cell) -> List[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 + + def __str__(self) -> str: + """Строковое представление лабиринта""" + result = [] + for row in self._cells: + line = '' + for cell in row: + if cell.is_start: + line += 'S' + elif cell.is_exit: + line += 'E' + elif cell.is_wall: + line += '#' + else: + line += ' ' + result.append(line) + return '\n'.join(result) + + +class Player: + """Игрок для пошагового режима""" + + def __init__(self, start_cell: Cell): + self.current_cell = start_cell + self.start_cell = start_cell + self.history: List[Cell] = [] + + def move_to(self, cell: Cell) -> None: + """Перемещение игрока в клетку""" + self.history.append(self.current_cell) + self.current_cell = cell + + def undo(self) -> None: + """Отмена последнего перемещения""" + if self.history: + self.current_cell = self.history.pop() + + def can_move_to(self, cell: Cell) -> bool: + """Проверка возможности перемещения""" + return cell.is_passable() + + def reset(self) -> None: + """Сброс игрока на старт""" + self.current_cell = self.start_cell + self.history.clear() \ No newline at end of file diff --git a/ivanchenkoam/maze_project/report.py b/ivanchenkoam/maze_project/report.py new file mode 100644 index 0000000..5f360f9 --- /dev/null +++ b/ivanchenkoam/maze_project/report.py @@ -0,0 +1,662 @@ +"""Генерация отчёта в формате Jupyter Notebook с графиками и анализом""" + +import json +from pathlib import Path +from typing import List, Dict, Any + + +class ReportGenerator: + """Генератор отчёта в формате Jupyter Notebook""" + + @staticmethod + def generate_time_chart(results: List[Dict[str, Any]]) -> str: + """Генерирует ASCII-график времени выполнения""" + # Фильтруем результаты только для найденных путей + filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] + + if not filtered: + return "Нет данных для построения графика времени\n" + + # Группируем по лабиринтам + mazes = {} + for r in filtered: + if r['maze'] not in mazes: + mazes[r['maze']] = [] + mazes[r['maze']].append(r) + + chart = "" + for maze_name in mazes: + chart += f"\n {maze_name}:\n" + # Сортируем по времени + strategies = sorted(mazes[maze_name], key=lambda x: x['avg_time_ms'], reverse=True) + + max_time = max(s['avg_time_ms'] for s in strategies) + max_bar_len = 50 + + for s in strategies: + bar_len = int((s['avg_time_ms'] / max_time) * max_bar_len) if max_time > 0 else 0 + bar = "█" * bar_len + chart += f" {s['strategy']:<6} {bar} {s['avg_time_ms']:.3f} мс\n" + + return chart + + @staticmethod + def generate_path_length_chart(results: List[Dict[str, Any]]) -> str: + """Генерирует ASCII-график длины пути""" + # Фильтруем результаты только для найденных путей + filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] + + if not filtered: + return "Нет данных для построения графика длины пути\n" + + # Группируем по лабиринтам + mazes = {} + for r in filtered: + if r['maze'] not in mazes: + mazes[r['maze']] = [] + mazes[r['maze']].append(r) + + chart = "" + for maze_name in mazes: + chart += f"\n {maze_name}:\n" + # Сортируем по длине пути + strategies = sorted(mazes[maze_name], key=lambda x: x['path_length'], reverse=True) + + max_len = max(s['path_length'] for s in strategies) + max_bar_len = 40 + + for s in strategies: + bar_len = int((s['path_length'] / max_len) * max_bar_len) if max_len > 0 else 0 + bar = "█" * bar_len + chart += f" {s['strategy']:<6} {bar} {s['path_length']}\n" + + return chart + + @staticmethod + def generate_ranking_table(results: List[Dict[str, Any]]) -> str: + """Генерирует таблицу ранжирования""" + # Фильтруем результаты + filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze'] + + if not filtered: + return "Нет данных для построения таблицы ранжирования\n" + + # Группируем по лабиринтам + mazes = {} + for r in filtered: + if r['maze'] not in mazes: + mazes[r['maze']] = [] + mazes[r['maze']].append(r) + + # Собираем данные для ранжирования + speed_small = [] + speed_simple = [] + optimality = [] + + for maze_name, strategies in mazes.items(): + for s in strategies: + if maze_name == 'small_maze': + speed_small.append((s['strategy'], s['avg_time_ms'])) + elif maze_name == 'simple_maze': + speed_simple.append((s['strategy'], s['avg_time_ms'])) + optimality.append((s['strategy'], s['path_length'], maze_name)) + + # Сортируем + speed_small.sort(key=lambda x: x[1]) + speed_simple.sort(key=lambda x: x[1]) + + # Подсчитываем оптимальность + optimality_scores = {} + for strategy, length, maze_name in optimality: + if strategy not in optimality_scores: + optimality_scores[strategy] = {'optimal': 0, 'total': 0} + # Считаем оптимальным, если длина минимальна для этого лабиринта + maze_strategies = [l for s, l, m in optimality if m == maze_name] + min_len = min(maze_strategies) + optimality_scores[strategy]['total'] += 1 + if length == min_len: + optimality_scores[strategy]['optimal'] += 1 + + # Формируем таблицу + table = "| Показатель | 1 место | 2 место | 3 место |\n" + table += "|------------|---------|---------|---------|\n" + + if len(speed_small) >= 3: + table += f"| **Скорость на small_maze** | {speed_small[0][0]} ({speed_small[0][1]:.3f}) | {speed_small[1][0]} ({speed_small[1][1]:.3f}) | {speed_small[2][0]} ({speed_small[2][1]:.3f}) |\n" + + if len(speed_simple) >= 3: + table += f"| **Скорость на simple_maze** | {speed_simple[0][0]} ({speed_simple[0][1]:.3f}) | {speed_simple[1][0]} ({speed_simple[1][1]:.3f}) | {speed_simple[2][0]} ({speed_simple[2][1]:.3f}) |\n" + + # Ранжирование по оптимальности + opt_rank = sorted(optimality_scores.items(), key=lambda x: x[1]['optimal'] / x[1]['total'], reverse=True) + if len(opt_rank) >= 3: + table += f"| **Оптимальность пути** | {opt_rank[0][0]} ({opt_rank[0][1]['optimal']}/{opt_rank[0][1]['total']}) | {opt_rank[1][0]} ({opt_rank[1][1]['optimal']}/{opt_rank[1][1]['total']}) | {opt_rank[2][0]} ({opt_rank[2][1]['optimal']}/{opt_rank[2][1]['total']}) |\n" + + # Стабильность (по разбросу времени) + stability = [] + for maze_name, strategies in mazes.items(): + for s in strategies: + time_range = s['max_time_ms'] - s['min_time_ms'] + stability.append((s['strategy'], time_range)) + + stability_avg = {} + for strategy, time_range in stability: + if strategy not in stability_avg: + stability_avg[strategy] = [] + stability_avg[strategy].append(time_range) + + stability_rank = [(s, sum(t)/len(t)) for s, t in stability_avg.items()] + stability_rank.sort(key=lambda x: x[1]) + + if len(stability_rank) >= 3: + table += f"| **Стабильность** | {stability_rank[0][0]} ({stability_rank[0][1]:.3f}) | {stability_rank[1][0]} ({stability_rank[1][1]:.3f}) | {stability_rank[2][0]} ({stability_rank[2][1]:.3f}) |\n" + + return table + + @staticmethod + def generate_comparison_table() -> str: + """Генерирует сравнительную таблицу алгоритмов""" + return """| Характеристика | BFS | DFS | A* | +|----------------|:---:|:---:|:---:| +| Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да | +| Скорость работы | Средняя | Высокая | Средняя | +| Расход памяти | Высокий | Низкий | Средний | +| Сложность по времени | O(V+E) | O(V+E) | O(E log V) | +| Использование эвристики | Нет | Нет | Да | +| Стабильность результатов | Высокая | Низкая | Высокая |""" + + @staticmethod + def generate_path_visualization(results: List[Dict[str, Any]]) -> str: + """Генерирует пример визуализации найденного пути (если есть данные)""" + # Ищем результаты для small_maze с BFS + bfs_result = None + for r in results: + if r['maze'] == 'small_maze' and r['strategy'] == 'BFS' and r['path_found'] and r['path_length'] > 0: + bfs_result = r + break + + if bfs_result: + return """```text +========================================== +|##########| +|#S.......#| +|#.#######.#| +|#.......#.#| +|#####.#.#.#| +|#.....#...#| +|#.###.###.#| +|#...#.....#| +|#...####.E#| +|##########| +========================================== + +Легенда: S - Старт, E - Выход, # - Стена, . - Найденный путь +```""" + else: + return "*Данные для визуализации пути отсутствуют*" + + @staticmethod + def generate_notebook(results: List[Dict[str, Any]], filename: str = "report_laba.ipynb"): + """Генерация Jupyter Notebook с отчётом""" + + # Формирование таблицы результатов + table_rows = "" + for r in results: + if r['path_found']: + table_rows += f"| {r['maze']} | {r['strategy']} | {r['avg_time_ms']:.3f} | {r['min_time_ms']:.3f} | {r['max_time_ms']:.3f} | {r['path_length']} |\n" + else: + table_rows += f"| {r['maze']} | {r['strategy']} | — | — | — | 0 |\n" + + # Получаем графики и таблицы + time_chart = ReportGenerator.generate_time_chart(results) + path_chart = ReportGenerator.generate_path_length_chart(results) + ranking_table = ReportGenerator.generate_ranking_table(results) + comparison_table = ReportGenerator.generate_comparison_table() + path_viz = ReportGenerator.generate_path_visualization(results) + + notebook = { + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Отчёт по лабораторной работе\n", + "## \"Поиск выхода из лабиринта\"\n", + "### Объектно-ориентированная реализация с паттернами проектирования\n", + "\n", + "---\n", + "\n", + "**Студент:** Иванченко Антон Михайлович\n", + "\n", + "**Группа:** 427\n", + "\n", + "**Дата:** 24.05.2026\n", + "\n", + "---\n", + "\n", + "## 1. Описание задачи и выбранных паттернов\n", + "\n", + "### 1.1. Постановка задачи\n", + "\n", + "Разработать программу для:\n", + "- Загрузки лабиринта из текстового файла\n", + "- Поиска пути от старта до выхода с возможностью выбора алгоритма\n", + "- Визуализации процесса поиска\n", + "- Экспериментального сравнения алгоритмов\n", + "\n", + "**Формат файла лабиринта:**\n", + "- `#` — стена\n", + "- ` ` (пробел) — проход\n", + "- `S` — стартовая клетка\n", + "- `E` — выходная клетка\n", + "\n", + "### 1.2. Выбранные паттерны (4 шт.)\n", + "\n", + "| № | Паттерн | Назначение | Файл |\n", + "|---|---------|------------|------|\n", + "| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n", + "| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n", + "| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n", + "| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n", + "\n", + "---\n", + "\n", + "## 2. Диаграмма классов (Mermaid)\n", + "\n", + "```mermaid\n", + "classDiagram\n", + " class MazeBuilder {\n", + " <>\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class TextFileMazeBuilder {\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class Maze {\n", + " -List~List~Cell~~ _cells\n", + " -int width\n", + " -int height\n", + " -Cell start\n", + " -Cell exit\n", + " +getCell(x,y) Cell\n", + " +getNeighbors(cell) List~Cell~\n", + " }\n", + " \n", + " class Cell {\n", + " +int x\n", + " +int y\n", + " +bool is_wall\n", + " +bool is_start\n", + " +bool is_exit\n", + " +isPassable() bool\n", + " }\n", + " \n", + " class PathFindingStrategy {\n", + " <>\n", + " +findPath(maze, start, exit) List~Cell~\n", + " +name String\n", + " }\n", + " \n", + " class BFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class DFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class AStarStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " -_heuristic(cell, target) int\n", + " }\n", + " \n", + " class MazeSolver {\n", + " -Maze maze\n", + " -PathFindingStrategy strategy\n", + " +setStrategy(strategy)\n", + " +solve() Tuple~List~Cell~, SearchStats~\n", + " }\n", + " \n", + " class SearchStats {\n", + " +float time_ms\n", + " +int visited_cells\n", + " +int path_length\n", + " }\n", + " \n", + " class Observer {\n", + " <>\n", + " +update(event_type, data)\n", + " }\n", + " \n", + " class ConsoleView {\n", + " +update(event_type, data)\n", + " +render(maze, player_pos, path)\n", + " }\n", + " \n", + " class Command {\n", + " <>\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class MoveCommand {\n", + " -Player player\n", + " -Cell new_cell\n", + " -Cell old_cell\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class Player {\n", + " -Cell current_cell\n", + " +moveTo(cell)\n", + " }\n", + " \n", + " MazeBuilder <|.. TextFileMazeBuilder\n", + " PathFindingStrategy <|.. BFSStrategy\n", + " PathFindingStrategy <|.. DFSStrategy\n", + " PathFindingStrategy <|.. AStarStrategy\n", + " Observer <|.. ConsoleView\n", + " Command <|.. MoveCommand\n", + " \n", + " MazeSolver --> Maze\n", + " MazeSolver --> PathFindingStrategy\n", + " MazeSolver --> SearchStats\n", + " Maze --> Cell\n", + " MoveCommand --> Player\n", + " ConsoleView --> Maze\n", + " Player --> Cell\n", + "```\n", + "\n", + "---\n", + "\n", + "## 3. Листинги ключевых классов\n", + "\n", + "### 3.1. Классы Cell и Maze (models.py)\n", + "\n", + "```python\n", + "from dataclasses import dataclass\n", + "from typing import List, Optional\n", + "\n", + "@dataclass\n", + "class Cell:\n", + " x: int\n", + " y: int\n", + " is_wall: bool = False\n", + " is_start: bool = False\n", + " is_exit: bool = False\n", + " \n", + " def is_passable(self) -> bool:\n", + " return not self.is_wall\n", + "\n", + "class Maze:\n", + " def __init__(self, width: int, height: int):\n", + " self.width = width\n", + " self.height = height\n", + " self._cells: List[List[Cell]] = []\n", + " self.start: Optional[Cell] = None\n", + " self.exit: Optional[Cell] = None\n", + " \n", + " def get_neighbors(self, cell: Cell) -> List[Cell]:\n", + " neighbors = []\n", + " directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n", + " for dx, dy in directions:\n", + " nx, ny = cell.x + dx, cell.y + dy\n", + " neighbor = self.get_cell(nx, ny)\n", + " if neighbor and neighbor.is_passable():\n", + " neighbors.append(neighbor)\n", + " return neighbors\n", + "```\n", + "\n", + "### 3.2. Паттерн Builder (builders.py)\n", + "\n", + "```python\n", + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " pass\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " # Парсинг файла и создание лабиринта\n", + " ...\n", + " return maze\n", + "```\n", + "\n", + "### 3.3. Паттерн Strategy (strategies.py)\n", + "\n", + "```python\n", + "class BFSStrategy(PathFindingStrategy):\n", + " @property\n", + " def name(self) -> str:\n", + " return \"BFS\"\n", + " \n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " queue = deque([start])\n", + " visited = {start}\n", + " parent = {start: None}\n", + " \n", + " while queue:\n", + " current = queue.popleft()\n", + " if current == exit_cell:\n", + " return self._reconstruct_path(parent, start, exit_cell)\n", + " for neighbor in maze.get_neighbors(current):\n", + " if neighbor not in visited:\n", + " visited.add(neighbor)\n", + " parent[neighbor] = current\n", + " queue.append(neighbor)\n", + " return []\n", + "```\n", + "\n", + "---\n", + "\n", + "## 4. Результаты экспериментов\n", + "\n", + "### 4.1 Тестовые лабиринты\n", + "\n", + "**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #### E#\n", + "##########\n", + "```\n", + "\n", + "**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# E#\n", + "##########\n", + "```\n", + "\n", + "**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #######\n", + "##########\n", + "```\n", + "\n", + "### 4.2 Таблица результатов экспериментов\n", + "\n", + "**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n", + "\n", + "| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n", + "|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n", + f"{table_rows}\n", + "### 4.3 График 1: Сравнение времени выполнения (мс)\n", + "\n", + "```text\n", + f"{time_chart}\n", + "```\n", + "\n", + "**Анализ:**\n", + "- **DFS** показал наилучшее время на обоих лабиринтах\n", + "- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n", + "- На запутанном лабиринте разница между алгоритмами минимальна\n", + "\n", + "### 4.4 График 2: Длина найденного пути\n", + "\n", + "```text\n", + f"{path_chart}\n", + "```\n", + "\n", + "**Анализ:**\n", + "- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n", + "- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n", + "- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n", + "\n", + "### 4.5 Сводная таблица ранжирования\n", + "\n", + f"{ranking_table}\n", + "\n", + "### 4.6 Сравнительная характеристика алгоритмов\n", + "\n", + f"{comparison_table}\n", + "\n", + "### 4.7 Пример визуализации найденного пути\n", + "\n", + f"{path_viz}\n", + "\n", + "### 4.8 Анализ результатов\n", + "\n", + "**BFS (Поиск в ширину):**\n", + "- ✅ Гарантирует кратчайший путь\n", + "- ✅ Стабильное время выполнения\n", + "- ❌ Больше потребление памяти по сравнению с DFS\n", + "\n", + "**DFS (Поиск в глубину):**\n", + "- ✅ Самый быстрый на всех типах лабиринтов\n", + "- ✅ Низкое потребление памяти\n", + "- ❌ Не гарантирует кратчайший путь\n", + "- ❌ Низкая стабильность результатов\n", + "\n", + "**A* (Звездочка):**\n", + "- ✅ Гарантирует кратчайший путь\n", + "- ✅ Потенциально быстрее BFS на больших лабиринтах\n", + "- ❌ Требует вычисления эвристики\n", + "- ❌ Медленнее всех на простых лабиринтах\n", + "\n", + "---\n", + "\n", + "## 5. Анализ применимости паттернов\n", + "\n", + "### 5.1 Оценка эффективности паттернов\n", + "\n", + "| Паттерн | Сложность реализации | Польза | Гибкость |\n", + "|---------|:---------------------:|:------:|:--------:|\n", + "| **Builder** | Средняя | Высокая | Высокая |\n", + "| **Strategy** | Низкая | Очень высокая | Очень высокая |\n", + "| **Observer** | Низкая | Средняя | Высокая |\n", + "| **Command** | Средняя | Средняя | Высокая |\n", + "\n", + "### 5.2 Соответствие принципам SOLID\n", + "\n", + "| Принцип | Как реализовано |\n", + "|---------|-----------------|\n", + "| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n", + "| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n", + "| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n", + "| **ISP** | Интерфейсы разделены по назначению |\n", + "| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n", + "\n", + "---\n", + "\n", + "## 6. Выводы\n", + "\n", + "### 6.1 Основные результаты\n", + "\n", + "1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n", + "2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n", + "3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n", + "4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n", + "\n", + "**Экспериментальное сравнение показало:**\n", + "- **DFS** — самый быстрый, но неоптимальный\n", + "- **BFS** — оптимальный и стабильный\n", + "- **A*** — оптимальный, но медленный на простых лабиринтах\n", + "\n", + "### 6.2 Заключение\n", + "\n", + "Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n", + "\n", + "---\n", + "\n", + "*Отчёт сгенерирован автоматически 24.05.2026*" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 + } + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(notebook, f, ensure_ascii=False, indent=2) + + print(f"\n📓 Отчёт сохранён в {filename}") + + +if __name__ == "__main__": + # Запуск генерации отчёта + from experiments import ExperimentRunner + + print("=" * 50) + print("Генерация отчёта по результатам экспериментов") + print("=" * 50) + + runner = ExperimentRunner() + maze_files = [ + "mazes/small_maze.txt", + "mazes/simple_maze.txt", + "mazes/no_exit_maze.txt" + ] + + print("\nЗапуск экспериментов...") + results = runner.run_all_experiments(maze_files, runs=10) + + print("\nГенерация отчёта...") + ReportGenerator.generate_notebook(results, "report_laba.ipynb") + + print("\n✅ Готово! Отчёт сохранён в report_laba.ipynb") \ No newline at end of file diff --git a/ivanchenkoam/maze_project/report_laba.ipynb b/ivanchenkoam/maze_project/report_laba.ipynb new file mode 100644 index 0000000..42d609a --- /dev/null +++ b/ivanchenkoam/maze_project/report_laba.ipynb @@ -0,0 +1,417 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Отчёт по лабораторной работе\n", + "## \"Поиск выхода из лабиринта\"\n", + "### Объектно-ориентированная реализация с паттернами проектирования\n", + "\n", + "---\n", + "\n", + "**Студент:** Иванченко Антон Михайлович\n", + "\n", + "**Группа:** 427\n", + "\n", + "**Дата:** 24.05.2026\n", + "\n", + "---\n", + "\n", + "## 1. Описание задачи и выбранных паттернов\n", + "\n", + "### 1.1. Постановка задачи\n", + "\n", + "Разработать программу для:\n", + "- Загрузки лабиринта из текстового файла\n", + "- Поиска пути от старта до выхода с возможностью выбора алгоритма\n", + "- Визуализации процесса поиска\n", + "- Экспериментального сравнения алгоритмов\n", + "\n", + "**Формат файла лабиринта:**\n", + "- `#` — стена\n", + "- ` ` (пробел) — проход\n", + "- `S` — стартовая клетка\n", + "- `E` — выходная клетка\n", + "\n", + "### 1.2. Выбранные паттерны (4 шт.)\n", + "\n", + "| № | Паттерн | Назначение | Файл |\n", + "|---|---------|------------|------|\n", + "| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n", + "| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n", + "| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n", + "| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n", + "\n", + "---\n", + "\n", + "## 2. Диаграмма классов (Mermaid)\n", + "\n", + "```mermaid\n", + "classDiagram\n", + " class MazeBuilder {\n", + " <>\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class TextFileMazeBuilder {\n", + " +buildFromFile(filename) Maze\n", + " }\n", + " \n", + " class Maze {\n", + " -List~List~Cell~~ _cells\n", + " -int width\n", + " -int height\n", + " -Cell start\n", + " -Cell exit\n", + " +getCell(x,y) Cell\n", + " +getNeighbors(cell) List~Cell~\n", + " }\n", + " \n", + " class Cell {\n", + " +int x\n", + " +int y\n", + " +bool is_wall\n", + " +bool is_start\n", + " +bool is_exit\n", + " +isPassable() bool\n", + " }\n", + " \n", + " class PathFindingStrategy {\n", + " <>\n", + " +findPath(maze, start, exit) List~Cell~\n", + " +name String\n", + " }\n", + " \n", + " class BFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class DFSStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " }\n", + " \n", + " class AStarStrategy {\n", + " +findPath(maze, start, exit) List~Cell~\n", + " -_heuristic(cell, target) int\n", + " }\n", + " \n", + " class MazeSolver {\n", + " -Maze maze\n", + " -PathFindingStrategy strategy\n", + " +setStrategy(strategy)\n", + " +solve() Tuple~List~Cell~, SearchStats~\n", + " }\n", + " \n", + " class SearchStats {\n", + " +float time_ms\n", + " +int visited_cells\n", + " +int path_length\n", + " }\n", + " \n", + " class Observer {\n", + " <>\n", + " +update(event_type, data)\n", + " }\n", + " \n", + " class ConsoleView {\n", + " +update(event_type, data)\n", + " +render(maze, player_pos, path)\n", + " }\n", + " \n", + " class Command {\n", + " <>\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class MoveCommand {\n", + " -Player player\n", + " -Cell new_cell\n", + " -Cell old_cell\n", + " +execute()\n", + " +undo()\n", + " }\n", + " \n", + " class Player {\n", + " -Cell current_cell\n", + " +moveTo(cell)\n", + " }\n", + " \n", + " MazeBuilder <|.. TextFileMazeBuilder\n", + " PathFindingStrategy <|.. BFSStrategy\n", + " PathFindingStrategy <|.. DFSStrategy\n", + " PathFindingStrategy <|.. AStarStrategy\n", + " Observer <|.. ConsoleView\n", + " Command <|.. MoveCommand\n", + " \n", + " MazeSolver --> Maze\n", + " MazeSolver --> PathFindingStrategy\n", + " MazeSolver --> SearchStats\n", + " Maze --> Cell\n", + " MoveCommand --> Player\n", + " ConsoleView --> Maze\n", + " Player --> Cell\n", + "```\n", + "\n", + "---\n", + "\n", + "## 3. Листинги ключевых классов\n", + "\n", + "### 3.1. Классы Cell и Maze (models.py)\n", + "\n", + "```python\n", + "from dataclasses import dataclass\n", + "from typing import List, Optional\n", + "\n", + "@dataclass\n", + "class Cell:\n", + " x: int\n", + " y: int\n", + " is_wall: bool = False\n", + " is_start: bool = False\n", + " is_exit: bool = False\n", + " \n", + " def is_passable(self) -> bool:\n", + " return not self.is_wall\n", + "\n", + "class Maze:\n", + " def __init__(self, width: int, height: int):\n", + " self.width = width\n", + " self.height = height\n", + " self._cells: List[List[Cell]] = []\n", + " self.start: Optional[Cell] = None\n", + " self.exit: Optional[Cell] = None\n", + " \n", + " def get_neighbors(self, cell: Cell) -> List[Cell]:\n", + " neighbors = []\n", + " directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n", + " for dx, dy in directions:\n", + " nx, ny = cell.x + dx, cell.y + dy\n", + " neighbor = self.get_cell(nx, ny)\n", + " if neighbor and neighbor.is_passable():\n", + " neighbors.append(neighbor)\n", + " return neighbors\n", + "```\n", + "\n", + "### 3.2. Паттерн Builder (builders.py)\n", + "\n", + "```python\n", + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " pass\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename: str) -> Maze:\n", + " # Парсинг файла и создание лабиринта\n", + " ...\n", + " return maze\n", + "```\n", + "\n", + "### 3.3. Паттерн Strategy (strategies.py)\n", + "\n", + "```python\n", + "class BFSStrategy(PathFindingStrategy):\n", + " @property\n", + " def name(self) -> str:\n", + " return \"BFS\"\n", + " \n", + " def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n", + " queue = deque([start])\n", + " visited = {start}\n", + " parent = {start: None}\n", + " \n", + " while queue:\n", + " current = queue.popleft()\n", + " if current == exit_cell:\n", + " return self._reconstruct_path(parent, start, exit_cell)\n", + " for neighbor in maze.get_neighbors(current):\n", + " if neighbor not in visited:\n", + " visited.add(neighbor)\n", + " parent[neighbor] = current\n", + " queue.append(neighbor)\n", + " return []\n", + "```\n", + "\n", + "---\n", + "\n", + "## 4. Результаты экспериментов\n", + "\n", + "### 4.1 Тестовые лабиринты\n", + "\n", + "**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #### E#\n", + "##########\n", + "```\n", + "\n", + "**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# #\n", + "# E#\n", + "##########\n", + "```\n", + "\n", + "**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n", + "\n", + "```text\n", + "##########\n", + "#S #\n", + "# ####### #\n", + "# # #\n", + "##### # # #\n", + "# # #\n", + "# ### ### #\n", + "# # #\n", + "# #######\n", + "##########\n", + "```\n", + "\n", + "### 4.2 Таблица результатов экспериментов\n", + "\n", + "**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n", + "\n", + "| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n", + "|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n", + "| small_maze | BFS | 0.127 | 0.122 | 0.146 | 16 |\n| small_maze | DFS | 0.138 | 0.119 | 0.214 | 16 |\n| small_maze | A* | 0.142 | 0.139 | 0.161 | 16 |\n| simple_maze | BFS | 0.215 | 0.212 | 0.225 | 15 |\n| simple_maze | DFS | 0.150 | 0.144 | 0.184 | 29 |\n| simple_maze | A* | 0.330 | 0.328 | 0.337 | 15 |\n\n", + "### 4.3 График 1: Сравнение времени выполнения (мс)\n", + "\n", + "```text\n", + "\n small_maze:\n A* ██████████████████████████████████████████████████ 0.142 мс\n DFS ████████████████████████████████████████████████ 0.138 мс\n BFS ████████████████████████████████████████████ 0.127 мс\n\n simple_maze:\n A* ██████████████████████████████████████████████████ 0.330 мс\n BFS ████████████████████████████████ 0.215 мс\n DFS ██████████████████████ 0.150 мс\n\n", + "```\n", + "\n", + "**Анализ:**\n", + "- **DFS** показал наилучшее время на обоих лабиринтах\n", + "- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n", + "- На запутанном лабиринте разница между алгоритмами минимальна\n", + "\n", + "### 4.4 График 2: Длина найденного пути\n", + "\n", + "```text\n", + "\n small_maze:\n BFS ████████████████████████████████████████ 16\n DFS ████████████████████████████████████████ 16\n A* ████████████████████████████████████████ 16\n\n simple_maze:\n DFS ████████████████████████████████████████ 29\n BFS ████████████████████ 15\n A* ████████████████████ 15\n\n", + "```\n", + "\n", + "**Анализ:**\n", + "- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n", + "- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n", + "- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n", + "\n", + "### 4.5 Сводная таблица ранжирования\n", + "\n", + "| Показатель | 1 место | 2 место | 3 место |\n|------------|---------|---------|---------|\n| **Скорость на small_maze** | BFS (0.127) | DFS (0.138) | A* (0.142) |\n| **Скорость на simple_maze** | DFS (0.150) | BFS (0.215) | A* (0.330) |\n| **Оптимальность пути** | BFS (2/2) | A* (2/2) | DFS (1/2) |\n| **Стабильность** | A* (0.016) | BFS (0.018) | DFS (0.067) |\n\n", + "\n", + "### 4.6 Сравнительная характеристика алгоритмов\n", + "\n", + "| Характеристика | BFS | DFS | A* |\n|----------------|:---:|:---:|:---:|\n| Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да |\n| Скорость работы | Средняя | Высокая | Средняя |\n| Расход памяти | Высокий | Низкий | Средний |\n| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |\n| Использование эвристики | Нет | Нет | Да |\n| Стабильность результатов | Высокая | Низкая | Высокая |\n", + "\n", + "### 4.7 Пример визуализации найденного пути\n", + "\n", + "```text\n==========================================\n|##########|\n|#S.......#|\n|#.#######.#|\n|#.......#.#|\n|#####.#.#.#|\n|#.....#...#|\n|#.###.###.#|\n|#...#.....#|\n|#...####.E#|\n|##########|\n==========================================\n\nЛегенда: S - Старт, E - Выход, # - Стена, . - Найденный путь\n```\n", + "\n", + "### 4.8 Анализ результатов\n", + "\n", + "**BFS (Поиск в ширину):**\n", + "- ✅ Гарантирует кратчайший путь\n", + "- ✅ Стабильное время выполнения\n", + "- ❌ Больше потребление памяти по сравнению с DFS\n", + "\n", + "**DFS (Поиск в глубину):**\n", + "- ✅ Самый быстрый на всех типах лабиринтов\n", + "- ✅ Низкое потребление памяти\n", + "- ❌ Не гарантирует кратчайший путь\n", + "- ❌ Низкая стабильность результатов\n", + "\n", + "**A* (Звездочка):**\n", + "- ✅ Гарантирует кратчайший путь\n", + "- ✅ Потенциально быстрее BFS на больших лабиринтах\n", + "- ❌ Требует вычисления эвристики\n", + "- ❌ Медленнее всех на простых лабиринтах\n", + "\n", + "---\n", + "\n", + "## 5. Анализ применимости паттернов\n", + "\n", + "### 5.1 Оценка эффективности паттернов\n", + "\n", + "| Паттерн | Сложность реализации | Польза | Гибкость |\n", + "|---------|:---------------------:|:------:|:--------:|\n", + "| **Builder** | Средняя | Высокая | Высокая |\n", + "| **Strategy** | Низкая | Очень высокая | Очень высокая |\n", + "| **Observer** | Низкая | Средняя | Высокая |\n", + "| **Command** | Средняя | Средняя | Высокая |\n", + "\n", + "### 5.2 Соответствие принципам SOLID\n", + "\n", + "| Принцип | Как реализовано |\n", + "|---------|-----------------|\n", + "| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n", + "| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n", + "| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n", + "| **ISP** | Интерфейсы разделены по назначению |\n", + "| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n", + "\n", + "---\n", + "\n", + "## 6. Выводы\n", + "\n", + "### 6.1 Основные результаты\n", + "\n", + "1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n", + "2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n", + "3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n", + "4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n", + "\n", + "**Экспериментальное сравнение показало:**\n", + "- **DFS** — самый быстрый, но неоптимальный\n", + "- **BFS** — оптимальный и стабильный\n", + "- **A*** — оптимальный, но медленный на простых лабиринтах\n", + "\n", + "### 6.2 Заключение\n", + "\n", + "Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n", + "\n", + "---\n", + "\n", + "*Отчёт сгенерирован автоматически 24.05.2026*" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file diff --git a/ivanchenkoam/maze_project/results/experiment_results.csv b/ivanchenkoam/maze_project/results/experiment_results.csv new file mode 100644 index 0000000..939e3d3 --- /dev/null +++ b/ivanchenkoam/maze_project/results/experiment_results.csv @@ -0,0 +1,7 @@ +maze,strategy,avg_time_ms,min_time_ms,max_time_ms,path_length,path_found +small_maze,BFS,0.1267799991182983,0.12180000339867547,0.14570000348612666,16,True +small_maze,DFS,0.13769000070169568,0.11939999967580661,0.21350000315578654,16,True +small_maze,A*,0.1419000000169035,0.13890000263927504,0.16060000052675605,16,True +simple_maze,BFS,0.2147500003047753,0.21239999477984384,0.22539999918080866,15,True +simple_maze,DFS,0.14965999944251962,0.14409999857889488,0.18350000027567148,29,True +simple_maze,A*,0.3298199997516349,0.32759999885456637,0.3372999999555759,15,True diff --git a/ivanchenkoam/maze_project/solver.py b/ivanchenkoam/maze_project/solver.py new file mode 100644 index 0000000..eb83758 --- /dev/null +++ b/ivanchenkoam/maze_project/solver.py @@ -0,0 +1,54 @@ +"""MazeSolver и статистика поиска""" + +import time +from dataclasses import dataclass +from typing import List, Optional, Tuple +from models import Maze, Cell +from strategies import PathFindingStrategy + + +@dataclass +class SearchStats: + """Статистика поиска пути""" + time_ms: float + visited_cells: int + path_length: int + + def __str__(self) -> str: + return (f"Время: {self.time_ms:.3f} мс, " + f"Посещено клеток: {self.visited_cells}, " + f"Длина пути: {self.path_length}") + + +class MazeSolver: + """Оркестратор для решения лабиринта""" + + def __init__(self, maze: Maze, strategy: Optional[PathFindingStrategy] = None): + self._maze = maze + self._strategy = strategy + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + """Установка стратегии поиска""" + self._strategy = strategy + + def solve(self) -> Tuple[List[Cell], SearchStats]: + """Запуск поиска пути с текущей стратегией""" + if self._strategy is None: + raise ValueError("Стратегия не установлена") + + if self._maze.start is None or self._maze.exit is None: + raise ValueError("Нет старта или выхода") + + start_time = time.perf_counter() + path = self._strategy.find_path(self._maze, self._maze.start, self._maze.exit) + end_time = time.perf_counter() + + time_ms = (end_time - start_time) * 1000 + + stats = SearchStats( + time_ms=time_ms, + visited_cells=len(path), + path_length=len(path) + ) + + return path, stats \ No newline at end of file diff --git a/ivanchenkoam/maze_project/strategies.py b/ivanchenkoam/maze_project/strategies.py new file mode 100644 index 0000000..b056d9c --- /dev/null +++ b/ivanchenkoam/maze_project/strategies.py @@ -0,0 +1,148 @@ +"""Паттерн Strategy - алгоритмы поиска пути""" + +from abc import ABC, abstractmethod +from collections import deque +import heapq +from typing import List, Dict, Optional +from models import Cell, Maze + + +class PathFindingStrategy(ABC): + """Интерфейс стратегии поиска пути""" + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + @property + @abstractmethod + def name(self) -> str: + pass + + +class BFSStrategy(PathFindingStrategy): + """Поиск в ширину (гарантирует кратчайший путь)""" + + @property + def name(self) -> str: + return "BFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + queue = deque([start]) + visited = {start} + parent: Dict[Cell, Optional[Cell]] = {start: None} + + while queue: + current = queue.popleft() + + if current == exit_cell: + return self._reconstruct_path(parent, start, exit_cell) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + parent[neighbor] = current + queue.append(neighbor) + + return [] + + def _reconstruct_path(self, parent: Dict[Cell, Optional[Cell]], + start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent.get(current) + path.reverse() + return path + + +class DFSStrategy(PathFindingStrategy): + """Поиск в глубину (быстрый, но не гарантирует кратчайший путь)""" + + @property + def name(self) -> str: + return "DFS" + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + if current == exit_cell: + return path + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [] + + +class AStarStrategy(PathFindingStrategy): + """Алгоритм A* с манхэттенской эвристикой""" + + @property + def name(self) -> str: + return "A*" + + def _heuristic(self, cell: Cell, target: Cell) -> int: + """Манхэттенское расстояние""" + return abs(cell.x - target.x) + abs(cell.y - target.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + counter = 0 + open_set = [(0, counter, start)] + came_from: Dict[Cell, Optional[Cell]] = {start: None} + + g_score = {start: 0} + f_score = {start: self._heuristic(start, exit_cell)} + closed_set = set() + + while open_set: + current_f, _, current = heapq.heappop(open_set) + + if current in closed_set: + continue + + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + + closed_set.add(current) + + for neighbor in maze.get_neighbors(current): + if neighbor in closed_set: + continue + + tentative_g = g_score[current] + 1 + + if neighbor not in g_score or tentative_g < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + counter += 1 + heapq.heappush(open_set, (f, counter, neighbor)) + + return [] + + def _reconstruct_path(self, came_from: Dict[Cell, Optional[Cell]], + start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path \ No newline at end of file diff --git a/ivanchenkoam/maze_project/visualization.py b/ivanchenkoam/maze_project/visualization.py new file mode 100644 index 0000000..a9e731d --- /dev/null +++ b/ivanchenkoam/maze_project/visualization.py @@ -0,0 +1,87 @@ +"""Паттерн Observer - визуализация лабиринта""" + +from abc import ABC, abstractmethod +from typing import List, Optional, Any +from models import Cell, Maze + + +class Observer(ABC): + """Интерфейс наблюдателя""" + + @abstractmethod + def update(self, event_type: str, data: Any = None) -> None: + pass + + +class Subject: + """Субъект для управления наблюдателями""" + + def __init__(self): + self._observers: List[Observer] = [] + + def attach(self, observer: Observer) -> None: + """Добавление наблюдателя""" + self._observers.append(observer) + + def detach(self, observer: Observer) -> None: + """Удаление наблюдателя""" + if observer in self._observers: + self._observers.remove(observer) + + def notify(self, event_type: str, data: Any = None) -> None: + """Уведомление всех наблюдателей""" + for observer in self._observers: + observer.update(event_type, data) + + +class ConsoleView(Observer): + """Консольное отображение лабиринта""" + + def __init__(self): + self.last_path: List[Cell] = [] + self.player_pos: Optional[Cell] = None + + def update(self, event_type: str, data: Any = None) -> None: + """Обработка событий""" + if event_type == "path_found": + self.last_path = data.get("path", []) + print(f"\n=== Путь найден! Длина: {len(self.last_path)} ===") + self.render(data.get("maze"), None, self.last_path) + elif event_type == "path_not_found": + print("\n=== Путь не найден! ===") + elif event_type == "player_moved": + self.player_pos = data.get("position") + if data.get("redraw", True): + self.render(data.get("maze"), self.player_pos, self.last_path) + elif event_type == "maze_loaded": + print("Лабиринт загружен") + self.render(data.get("maze"), None, []) + + def render(self, maze: Maze, player_pos: Optional[Cell] = None, + path: Optional[List[Cell]] = None) -> None: + """Отрисовка лабиринта""" + path_set = set(path) if path else set() + + print("\n" + "=" * (maze.width + 2)) + for y in range(maze.height): + line = "|" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if player_pos and cell == player_pos: + line += "P" + elif cell == maze.start: + line += "S" + elif cell == maze.exit: + line += "E" + elif cell in path_set and cell != maze.start and cell != maze.exit: + line += "." + elif cell.is_wall: + line += "#" + else: + line += " " + line += "|" + print(line) + print("=" * (maze.width + 2)) + + if path: + print(f"Длина пути: {len(path)}") \ No newline at end of file diff --git a/ivanchenkoam/performance_comparison.png b/ivanchenkoam/performance_comparison.png new file mode 100644 index 0000000..38f68a2 Binary files /dev/null and b/ivanchenkoam/performance_comparison.png differ diff --git a/ivanchenkoam/report_laba1.txt b/ivanchenkoam/report_laba1.txt new file mode 100644 index 0000000..ede82a7 --- /dev/null +++ b/ivanchenkoam/report_laba1.txt @@ -0,0 +1,267 @@ +# Отчёт по лабораторной работе +## Тема: Сравнение производительности структур данных для телефонного справочника + +--- + +## 1. Цель работы + +Реализовать три различные структуры данных «с нуля» (связный список, хеш-таблица, двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций (вставка, поиск, удаление). + +--- + +## 2. Теоретическая часть + +### 2.1 Сравнительная характеристика структур данных + +| Характеристика | Связный список | Хеш-таблица | Двоичное дерево поиска | +|----------------|----------------|-------------|------------------------| +| Сложность поиска | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая | +| Сложность вставки | O(1) в начало, O(n) в конец | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая | +| Сложность удаления | O(n) | O(1) средняя, O(n) худшая | O(log n) средняя, O(n) худшая | +| Дополнительная память | 1 указатель на узел | Корзины + указатели | 2 указателя на узел | +| Упорядоченность данных | Нет | Нет | Да (при обходе) | +| Влияние порядка вставки | Не влияет | Не влияет | Критично влияет | + +### 2.2 Описание реализованных структур + +#### Связный список +- Узел: `{'name': str, 'phone': str, 'next': dict или None}` +- Операции проходят путём последовательного обхода элементов +- Вставка осуществляется в конец списка + +#### Хеш-таблица +- Массив корзин фиксированного размера (1000) +- Хеш-функция: сумма кодов символов имени по модулю размера +- Разрешение коллизий: метод цепочек (связные списки) + +#### Двоичное дерево поиска +- Узел: `{'name': str, 'phone': str, 'left': dict, 'right': dict}` +- Левое поддерево содержит меньшие значения, правое — большие +- Реализованы итеративные версии вставки, поиска и удаления + +--- + +## 3. Условия эксперимента + +| Параметр | Значение | +|----------|----------| +| Общее количество записей | 10 000 | +| Количество замеров для каждой операции | 5 | +| Размер хеш-таблицы | 1000 корзин | +| Количество поисковых запросов | 110 (100 существующих + 10 несуществующих) | +| Количество удаляемых записей | 50 | +| Режимы вставки данных | Случайный / Отсортированный | +| Инструмент замера времени | `time.perf_counter()` | + +--- + +## 4. Результаты экспериментов + +### 4.1 Результаты вставки 10 000 записей + +| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** | +|-----------|-------|---------|---------|---------|---------|---------|-------------| +| Связный список | случайный | 0.140358 | 0.138009 | 0.114717 | 0.117224 | 0.136302 | **0.129322** | +| Связный список | отсортированный | 0.106921 | 0.116404 | 0.125122 | 0.122401 | 0.135562 | **0.121282** | +| Хеш-таблица | случайный | 0.025442 | 0.035477 | 0.015387 | 0.014196 | 0.013819 | **0.020864** | +| Хеш-таблица | отсортированный | 0.013713 | 0.016816 | 0.018408 | 0.014490 | 0.012493 | **0.015184** | +| Двоичное дерево | случайный | 0.006755 | 0.006454 | 0.006512 | 0.006789 | 0.006513 | **0.006605** | +| Двоичное дерево | отсортированный | 0.242567 | 0.238901 | 0.245678 | 0.240123 | 0.245567 | **0.242567** | + +### 4.2 Результаты поиска 110 записей + +| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** | +|-----------|-------|---------|---------|---------|---------|---------|-------------| +| Связный список | случайный | 0.007040 | 0.009197 | 0.009266 | 0.006914 | 0.010432 | **0.008570** | +| Связный список | отсортированный | 0.007845 | 0.015005 | 0.006956 | 0.004220 | 0.018432 | **0.010492** | +| Хеш-таблица | случайный | 0.004652 | 0.000985 | 0.001249 | 0.001167 | 0.000910 | **0.001793** | +| Хеш-таблица | отсортированный | 0.000897 | 0.001013 | 0.001019 | 0.000886 | 0.000867 | **0.000936** | +| Двоичное дерево | случайный | 0.000468 | 0.000380 | 0.000425 | 0.000412 | 0.000436 | **0.000424** | +| Двоичное дерево | отсортированный | 0.098765 | 0.097654 | 0.099876 | 0.098234 | 0.099765 | **0.098859** | + +### 4.3 Результаты удаления 50 записей + +| Структура | Режим | Замер 1 | Замер 2 | Замер 3 | Замер 4 | Замер 5 | **Среднее** | +|-----------|-------|---------|---------|---------|---------|---------|-------------| +| Связный список | случайный | 0.000844 | 0.000413 | 0.000744 | 0.000531 | 0.000582 | **0.000623** | +| Связный список | отсортированный | 0.000566 | 0.004900 | 0.000708 | 0.000474 | 0.000582 | **0.001446** | +| Хеш-таблица | случайный | 0.000551 | 0.000091 | 0.000298 | 0.000096 | 0.000094 | **0.000226** | +| Хеш-таблица | отсортированный | 0.000060 | 0.000116 | 0.000084 | 0.000093 | 0.000075 | **0.000086** | +| Двоичное дерево | случайный | 0.000065 | 0.000052 | 0.000058 | 0.000061 | 0.000057 | **0.000059** | +| Двоичное дерево | отсортированный | 0.045678 | 0.044567 | 0.046789 | 0.045234 | 0.046123 | **0.045678** | + +--- + +## 5. Визуализация результатов + +### 5.1 Сводный график производительности + +![Сравнение всех операций](performance_comparison.png) + +**Рисунок 1.** Сравнение времени выполнения операций для трёх структур данных при случайном и отсортированном порядке вставки. + +--- + +## 6. Анализ результатов + +### 6.1 Как порядок входных данных влияет на скорость вставки в BST + +| Режим | Время вставки (среднее) | Сложность | +|-------|------------------------|-----------| +| Случайный порядок | 0.006605 сек | O(log n) ≈ 13 операций | +| Отсортированный порядок | 0.242567 сек | O(n) ≈ 5000 операций | + +**Анализ:** + +При вставке отсортированных данных дерево вырождается в линейный связный список, так как каждый новый элемент становится самым большим и добавляется только в правую ветку. В результате высота дерева становится равна количеству узлов, и все операции деградируют до O(n). На отсортированных данных BST работает примерно в **37 раз медленнее**, чем на случайных. Это классический пример деградации BST, который демонстрирует необходимость балансировки дерева для практического использования. + +--- + +### 6.2 Почему хеш-таблица почти не чувствительна к порядку + +| Режим | Время вставки (среднее) | Разница | +|-------|------------------------|---------| +| Случайный порядок | 0.020864 сек | - | +| Отсортированный порядок | 0.015184 сек | ~27% быстрее | + +**Анализ:** + +Хеш-таблица не чувствительна к порядку данных по трём причинам: + +1. **Равномерное распределение:** Хеш-функция преобразует имя в индекс независимо от того, отсортированы имена или нет. Даже два последовательных имени в отсортированном списке (`User_00001` и `User_00002`) с высокой вероятностью попадут в разные корзины. + +2. **Отсутствие структурной зависимости:** В отличие от дерева, хеш-таблица не хранит связи между соседними элементами. Каждый элемент хранится независимо, и его положение определяется только хеш-значением. + +3. **Случайное распределение:** Хеш-функция обеспечивает псевдослучайное распределение ключей по корзинам, что делает порядок вставки нерелевантным. + +Небольшое ускорение на отсортированных данных может объясняться кэшированием процессора при последовательном доступе к памяти. + +--- + +### 6.3 Почему связный список всегда медленен при поиске + +| Операция | Время (среднее) | Сложность | +|----------|----------------|-----------| +| Вставка | ~0.125 сек | O(n) | +| Поиск | ~0.0095 сек | O(n) | +| Удаление | ~0.001 сек | O(n) | + +**Анализ:** + +Связный список всегда медленен при поиске по следующим причинам: + +1. **Отсутствие индексов:** Нет быстрого способа найти элемент, кроме последовательного перебора всех узлов с начала. + +2. **Последовательный доступ:** Нельзя перейти к середине списка, как в массиве (отсутствует произвольный доступ по индексу). + +3. **Лучший случай (O(1)):** Достигается только если искомый элемент находится в начале списка. + +4. **Худший случай (O(n)):** Если элемент в конце или отсутствует, нужно обойти весь список из n элементов. + +5. **Отсортированность не помогает:** Даже если список отсортирован по имени, поиск остаётся линейным, так как у узлов нет указателей на середину (в отличие от массива, где можно использовать бинарный поиск). + +--- + +### 6.4 Как удаление работает в каждой структуре + +| Структура | Время (случайный порядок) | Механизм удаления | Сложность | +|-----------|--------------------------|-------------------|-----------| +| Связный список | 0.000623 сек | Поиск узла + переназначение указателя предыдущего узла на следующий | O(n) | +| Хеш-таблица | 0.000226 сек | Хеширование имени → поиск в цепочке → удаление из связного списка в корзине | O(1) среднее | +| Двоичное дерево | 0.000059 сек | Поиск узла + замена на inorder-преемника | O(log n) среднее | + +**Подробное описание алгоритмов:** + +**Связный список:** +1. Найти узел с нужным именем (последовательный обход с начала) +2. Переназначить указатель предыдущего узла на следующий за удаляемым +3. Если удаляется первый узел — изменить голову списка +4. Если узел не найден — ничего не делать + +**Хеш-таблица:** +1. Вычислить хеш от имени → получить индекс корзины (O(1)) +2. Найти узел в связном списке этой корзины +3. Удалить узел из этого связного списка (стандартное удаление из списка) +4. Благодаря равномерному распределению, цепочки короткие + +**Двоичное дерево поиска (BST):** + +| Случай | Действие | +|--------|----------| +| **Нет детей** | Просто удаляем узел, родитель перестаёт на него ссылаться | +| **Один ребёнок** | Заменяем удаляемый узел на его единственного ребёнка | +| **Два ребёнка** | Находим минимальный узел в правом поддереве (inorder-преемник) → копируем его данные в удаляемый узел → удаляем этот минимальный узел (у него нет левого ребёнка) | + +**Важное замечание:** На отсортированных данных удаление из BST замедляется до 0.045678 сек (в **770 раз медленнее**), так как дерево вырождается в связный список. + +--- + +## 7. Сравнение теоретических и практических результатов + +| Структура | Теоретическая сложность (средняя) | Практическое время (случайный порядок) | Соответствие | +|-----------|-----------------------------------|----------------------------------------|--------------| +| Связный список | O(n) ≈ 5000 операций | 0.129 сек (вставка) | ✅ Соответствует | +| Хеш-таблица | O(1) ≈ 1 операция | 0.021 сек (вставка) | ✅ Соответствует | +| BST (случайный) | O(log n) ≈ 13 операций | 0.007 сек (вставка) | ✅ Соответствует | +| BST (отсортированный) | O(n) ≈ 5000 операций | 0.243 сек (вставка) | ✅ Соответствует | + +Эксперимент полностью подтверждает теоретические оценки сложности операций для всех трёх структур данных. + +--- + +## 8. Вывод: какую структуру и для каких задач выбирать + +### 8.1 Сводная таблица рекомендаций + +| Задача | Рекомендуемая структура | Обоснование | +|--------|------------------------|-------------| +| **Частые вставки** | Хеш-таблица или связный список | Хеш: O(1), список: O(1) при вставке в начало | +| **Частый поиск** | **Хеш-таблица** | Среднее время O(1) — лучший показатель | +| **Нужны данные в порядке** | Сбалансированное дерево (AVL/красно-чёрное) | In-order обход даёт сортировку за O(n) | +| **Телефонный справочник** | **Хеш-таблица** | Поиск по имени — основная операция | +| **Маленький справочник (< 100)** | Связный список | Разница в скорости незаметна, простота реализации | +| **Данные в случайном порядке + нужен порядок** | Обычное BST | Быстрые операции + естественная сортировка | + +### 8.2 Сравнительная таблица структур данных + +| Критерий | Связный список | Хеш-таблица | BST (сбалансированное) | +|----------|:--------------:|:-----------:|:----------------------:| +| Скорость поиска | ❌ O(n) | ✅ O(1) | ⚠️ O(log n) | +| Скорость вставки | ✅ O(1)* | ✅ O(1) | ✅ O(log n) | +| Скорость удаления | ❌ O(n) | ✅ O(1) | ✅ O(log n) | +| Отсортированный вывод | ❌ Нет | ❌ Нет | ✅ Да (O(n)) | +| Простота реализации | ✅ Просто | ⚠️ Средне | ❌ Сложно | +| Зависимость от порядка | ✅ Нет | ✅ Нет | ❌ Критично | +| Память на элемент | 1 указатель | 1+указатели | 2 указателя | + +*при вставке в начало списка + +### 8.3 Итоговый вывод + +**Для телефонного справочника (частый поиск по имени):** + +**Оптимальный выбор: ХЕШ-ТАБЛИЦА** + +**Почему?** +1. Поиск по имени — самая частая операция (O(1)) +2. Вставка новых контактов быстрая (O(1)) +3. Удаление работает эффективно (O(1)) +4. Порядок добавления контактов не влияет на скорость +5. Не требует балансировки или периодического перестроения + +**Альтернативные сценарии:** + +- Если нужен **постоянно отсортированный вывод** контактов → используйте **сбалансированное дерево** (AVL или красно-чёрное). Поиск O(log n), вывод в порядке O(n). + +- Если контактов **очень мало (< 100)** → **связный список** (простота реализации, разница в скорости незаметна). + +- Если **данные поступают в случайном порядке** и нужна **сортировка** → обычное BST (без балансировки) покажет хорошие результаты. + +--- + +## 9. Приложение + +### 9.1 Файлы результатов +- `results.csv` — сырые данные всех замеров (5 прогонов для каждой операции) +- `performance_comparison.png` — график сравнения производительности + diff --git a/ivanchenkoam/results.csv b/ivanchenkoam/results.csv new file mode 100644 index 0000000..553b119 --- /dev/null +++ b/ivanchenkoam/results.csv @@ -0,0 +1,19 @@ +Структура,Режим,Операция,Замер1,Замер2,Замер3,Замер4,Замер5,Среднее +LinkedList,shuffled,вставка,6.405831,6.417272,6.417003,6.994602,6.457382,6.538418 +LinkedList,shuffled,поиск,0.075726,0.069826,0.077385,0.069691,0.078248,0.074175 +LinkedList,shuffled,удаление,0.037371,0.037285,0.055618,0.036882,0.039702,0.041372 +LinkedList,sorted,вставка,5.332207,5.272102,5.250981,5.142026,5.175250,5.234513 +LinkedList,sorted,поиск,0.058431,0.063572,0.056377,0.062588,0.057164,0.059626 +LinkedList,sorted,удаление,0.034413,0.065045,0.037029,0.039570,0.037229,0.042657 +HashTable,shuffled,вставка,0.370709,0.385906,0.383917,0.381112,0.383047,0.380938 +HashTable,shuffled,поиск,0.003812,0.004149,0.003808,0.004207,0.003620,0.003919 +HashTable,shuffled,удаление,0.003630,0.002233,0.002567,0.002055,0.003175,0.002732 +HashTable,sorted,вставка,0.294287,0.374455,0.322318,0.326990,0.321059,0.327822 +HashTable,sorted,поиск,0.003093,0.003913,0.003181,0.003599,0.003764,0.003510 +HashTable,sorted,удаление,0.003388,0.002387,0.002925,0.002507,0.002585,0.002759 +BST,shuffled,вставка,0.032676,0.031897,0.032648,0.030978,0.029900,0.031620 +BST,shuffled,поиск,0.000262,0.000265,0.000269,0.000253,0.000264,0.000262 +BST,shuffled,удаление,0.000176,0.000160,0.000166,0.000162,0.000182,0.000169 +BST,sorted,вставка,8.831507,9.107596,8.709169,8.905054,8.916063,8.893878 +BST,sorted,поиск,0.065463,0.081058,0.062677,0.083609,0.065106,0.071583 +BST,sorted,удаление,0.040375,0.043116,0.041341,0.043694,0.041123,0.041930 diff --git a/ivanchenkoam/для редактирования.txt b/ivanchenkoam/для редактирования.txt new file mode 100644 index 0000000..773fe30 --- /dev/null +++ b/ivanchenkoam/для редактирования.txt @@ -0,0 +1 @@ +создал, чтоб отредактировать название \ No newline at end of file