diff --git a/osipovamd/docs/Task1.docx b/osipovamd/docs/Task1.docx new file mode 100644 index 0000000..0ff50ec Binary files /dev/null and b/osipovamd/docs/Task1.docx differ diff --git a/osipovamd/docs/results.csv b/osipovamd/docs/results.csv new file mode 100644 index 0000000..0543058 --- /dev/null +++ b/osipovamd/docs/results.csv @@ -0,0 +1,19 @@ +Структура,Режим,Операция,Замер1,Замер2,Замер3,Среднее +LinkedList,случайный,вставка,0.017595,0.018368,0.016781,0.017581 +LinkedList,случайный,поиск,0.002300,0.002235,0.002287,0.002274 +LinkedList,случайный,удаление,0.000868,0.000819,0.000819,0.000835 +LinkedList,отсортированный,вставка,0.014638,0.014335,0.014226,0.014400 +LinkedList,отсортированный,поиск,0.001955,0.001930,0.001897,0.001928 +LinkedList,отсортированный,удаление,0.000975,0.000984,0.000998,0.000986 +HashTable,случайный,вставка,0.001593,0.001598,0.001441,0.001544 +HashTable,случайный,поиск,0.000180,0.000156,0.000154,0.000163 +HashTable,случайный,удаление,0.000070,0.000068,0.000069,0.000069 +HashTable,отсортированный,вставка,0.001369,0.001375,0.001383,0.001376 +HashTable,отсортированный,поиск,0.000168,0.000160,0.000148,0.000159 +HashTable,отсортированный,удаление,0.000080,0.000077,0.000076,0.000078 +BST,случайный,вставка,0.000513,0.000501,0.000514,0.000509 +BST,случайный,поиск,0.000077,0.000059,0.000054,0.000063 +BST,случайный,удаление,0.000044,0.000042,0.000039,0.000042 +BST,отсортированный,вставка,0.019008,0.018696,0.018868,0.018857 +BST,отсортированный,поиск,0.001828,0.001813,0.001811,0.001817 +BST,отсортированный,удаление,0.001735,0.001842,0.001617,0.001731 diff --git a/osipovamd/docs/task1.py b/osipovamd/docs/task1.py new file mode 100644 index 0000000..d5fa661 --- /dev/null +++ b/osipovamd/docs/task1.py @@ -0,0 +1,481 @@ +import time +import random +import csv +import matplotlib.pyplot as plt +import numpy as np +import sys +from collections import defaultdict + +# Увеличиваем лимит рекурсии для BST +sys.setrecursionlimit(10000) + + +def ll_insert(head, name, phone): + current = head + while current: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + new_node = {'name': name, 'phone': phone, 'next': head} + return new_node + +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 not head: + return None + if head['name'] == name: + return head['next'] + current = head + while current['next']: + if current['next']['name'] == name: + current['next'] = current['next']['next'] + return head + 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(c) for c 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 bucket in buckets: + current = bucket + 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): + """Итеративная вставка для избежания RecursionError""" + 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 + current = current['left'] + elif name > current['name']: + if current['right'] is None: + current['right'] = new_node + break + current = current['right'] + else: + current['phone'] = phone + break + + return root + +def bst_find(root, name): + current = root + while current: + if name == current['name']: + return current['phone'] + elif name < current['name']: + current = current['left'] + else: + current = current['right'] + return None + +def bst_find_min(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'] + + min_node = bst_find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + + return root + +def bst_list_all(root): + records = [] + stack = [] + current = root + + while stack or current: + while current: + stack.append(current) + current = current['left'] + current = stack.pop() + records.append((current['name'], current['phone'])) + current = current['right'] + + return records + +def copy_linked_list(head): + if not head: + return None + new_head = {'name': head['name'], 'phone': head['phone'], 'next': None} + current_new = new_head + current_old = head['next'] + while current_old: + current_new['next'] = {'name': current_old['name'], 'phone': current_old['phone'], 'next': None} + current_new = current_new['next'] + current_old = current_old['next'] + return new_head + +def copy_bst(node): + if not node: + return None + return { + 'name': node['name'], + 'phone': node['phone'], + 'left': copy_bst(node['left']), + 'right': copy_bst(node['right']) + } + + +def generate_test_data(N=10000): + + names = [f"User_{i:05d}" for i in range(N)] + records = [(name, f"+7-999-{random.randint(1000000, 9999999)}") for name in names] + + records_shuffled = records.copy() + random.shuffle(records_shuffled) + + records_sorted = sorted(records, key=lambda x: x[0]) + + return records_shuffled, records_sorted + +def get_test_queries(records, num_existing=100, num_nonexisting=10): + existing_names = [name for name, _ in random.sample(records, min(num_existing, len(records)))] + nonexisting_names = [f"None_{i:05d}" for i in range(num_nonexisting)] + + queries = existing_names + nonexisting_names + random.shuffle(queries) + + return queries + +def get_delete_names(records, num_to_delete=50): + return [name for name, _ in random.sample(records, min(num_to_delete, len(records)))] + + +def measure_insertion(structure_type, records, repeats=3): + times = [] + + for _ in range(repeats): + if structure_type == "LinkedList": + structure = None + insert_func = ll_insert + elif structure_type == "HashTable": + structure = ht_create(2000) + insert_func = ht_insert + elif structure_type == "BST": + structure = None + insert_func = bst_insert + else: + raise ValueError(f"Unknown structure: {structure_type}") + + start = time.perf_counter() + + for name, phone in records: + if structure_type == "HashTable": + insert_func(structure, name, phone) + else: + structure = insert_func(structure, name, phone) + + end = time.perf_counter() + times.append(end - start) + + return times + +def measure_search(structure_type, structure, queries, repeats=3): + times = [] + + for _ in range(repeats): + start = time.perf_counter() + + for name in queries: + if structure_type == "LinkedList": + ll_find(structure, name) + elif structure_type == "HashTable": + ht_find(structure, name) + elif structure_type == "BST": + bst_find(structure, name) + + end = time.perf_counter() + times.append(end - start) + + return times + +def measure_deletion(structure_type, structure, names_to_delete, repeats=3): + times = [] + + for _ in range(repeats): + if structure_type == "LinkedList": + temp_structure = copy_linked_list(structure) + delete_func = ll_delete + + elif structure_type == "HashTable": + temp_structure = structure.copy() + for i in range(len(temp_structure)): + if temp_structure[i]: + temp_structure[i] = copy_linked_list(temp_structure[i]) + delete_func = ht_delete + + elif structure_type == "BST": + temp_structure = copy_bst(structure) + delete_func = bst_delete + + start = time.perf_counter() + + for name in names_to_delete: + if structure_type == "HashTable": + delete_func(temp_structure, name) + else: + temp_structure = delete_func(temp_structure, name) + + end = time.perf_counter() + times.append(end - start) + + return times + +def run_experiment(N=2000): + + print(f"Генерация тестовых данных (N={N})...") + records_shuffled, records_sorted = generate_test_data(N) + + queries = get_test_queries(records_shuffled, num_existing=100, num_nonexisting=10) + delete_names = get_delete_names(records_shuffled, num_to_delete=50) + + structures = ["LinkedList", "HashTable", "BST"] + modes = ["случайный", "отсортированный"] + + results = [] + + print("\nНачало экспериментов:") + + for structure in structures: + print(f"\nТестирование {structure}...") + + for mode in modes: + print(f" Режим: {mode}") + records = records_shuffled if mode == "случайный" else records_sorted + + print(f" Измерение вставки...") + try: + insert_times = measure_insertion(structure, records, repeats=3) + avg_insert = sum(insert_times) / len(insert_times) + except RecursionError: + print(f" ОШИБКА: Превышена глубина рекурсии при вставке в {structure} для {mode} режима") + continue + + print(f" Создание финальной структуры...") + if structure == "LinkedList": + final_structure = None + for name, phone in records: + final_structure = ll_insert(final_structure, name, phone) + elif structure == "HashTable": + final_structure = ht_create(2000) + for name, phone in records: + ht_insert(final_structure, name, phone) + elif structure == "BST": + final_structure = None + for name, phone in records: + final_structure = bst_insert(final_structure, name, phone) + + print(f" Измерение поиска...") + search_times = measure_search(structure, final_structure, queries, repeats=3) + avg_search = sum(search_times) / len(search_times) + + print(f" Измерение удаления...") + deletion_times = measure_deletion(structure, final_structure, delete_names, repeats=3) + avg_deletion = sum(deletion_times) / len(deletion_times) + + results.append({ + "Структура": structure, + "Режим": mode, + "Операция": "вставка", + "Замеры": insert_times, + "Среднее": avg_insert + }) + results.append({ + "Структура": structure, + "Режим": mode, + "Операция": "поиск", + "Замеры": search_times, + "Среднее": avg_search + }) + results.append({ + "Структура": structure, + "Режим": mode, + "Операция": "удаление", + "Замеры": deletion_times, + "Среднее": avg_deletion + }) + + print(f" Вставка: {avg_insert:.6f} сек") + print(f" Поиск: {avg_search:.6f} сек") + print(f" Удаление: {avg_deletion:.6f} сек") + + return results + + +import os +import csv +from datetime import datetime + +def save_to_csv(results, filename="results.csv"): + save_dir = "/Users/mariiaos/2026-rff_mp/osipovamd/docs" + filepath = os.path.join(save_dir, filename) + + with open(filepath, "w", newline="", encoding="utf-8") as f: + writer = csv.writer(f) + writer.writerow(["Структура", "Режим", "Операция", "Замер1", "Замер2", "Замер3", "Среднее"]) + + for res in results: + row = [ + res["Структура"], + res["Режим"], + res["Операция"], + *[f"{t:.6f}" for t in res["Замеры"]], + f"{res['Среднее']:.6f}" + ] + writer.writerow(row) + + print(f"\nРезультаты сохранены в: {filepath}") + return filepath + +def plot_results(results): + + if not results: + print("Нет данных для построения графиков!") + return + + plt.style.use('seaborn-v0_8-darkgrid') + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + + operations = ["вставка", "поиск", "удаление"] + structures = ["LinkedList", "HashTable", "BST"] + modes = ["случайный", "отсортированный"] + + colors = {'LinkedList': '#FF6B6B', 'HashTable': '#4ECDC4', 'BST': '#45B7D1'} + + for idx, operation in enumerate(operations): + ax = axes[idx] + + x = np.arange(len(modes)) + width = 0.25 + multiplier = 0 + + for structure in structures: + values = [] + for mode in modes: + found = False + for res in results: + if (res["Структура"] == structure and + res["Режим"] == mode and + res["Операция"] == operation): + values.append(res["Среднее"]) + found = True + break + if not found: + values.append(0) + + if max(values) > 0: + offset = width * multiplier + bars = ax.bar(x + offset, values, width, label=structure, color=colors[structure]) + multiplier += 1 + + ax.set_xlabel('Режим данных', fontsize=12) + ax.set_ylabel('Время (секунды)', fontsize=12) + ax.set_title(f'{operation.capitalize()}', fontsize=14, fontweight='bold') + ax.set_xticks(x + width) + ax.set_xticklabels(modes) + ax.legend(loc='upper left') + ax.grid(True, alpha=0.3) + + plt.suptitle('Сравнение производительности структур данных', + fontsize=16, fontweight='bold') + plt.tight_layout() + plt.savefig('performance_comparison.png', dpi=300, bbox_inches='tight') + plt.show() + + + +if __name__ == "__main__": + print("тестирование производительности структур данных") + + results = run_experiment(N=1000) + save_to_csv(results) + + if results: + print("\nПостроение графиков...") + plot_results(results) + + print("Сводная таблица результатов (среднее время в секундах)") + print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<10} {'Поиск':<10} {'Удаление':<10}") + + + for structure in ["LinkedList", "HashTable", "BST"]: + for mode in ["случайный", "отсортированный"]: + insert_time = search_time = delete_time = 0 + for res in results: + if res["Структура"] == structure and res["Режим"] == mode: + if res["Операция"] == "вставка": + insert_time = res["Среднее"] + elif res["Операция"] == "поиск": + search_time = res["Среднее"] + elif res["Операция"] == "удаление": + delete_time = res["Среднее"] + + if insert_time > 0 or search_time > 0 or delete_time > 0: + print(f"{structure:<12} {mode:<12} {insert_time:<10.6f} {search_time:<10.6f} {delete_time:<10.6f}") + else: + print("\nЭксперимент не дал результатов из-за ошибок.") + + print("\nЭксперимент завершён!") \ No newline at end of file diff --git a/osipovamd/maze_project/experiment.py b/osipovamd/maze_project/experiment.py new file mode 100644 index 0000000..bc3a0da --- /dev/null +++ b/osipovamd/maze_project/experiment.py @@ -0,0 +1,283 @@ +""" +Экспериментальный запуск для всех лабиринтов и алгоритмов +Создание CSV и графиков +""" + +import os +import csv +from datetime import datetime +from maze_model import Maze +from maze_builder import TextFileMazeBuilder +from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy +from maze_solver import MazeSolver, SearchStats + + +class ExperimentRunner: + def __init__(self): + self.all_results = [] + self.labirints = { + 'labirint1.txt': 'Маленький (10x10) с простым путём', + 'labirint2.txt': 'Средний (50x50) с тупиками', + 'labirint3.txt': 'Большой (100x100) запутанный', + 'labirint4.txt': 'Пустой (20x20) без стен', + 'labirint5.txt': 'Без выхода (20x20)' + } + self.strategies = [ + BFSStrategy(), + DFSStrategy(), + AStarStrategy() + ] + + def run_all_experiments(self): + """Запускает эксперименты для всех лабиринтов и алгоритмов""" + print("\n" + "="*70) + print("ЗАПУСК ЭКСПЕРИМЕНТОВ") + print("="*70) + + builder = TextFileMazeBuilder() + + for filename, description in self.labirints.items(): + if not os.path.exists(filename): + print(f"\n⚠️ Файл {filename} не найден, пропускаем...") + continue + + print(f"\n📁 Лабиринт: {description}") + print(f" Файл: {filename}") + print("-" * 50) + + try: + maze = builder.build_from_file(filename) + maze_name = filename.replace('.txt', '') + + for strategy in self.strategies: + print(f" Тестирование {strategy.get_name()}...", end=" ", flush=True) + + solver = MazeSolver(maze, maze_name, strategy) + path, stats = solver.solve_with_stats() + + self.all_results.append({ + 'лабиринт': description, + 'стратегия': stats.algorithm_name, + 'время_мс': stats.execution_time_ms, + 'посещено_клеток': stats.visited_cells, + 'длина_пути': stats.path_length, + 'путь_найден': 'Да' if stats.path_found else 'Нет', + 'размер': stats.maze_size + }) + + print(f"готово! время={stats.execution_time_ms:.3f}мс, путь={stats.path_length}") + + except Exception as e: + print(f" ❌ Ошибка: {e}") + + print("\n" + "="*70) + print("ЭКСПЕРИМЕНТЫ ЗАВЕРШЕНЫ") + print("="*70) + + def save_to_csv(self, filename="experiment_results.csv"): + """Сохраняет результаты в CSV""" + if not self.all_results: + print("Нет результатов для сохранения!") + return + + with open(filename, 'w', newline='', encoding='utf-8-sig') as f: + fieldnames = ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути', 'путь_найден', 'размер'] + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(self.all_results) + + print(f"\n✅ Результаты сохранены в {filename}") + + # Показываем содержимое CSV + print("\n" + "="*70) + print("СОДЕРЖИМОЕ CSV ФАЙЛА:") + print("="*70) + with open(filename, 'r', encoding='utf-8-sig') as f: + print(f.read()) + + def create_charts(self): + """Создаёт графики для каждого лабиринта""" + try: + import matplotlib.pyplot as plt + import numpy as np + + print("\n" + "="*70) + print("ПОСТРОЕНИЕ ГРАФИКОВ") + print("="*70) + + # Группируем результаты по лабиринтам + results_by_maze = {} + for result in self.all_results: + maze = result['лабиринт'] + if maze not in results_by_maze: + results_by_maze[maze] = [] + results_by_maze[maze].append(result) + + # Для каждого лабиринта создаём отдельный график + for maze_name, maze_results in results_by_maze.items(): + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + fig.suptitle(f'Сравнение алгоритмов: {maze_name}', fontsize=14, fontweight='bold') + + algorithms = [r['стратегия'] for r in maze_results] + + # График 1: Время выполнения + times = [r['время_мс'] for r in maze_results] + bars1 = axes[0].bar(algorithms, times, color=['blue', 'green', 'red']) + axes[0].set_ylabel('Время (мс)') + axes[0].set_title('Время выполнения') + axes[0].tick_params(axis='x', rotation=15) + for bar, val in zip(bars1, times): + axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, + f'{val:.3f}', ha='center', va='bottom', fontsize=9) + + # График 2: Посещённые клетки + visited = [r['посещено_клеток'] for r in maze_results] + bars2 = axes[1].bar(algorithms, visited, color=['blue', 'green', 'red']) + axes[1].set_ylabel('Количество клеток') + axes[1].set_title('Посещённые клетки') + axes[1].tick_params(axis='x', rotation=15) + for bar, val in zip(bars2, visited): + axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, + f'{val:.0f}', ha='center', va='bottom', fontsize=9) + + # График 3: Длина пути + lengths = [r['длина_пути'] for r in maze_results] + bars3 = axes[2].bar(algorithms, lengths, color=['blue', 'green', 'red']) + axes[2].set_ylabel('Шагов') + axes[2].set_title('Длина найденного пути') + axes[2].tick_params(axis='x', rotation=15) + for bar, val in zip(bars3, lengths): + axes[2].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, + f'{val:.0f}', ha='center', va='bottom', fontsize=9) + + plt.tight_layout() + + # Сохраняем график + safe_name = maze_name.replace(' ', '_').replace('(', '').replace(')', '').replace('×', 'x') + filename = f"chart_{safe_name}.png" + plt.savefig(filename, dpi=150, bbox_inches='tight') + print(f"✅ Сохранён: {filename}") + plt.close() + + # Общий сводный график + self._create_summary_chart() + + except ImportError: + print("\n⚠️ Для построения графиков установите matplotlib:") + print(" pip install matplotlib numpy") + + def _create_summary_chart(self): + """Создаёт сводный график по всем лабиринтам""" + import matplotlib.pyplot as plt + import numpy as np + + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle('Сводное сравнение алгоритмов по всем лабиринтам', fontsize=14, fontweight='bold') + + # Получаем уникальные лабиринты и алгоритмы + mazes = list(set([r['лабиринт'] for r in self.all_results])) + algorithms = ['BFS (Поиск в ширину)', 'DFS (Поиск в глубину)', 'A* (A-Star)'] + + # 1. Время по лабиринтам + ax1 = axes[0, 0] + x = np.arange(len(mazes)) + width = 0.25 + + for i, algo in enumerate(algorithms): + times = [] + for maze in mazes: + result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None) + times.append(result['время_мс'] if result else 0) + bars = ax1.bar(x + (i - 1) * width, times, width, label=algo) + for bar, val in zip(bars, times): + if val > 0: + ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5, + f'{val:.1f}', ha='center', va='bottom', fontsize=7) + + ax1.set_xlabel('Лабиринт') + ax1.set_ylabel('Время (мс)') + ax1.set_title('Сравнение времени выполнения') + ax1.set_xticks(x) + ax1.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right') + ax1.legend() + + # 2. Посещённые клетки + ax2 = axes[0, 1] + for i, algo in enumerate(algorithms): + visited = [] + for maze in mazes: + result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None) + visited.append(result['посещено_клеток'] if result else 0) + bars = ax2.bar(x + (i - 1) * width, visited, width, label=algo) + for bar, val in zip(bars, visited): + if val > 0: + ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5, + f'{val:.0f}', ha='center', va='bottom', fontsize=7) + + ax2.set_xlabel('Лабиринт') + ax2.set_ylabel('Посещённые клетки') + ax2.set_title('Сравнение посещённых клеток') + ax2.set_xticks(x) + ax2.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right') + ax2.legend() + + # 3. Длина пути + ax3 = axes[1, 0] + for i, algo in enumerate(algorithms): + lengths = [] + for maze in mazes: + result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None) + lengths.append(result['длина_пути'] if result else 0) + bars = ax3.bar(x + (i - 1) * width, lengths, width, label=algo) + for bar, val in zip(bars, lengths): + if val > 0: + ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1, + f'{val:.0f}', ha='center', va='bottom', fontsize=7) + + ax3.set_xlabel('Лабиринт') + ax3.set_ylabel('Длина пути (шагов)') + ax3.set_title('Сравнение длины пути') + ax3.set_xticks(x) + ax3.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right') + ax3.legend() + + # 4. Таблица результатов + ax4 = axes[1, 1] + ax4.axis('tight') + ax4.axis('off') + + # Создаём таблицу + table_data = [] + for maze in mazes: + row = [maze[:25]] + for algo in algorithms: + result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None) + if result: + row.append(f"{result['время_мс']:.1f}мс") + else: + row.append("-") + table_data.append(row) + + columns = ['Лабиринт', 'BFS', 'DFS', 'A*'] + table = ax4.table(cellText=table_data, colLabels=columns, cellLoc='center', loc='center') + table.auto_set_font_size(False) + table.set_fontsize(9) + table.scale(1.2, 1.5) + ax4.set_title('Сводная таблица (время в мс)', fontsize=10) + + plt.tight_layout() + plt.savefig('summary_chart.png', dpi=150, bbox_inches='tight') + print("Сохранён: summary_chart.png") + plt.close() + + +def main(): + + runner = ExperimentRunner() + runner.run_all_experiments() + runner.save_to_csv("experiment_results.csv") + runner.create_charts() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/osipovamd/maze_project/experiment_results.csv b/osipovamd/maze_project/experiment_results.csv new file mode 100644 index 0000000..73cccea --- /dev/null +++ b/osipovamd/maze_project/experiment_results.csv @@ -0,0 +1,18 @@ +лабиринт,стратегия,время_мс,длина_пути,путь_найден,размер,дата_время +labirint1,BFS (Поиск в ширину),0.211,14,Да,10x10,2026-05-27 22:26:56 +labirint2,BFS (Поиск в ширину),0.858,0,Нет,50x51,2026-05-27 22:27:10 +labirint3,BFS (Поиск в ширину),0.055,0,Нет,100x100,2026-05-27 22:27:26 +labirint4,BFS (Поиск в ширину),1.326,35,Да,20x20,2026-05-27 22:27:35 +labirint5,BFS (Поиск в ширину),0.373,36,Да,21x20,2026-05-27 22:27:44 +labirint1,DFS (Поиск в глубину),0.135,14,Да,10x10,2026-05-27 22:28:16 +labirint2,DFS (Поиск в глубину),0.797,0,Нет,50x51,2026-05-27 22:28:25 +labirint3,DFS (Поиск в глубину),0.047,0,Нет,100x100,2026-05-27 22:28:31 +labirint4,DFS (Поиск в глубину),0.88,171,Да,20x20,2026-05-27 22:28:36 +labirint5,DFS (Поиск в глубину),0.772,36,Да,21x20,2026-05-27 22:28:41 +labirint1,A* (A-Star),0.311,14,Да,10x10,2026-05-27 22:28:45 +labirint2,A* (A-Star),1.318,0,Нет,50x51,2026-05-27 22:28:50 +labirint3,A* (A-Star),0.055,0,Нет,100x100,2026-05-27 22:28:55 +labirint4,A* (A-Star),2.301,35,Да,20x20,2026-05-27 22:29:00 +labirint5,A* (A-Star),0.684,36,Да,21x20,2026-05-27 22:29:04 +labirint1,A* (A-Star),0.316,14,Да,10x10,2026-05-27 22:36:50 +labirint1,DFS (Поиск в глубину),0.133,14,Да,10x10,2026-05-27 22:41:42 diff --git a/osipovamd/maze_project/labirint1.txt b/osipovamd/maze_project/labirint1.txt new file mode 100644 index 0000000..279d916 --- /dev/null +++ b/osipovamd/maze_project/labirint1.txt @@ -0,0 +1,10 @@ +########## +#S # +# ##### # +# # # +# ### # # +# # # # +# # ### # +# # # +# #####E# +########## \ No newline at end of file diff --git a/osipovamd/maze_project/labirint2.txt b/osipovamd/maze_project/labirint2.txt new file mode 100644 index 0000000..395bd05 --- /dev/null +++ b/osipovamd/maze_project/labirint2.txt @@ -0,0 +1,51 @@ +################################################## +#S # +# ############################################# # +# # # # +# # ######################################### # # +# # # # # # +# # # ##################################### # # # +# # # # # # # # +# # # # ################################# # # # # +# # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # ##### # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # ######### # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # +# # # # # # # # # ############# # # # # # # # # # +# # # # # # # # # # # # # # # # # # +# # # # # # # # ################# # # # # # # # # +# # # # # # # # # # # # # # # # +# # # # # # # ##################### # # # # # # # +# # # # # # # # # # # # # # +# # # # # # ######################### # # # # # # +# # # # # # # # # # # # +# # # # # ############################# # # # # # +# # # # # # # # # # +# # # # ################################# # # # # +# # # # # # # # +# # # ##################################### # # # +# # # # # # +# # ######################################### # # +# # # # +# ############################################# # +# # +################################################## +# E# +################################################## \ No newline at end of file diff --git a/osipovamd/maze_project/labirint3.txt b/osipovamd/maze_project/labirint3.txt new file mode 100644 index 0000000..a43068d --- /dev/null +++ b/osipovamd/maze_project/labirint3.txt @@ -0,0 +1,100 @@ +#################################################################################################### +#################################################################################################### +##S# # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +#################################################################################################### +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +#################################################################################################### +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +#################################################################################################### +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +#################################################################################################### +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +#################################################################################################### +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +#################################################################################################### +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +#################################################################################################### +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +#################################################################################################### +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +#################################################################################################### +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +#################################################################################################### +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +#################################################################################################### +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +#################################################################################################### +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +#################################################################################################### +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # +### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## +## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # # +## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # E# +#################################################################################################### diff --git a/osipovamd/maze_project/labirint4.txt b/osipovamd/maze_project/labirint4.txt new file mode 100644 index 0000000..10bbaf0 --- /dev/null +++ b/osipovamd/maze_project/labirint4.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# E# +#################### \ No newline at end of file diff --git a/osipovamd/maze_project/labirint5.txt b/osipovamd/maze_project/labirint5.txt new file mode 100644 index 0000000..d9b8169 --- /dev/null +++ b/osipovamd/maze_project/labirint5.txt @@ -0,0 +1,20 @@ +#################### +#S # +# ############### # +# # # # +# # ########### # # +# # # # # # +# # # ####### # # # +# # # # # # # # +# # # # ### # # # # +# # # # # # # # # +# # # # ##### # # # +# # # # # # # +# # # ######### # # +# # # # # +# # ############# # +# # # +# ################### +# # +# E# +#################### \ No newline at end of file diff --git a/osipovamd/maze_project/main.py b/osipovamd/maze_project/main.py new file mode 100644 index 0000000..ae35cea --- /dev/null +++ b/osipovamd/maze_project/main.py @@ -0,0 +1,161 @@ +import os +import csv +from datetime import datetime +from maze_model import Maze +from maze_builder import TextFileMazeBuilder +from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy +from maze_solver import MazeSolver + + +def get_maze_file(): + maze_files = [f for f in os.listdir('.') if f.endswith('.txt') and f != 'experiment_results.csv'] + + if not maze_files: + print("\nНет файлов лабиринтов! Поместите файлы labirint1.txt и т.д. в папку.") + exit(1) + + print("\nДоступные файлы лабиринтов:") + for i, f in enumerate(maze_files, 1): + print(f" {i}. {f}") + + while True: + choice = input(f"\nВыберите файл (1-{len(maze_files)}): ").strip() + try: + idx = int(choice) - 1 + if 0 <= idx < len(maze_files): + return maze_files[idx] + except ValueError: + pass + print(f"Неверный выбор. Введите число от 1 до {len(maze_files)}") + + +def display_maze_with_path(maze: Maze, path=None): + print("\n+" + "-" * maze.width + "+") + + for y in range(maze.height): + row = "|" + for x in range(maze.width): + cell = maze.get_cell(x, y) + if cell == maze.start_cell: + row += "S" + elif cell == maze.exit_cell: + row += "E" + elif path and cell in path: + row += "." + elif cell.is_wall: + row += "#" + else: + row += " " + row += "|" + print(row) + + print("+" + "-" * maze.width + "+") + + +def save_to_csv(maze_name: str, algorithm_name: str, time_ms: float, path_length: int, path_found: bool, maze_size: str): + csv_filename = "experiment_results.csv" + + file_exists = os.path.exists(csv_filename) + + with open(csv_filename, 'a', newline='', encoding='utf-8-sig') as f: + writer = csv.writer(f) + + if not file_exists: + writer.writerow(['лабиринт', 'стратегия', 'время_мс', 'длина_пути', 'путь_найден', 'размер', 'дата_время']) + + writer.writerow([ + maze_name, + algorithm_name, + round(time_ms, 3), + path_length, + 'Да' if path_found else 'Нет', + maze_size, + datetime.now().strftime("%Y-%m-%d %H:%M:%S") + ]) + + print(f"Результат сохранён в {csv_filename}") + + +def print_all_results(): + csv_filename = "experiment_results.csv" + + with open(csv_filename, 'r', encoding='utf-8-sig') as f: + reader = csv.reader(f) + headers = next(reader) + print(f"{headers[0]:<25} {headers[1]:<22} {headers[2]:<10} {headers[3]:<10} {headers[4]:<8} {headers[5]:<10}") + for row in reader: + print(f"{row[0]:<25} {row[1]:<22} {row[2]:<10} {row[3]:<10} {row[4]:<8} {row[5]:<10}") + + +def main(): + filename = get_maze_file() + + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(filename) + + maze_name = filename.replace('.txt', '') + maze_size = f"{maze.width}x{maze.height}" + + print(f"\nЛабиринт загружен из файла: {filename}") + print(f" Размер: {maze_size}") + print(f" Старт: ({maze.start_cell.x}, {maze.start_cell.y})") + print(f" Выход: ({maze.exit_cell.x}, {maze.exit_cell.y})") + + except FileNotFoundError: + print(f"\nФайл '{filename}' не найден!") + return + except ValueError as e: + print(f"\nОшибка в файле лабиринта: {e}") + return + + print("\nЗагруженный лабиринт:") + maze.display() + + print("ВЫБОР АЛГОРИТМА ПОИСКА") + print("1. BFS (Поиск в ширину)") + print("2. DFS (Поиск в глубину)") + print("3. A* (A-Star)") + + + choice = input("\nВыберите алгоритм (1-3): ").strip() + + + if choice == '1': + strategy = BFSStrategy() + elif choice == '2': + strategy = DFSStrategy() + elif choice == '3': + strategy = AStarStrategy() + else: + print("Неверный выбор!") + return + + + solver = MazeSolver(maze, strategy) + + path, stats = solver.solve_with_stats() + + print(stats.detailed_report()) + + if path: + print("\nЛабиринт с найденным путём (точки):") + display_maze_with_path(maze, path) + else: + print("Путь не найден!") + + # Сохраняем результат в CSV + save_to_csv( + maze_name=maze_name, + algorithm_name=stats.algorithm_name, + time_ms=stats.execution_time_ms, + path_length=stats.path_length, + path_found=stats.path_found, + maze_size=maze_size + ) + + print("\nПрограмма завершена!") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/osipovamd/maze_project/maze_builder.py b/osipovamd/maze_project/maze_builder.py new file mode 100644 index 0000000..662d7bd --- /dev/null +++ b/osipovamd/maze_project/maze_builder.py @@ -0,0 +1,84 @@ +from abc import ABC, abstractmethod +from maze_model import Maze + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + pass + + @abstractmethod + def build_from_string(self, content: str) -> Maze: + pass + + +class TextFileMazeBuilder(MazeBuilder): + def __init__(self): + self._maze = None + self._lines = [] + self._start_found = False + self._exit_found = False + + def build_from_file(self, filename: str) -> Maze: + try: + with open(filename, 'r', encoding='utf-8') as file: + content = file.read() + return self.build_from_string(content) + except FileNotFoundError: + raise FileNotFoundError(f"Файл не найден: {filename}") + + def build_from_string(self, content: str) -> Maze: + self._reset() + self._lines = [line.rstrip('\n\r') for line in content.split('\n')] + while self._lines and not self._lines[-1].strip(): + self._lines.pop() + + if not self._lines: + raise ValueError("Пустой файл лабиринта") + + height = len(self._lines) + width = max(len(line) for line in self._lines) + + for i, line in enumerate(self._lines): + if len(line) != width: + self._lines[i] = line.ljust(width) + + self._maze = Maze(width, height) + + for y, line in enumerate(self._lines): + for x, char in enumerate(line): + self._parse_cell(x, y, char) + + self._validate_maze() + return self._maze + + def _reset(self): + self._maze = None + self._lines = [] + self._start_found = False + self._exit_found = False + + def _parse_cell(self, x: int, y: int, char: str): + cell = self._maze.get_cell(x, y) + if char == '#': + cell.is_wall = True + elif char == 'S': + if self._start_found: + raise ValueError(f"Найден второй старт в ({x}, {y})") + self._maze.set_start(x, y) + self._start_found = True + elif char == 'E': + if self._exit_found: + raise ValueError(f"Найден второй выход в ({x}, {y})") + self._maze.set_exit(x, y) + self._exit_found = True + elif char == ' ': + pass + else: + raise ValueError(f"Неизвестный символ '{char}' в ({x}, {y})") + + def _validate_maze(self): + if not self._start_found: + raise ValueError("В лабиринте не найден старт (символ 'S')") + if not self._exit_found: + raise ValueError("В лабиринте не найден выход (символ 'E')") \ No newline at end of file diff --git a/osipovamd/maze_project/maze_model.py b/osipovamd/maze_project/maze_model.py new file mode 100644 index 0000000..b3d5c5f --- /dev/null +++ b/osipovamd/maze_project/maze_model.py @@ -0,0 +1,103 @@ +from typing import List, Optional + + +class Cell: + def __init__(self, x: int, y: int): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self) -> bool: + return not self.is_wall + + def __repr__(self) -> str: + if self.is_start: + return "S" + elif self.is_exit: + return "E" + elif self.is_wall: + return "#" + else: + return "." + + def __eq__(self, other) -> bool: + if not isinstance(other, Cell): + return False + return self.x == other.x and self.y == other.y + + def __hash__(self) -> int: + return hash((self.x, self.y)) + + def __lt__(self, other): + return (self.x, self.y) < (other.x, other.y) + + +class Maze: + def __init__(self, width: int, height: int): + self.width = width + self.height = height + self._cells: List[List[Cell]] = [] + self.start_cell: Optional[Cell] = None + self.exit_cell: Optional[Cell] = None + + for y in range(height): + row = [] + for x in range(width): + row.append(Cell(x, y)) + self._cells.append(row) + + 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 set_wall(self, x: int, y: int, is_wall: bool = True) -> None: + cell = self.get_cell(x, y) + if cell: + cell.is_wall = is_wall + + def set_start(self, x: int, y: int) -> None: + cell = self.get_cell(x, y) + if cell: + if self.start_cell: + self.start_cell.is_start = False + cell.is_start = True + self.start_cell = cell + + def set_exit(self, x: int, y: int) -> None: + cell = self.get_cell(x, y) + if cell: + if self.exit_cell: + self.exit_cell.is_exit = False + cell.is_exit = True + self.exit_cell = cell + + def display(self) -> None: + print("+" + "-" * self.width + "+") + for y in range(self.height): + row_str = "|" + for x in range(self.width): + cell = self._cells[y][x] + if cell.is_start: + row_str += "S" + elif cell.is_exit: + row_str += "E" + elif cell.is_wall: + row_str += "#" + else: + row_str += " " + row_str += "|" + print(row_str) + print("+" + "-" * self.width + "+") \ No newline at end of file diff --git a/osipovamd/maze_project/maze_solver.py b/osipovamd/maze_project/maze_solver.py new file mode 100644 index 0000000..0ec885c --- /dev/null +++ b/osipovamd/maze_project/maze_solver.py @@ -0,0 +1,71 @@ +import time +from typing import List, Optional +from dataclasses import dataclass +from maze_model import Maze, Cell +from pathfinding_strategies import PathFindingStrategy + + +@dataclass +class SearchStats: + algorithm_name: str + path_length: int + execution_time_ms: float + path_found: bool + + def __post_init__(self): + self.execution_time_ms = round(self.execution_time_ms, 3) + + def summary(self) -> str: + if self.path_found: + return f"{self.algorithm_name}: путь найден | длина={self.path_length} | время={self.execution_time_ms} мс" + return f"{self.algorithm_name}: путь НЕ найден | время={self.execution_time_ms} мс" + + def detailed_report(self) -> str: + separator = "=" * 50 + report = f"\n{separator}\nОтчёт о поиске пути\n{separator}\n" + report += f"Алгоритм: {self.algorithm_name}\n" + report += f"Путь найден: {'Да' if self.path_found else 'Нет'}\n" + if self.path_found: + report += f"Длина пути: {self.path_length} шагов\n" + report += f"Время выполнения: {self.execution_time_ms} мс\n{separator}" + return report + + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy = None): + self._maze = maze + self._strategy = strategy + self._last_stats: Optional[SearchStats] = None + self._validate_maze() + + def _validate_maze(self): + if not self._maze.start_cell: + raise ValueError("Лабиринт не имеет стартовой клетки") + if not self._maze.exit_cell: + raise ValueError("Лабиринт не имеет выходной клетки") + + def set_strategy(self, strategy: PathFindingStrategy) -> None: + self._strategy = strategy + + def solve(self) -> List[Cell]: + if self._strategy is None: + raise ValueError("Стратегия не установлена") + + start_time = time.perf_counter() + path = self._strategy.find_path(self._maze, self._maze.start_cell, self._maze.exit_cell) + end_time = time.perf_counter() + + self._last_stats = SearchStats( + algorithm_name=self._strategy.get_name(), + path_length=len(path), + execution_time_ms=(end_time - start_time) * 1000, + path_found=len(path) > 0 + ) + return path + + def solve_with_stats(self) -> tuple: + path = self.solve() + return path, self._last_stats + + def get_last_stats(self) -> Optional[SearchStats]: + return self._last_stats \ No newline at end of file diff --git a/osipovamd/maze_project/pathfinding_strategies.py b/osipovamd/maze_project/pathfinding_strategies.py new file mode 100644 index 0000000..817973c --- /dev/null +++ b/osipovamd/maze_project/pathfinding_strategies.py @@ -0,0 +1,123 @@ +from abc import ABC, abstractmethod +from typing import List, Dict, Optional +from collections import deque +import heapq +from maze_model import Maze, Cell + + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + + @abstractmethod + def get_name(self) -> str: + pass + + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + queue = deque([start]) + came_from: Dict[Cell, Optional[Cell]] = {start: None} + + while queue: + current = queue.popleft() + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + for neighbor in maze.get_neighbors(current): + if neighbor not in came_from: + came_from[neighbor] = current + queue.append(neighbor) + return [] + + def _reconstruct_path(self, came_from, start, exit_cell): + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path + + def get_name(self) -> str: + return "BFS (Поиск в ширину)" + + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + if start == exit_cell: + return [start] + + stack = [start] + came_from: Dict[Cell, Optional[Cell]] = {start: None} + + while stack: + current = stack.pop() + if current == exit_cell: + return self._reconstruct_path(came_from, start, exit_cell) + for neighbor in maze.get_neighbors(current): + if neighbor not in came_from: + came_from[neighbor] = current + stack.append(neighbor) + return [] + + def _reconstruct_path(self, came_from, start, exit_cell): + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path + + def get_name(self) -> str: + return "DFS (Поиск в глубину)" + + +class AStarStrategy(PathFindingStrategy): + 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)] + g_score: Dict[Cell, float] = {start: 0} + came_from: Dict[Cell, Optional[Cell]] = {start: None} + open_set_cells = {start} + + while open_set: + _, _, current = heapq.heappop(open_set) + open_set_cells.remove(current) + + if current == exit_cell: + return self._reconstruct_path(came_from, 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]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f = tentative_g + self._heuristic(neighbor, exit_cell) + if neighbor not in open_set_cells: + counter += 1 + heapq.heappush(open_set, (f, counter, neighbor)) + open_set_cells.add(neighbor) + return [] + + def _reconstruct_path(self, came_from, start, exit_cell): + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = came_from.get(current) + path.reverse() + return path + + def get_name(self) -> str: + return "A* (A-Star)" \ No newline at end of file