diff --git a/KolbasovPD/docs/data/1-st_exercise/main.py b/KolbasovPD/docs/data/1-st_exercise/main.py new file mode 100644 index 0000000..06125d2 --- /dev/null +++ b/KolbasovPD/docs/data/1-st_exercise/main.py @@ -0,0 +1,395 @@ +import time +import random +import csv +import sys +sys.setrecursionlimit(100000) + +def ll_insert(head, name, phone): + new_node = {'name': name, 'phone': phone, 'next': None} + + if head is None: + return new_node + + curr = head + prev = None + while curr: + if curr['name'] == name: + curr['phone'] = phone + return head + prev = curr + curr = curr['next'] + + prev['next'] = new_node + return head + +def ll_find(head, name): + curr = head + while curr: + if curr['name'] == name: + return curr['phone'] + curr = curr['next'] + return None + +def ll_delete(head, name): + if head is None: + return None + + if head['name'] == name: + return head['next'] + + curr = head + while curr['next']: + if curr['next']['name'] == name: + curr['next'] = curr['next']['next'] + return head + curr = curr['next'] + return head + +def ll_list_all(head): + records = [] + curr = head + while curr: + records.append((curr['name'], curr['phone'])) + curr = curr['next'] + records.sort(key=lambda x: x[0]) + return records + +def hash_function(name, table_size): + return sum(ord(c) for c in name) % table_size + +def ht_create(size=1000): + return [None] * size + +def ht_insert(buckets, name, phone): + index = hash_function(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + +def ht_find(buckets, name): + index = hash_function(name, len(buckets)) + return ll_find(buckets[index], name) + +def ht_delete(buckets, name): + index = hash_function(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + +def ht_list_all(buckets): + records = [] + for head in buckets: + curr = head + while curr: + records.append((curr['name'], curr['phone'])) + curr = curr['next'] + records.sort(key=lambda x: x[0]) + return records + +def bst_insert_iterative(root, name, phone): + new_node = {'name': name, 'phone': phone, 'left': None, 'right': None} + + if root is None: + return new_node + + curr = root + while True: + if name < curr['name']: + if curr['left'] is None: + curr['left'] = new_node + break + curr = curr['left'] + elif name > curr['name']: + if curr['right'] is None: + curr['right'] = new_node + break + curr = curr['right'] + else: + curr['phone'] = phone + break + + return root + +def bst_find_iterative(root, name): + curr = root + while curr: + if name == curr['name']: + return curr['phone'] + elif name < curr['name']: + curr = curr['left'] + else: + curr = curr['right'] + return None + +def bst_find_min(node): + while node and node['left']: + node = node['left'] + return node + +def bst_delete_iterative(root, name): + if root is None: + return None + + if name < root['name']: + root['left'] = bst_delete_iterative(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete_iterative(root['right'], name) + else: + if root['left'] is None: + return root['right'] + elif root['right'] is None: + return root['left'] + + parent = root + successor = root['right'] + while successor['left']: + parent = successor + successor = successor['left'] + + root['name'] = successor['name'] + root['phone'] = successor['phone'] + + if parent == root: + parent['right'] = successor['right'] + else: + parent['left'] = successor['right'] + + return root + +def bst_list_all(root): + result = [] + stack = [] + curr = root + + while stack or curr: + while curr: + stack.append(curr) + curr = curr['left'] + curr = stack.pop() + result.append((curr['name'], curr['phone'])) + curr = curr['right'] + + return result + +def generate_test_data(N=10000): + names = [f"User_{i:05d}" for i in range(N)] + phones = [f"+7-999-{random.randint(1000000, 9999999)}" for _ in range(N)] + + records = list(zip(names, phones)) + + records_shuffled = records.copy() + random.shuffle(records_shuffled) + + records_sorted = sorted(records, key=lambda x: x[0]) + + return records_shuffled, records_sorted + +def measure_insertion(structure_type, records, ht_size=1000): + if structure_type == "LinkedList": + head = None + start = time.perf_counter() + for name, phone in records: + head = ll_insert(head, name, phone) + end = time.perf_counter() + return head, (end - start) + + elif structure_type == "HashTable": + buckets = ht_create(ht_size) + start = time.perf_counter() + for name, phone in records: + ht_insert(buckets, name, phone) + end = time.perf_counter() + return buckets, (end - start) + + elif structure_type == "BST": + root = None + start = time.perf_counter() + for name, phone in records: + root = bst_insert_iterative(root, name, phone) + end = time.perf_counter() + return root, (end - start) + +def measure_search(data_structure, structure_type, existing_names, non_existing_names): + start = time.perf_counter() + for name in existing_names: + if structure_type == "LinkedList": + ll_find(data_structure, name) + elif structure_type == "HashTable": + ht_find(data_structure, name) + elif structure_type == "BST": + bst_find_iterative(data_structure, name) + + for name in non_existing_names: + if structure_type == "LinkedList": + ll_find(data_structure, name) + elif structure_type == "HashTable": + ht_find(data_structure, name) + elif structure_type == "BST": + bst_find_iterative(data_structure, name) + end = time.perf_counter() + + return end - start + +def measure_deletion(data_structure, structure_type, names_to_delete): + start = time.perf_counter() + for name in names_to_delete: + if structure_type == "LinkedList": + data_structure = ll_delete(data_structure, name) + elif structure_type == "HashTable": + ht_delete(data_structure, name) + elif structure_type == "BST": + data_structure = bst_delete_iterative(data_structure, name) + end = time.perf_counter() + + return data_structure, (end - start) + +def run_experiment(N=5000, repeats=5): + print(f"Генерация тестовых данных (N={N})...") + records_shuffled, records_sorted = generate_test_data(N) + + existing_names = [name for name, _ in random.sample(records_shuffled, min(100, N))] + non_existing_names = [f"None_{i}" for i in range(10)] + delete_names = [name for name, _ in random.sample(records_shuffled, min(50, N))] + + results = [] + + structures = ["LinkedList", "HashTable", "BST"] + modes = ["случайный", "отсортированный"] + + for struct in structures: + for mode in modes: + records = records_shuffled if mode == "случайный" else records_sorted + + print(f"\nТестирование: {struct}, режим: {mode}") + + insertion_times = [] + search_times = [] + deletion_times = [] + + for rep in range(repeats): + print(f" Повторение {rep+1}/{repeats}...") + + data_structure, insert_time = measure_insertion(struct, records) + insertion_times.append(insert_time) + + search_time = measure_search(data_structure, struct, existing_names, non_existing_names) + search_times.append(search_time) + + data_structure, delete_time = measure_deletion(data_structure, struct, delete_names) + deletion_times.append(delete_time) + + avg_insert = sum(insertion_times) / repeats + avg_search = sum(search_times) / repeats + avg_delete = sum(deletion_times) / repeats + + results.append({ + "structure": struct, + "mode": mode, + "insertion_avg": avg_insert, + "insertion_all": insertion_times, + "search_avg": avg_search, + "search_all": search_times, + "deletion_avg": avg_delete, + "deletion_all": deletion_times + }) + + print(f" Вставка: {avg_insert:.6f} сек (замеры: {[f'{t:.6f}' for t in insertion_times]})") + print(f" Поиск: {avg_search:.6f} сек (замеры: {[f'{t:.6f}' for t in search_times]})") + print(f" Удаление: {avg_delete:.6f} сек (замеры: {[f'{t:.6f}' for t in deletion_times]})") + + return results + +def save_results_to_csv(results, filename="results.csv"): + with open(filename, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(["Структура", "Режим", "Операция", "Повторение", "Время (сек)"]) + + for res in results: + struct = res["structure"] + mode = res["mode"] + + for i, t in enumerate(res["insertion_all"]): + writer.writerow([struct, mode, "вставка", i+1, t]) + writer.writerow([struct, mode, "вставка", "СРЕДНЕЕ", res["insertion_avg"]]) + + for i, t in enumerate(res["search_all"]): + writer.writerow([struct, mode, "поиск", i+1, t]) + writer.writerow([struct, mode, "поиск", "СРЕДНЕЕ", res["search_avg"]]) + + for i, t in enumerate(res["deletion_all"]): + writer.writerow([struct, mode, "удаление", i+1, t]) + writer.writerow([struct, mode, "удаление", "СРЕДНЕЕ", res["deletion_avg"]]) + + print(f"\nРезультаты сохранены в {filename}") + +def print_summary_table(results): + print("\n" + "="*80) + print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ (среднее время в секундах)") + print("="*80) + print(f"{'Структура':<15} {'Режим':<12} {'Вставка':<12} {'Поиск (110)':<12} {'Удаление (50)':<12}") + print("-"*80) + + for res in results: + print(f"{res['structure']:<15} {res['mode']:<12} {res['insertion_avg']:<12.6f} " + f"{res['search_avg']:<12.6f} {res['deletion_avg']:<12.6f}") + + print("\n" + "="*80) + print("АНАЛИЗ ДЕГРАДАЦИИ BST") + print("="*80) + + bst_random = next(r for r in results if r['structure'] == "BST" and r['mode'] == "случайный") + bst_sorted = next(r for r in results if r['structure'] == "BST" and r['mode'] == "отсортированный") + + degradation = bst_sorted['insertion_avg'] / bst_random['insertion_avg'] + print(f"BST: отсортированные данные в {degradation:.1f} раз медленнее случайных") + print("Причина: вырождение дерева в линейный связный список (O(n) вместо O(log n))") + +if __name__ == "__main__": + print("="*80) + print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ ДЛЯ ТЕЛЕФОННОГО СПРАВОЧНИКА") + print("="*80) + + results = run_experiment(N=5000, repeats=5) + + save_results_to_csv(results) + + print_summary_table(results) + + print("\n" + "="*80) + print("ВЫВОДЫ И РЕКОМЕНДАЦИИ") + print("="*80) + print(""" +1. Хеш-таблица: + Лучшая производительность для операций поиска и вставки (O(1) в среднем) + Не чувствительна к порядку входных данных + Требует память под массив бакетов + Не поддерживает естественный порядок (нужна сортировка) + Идеально для справочников с частым поиском + +2. Двоичное дерево поиска: + Естественная сортировка (in-order обход) + Хорошая производительность на случайных данных (O(log n)) + Сильная деградация на отсортированных данных (O(n)) + Рекурсивные операции требуют больше памяти + Хорошо для задач, где нужен отсортированный вывод + +3. Связный список: + Простота реализации + Медленный поиск и удаление (O(n)) + Неэффективен для больших объёмов данных + Применим только для очень маленьких справочников + +РЕКОМЕНДАЦИИ ДЛЯ РЕАЛЬНЫХ ЗАДАЧ: +Частый поиск, редкие вставки -> ХЕШ-ТАБЛИЦА +Нужен отсортированный вывод -> ДЕРЕВО (с балансировкой) +Очень маленький справочник (<100 записей) -> СПИСОК +В реальных БД -> хеш-таблица + B-деревья + """) + + print("\n" + "="*80) + print("ДОПОЛНИТЕЛЬНЫЙ АНАЛИЗ") + print("="*80) + + for struct in ["LinkedList", "HashTable", "BST"]: + res_random = next(r for r in results if r['structure'] == struct and r['mode'] == "случайный") + print(f"{struct:12} поиск 110 записей: {res_random['search_avg']:.6f} сек") + + ll_random = next(r for r in results if r['structure'] == "LinkedList" and r['mode'] == "случайный") + ll_sorted = next(r for r in results if r['structure'] == "LinkedList" and r['mode'] == "отсортированный") + print(f"\nСвязный список: деградация {ll_sorted['insertion_avg'] / ll_random['insertion_avg']:.2f}х") \ No newline at end of file diff --git a/KolbasovPD/docs/data/1-st_exercise/results.csv b/KolbasovPD/docs/data/1-st_exercise/results.csv new file mode 100644 index 0000000..db7b3e7 --- /dev/null +++ b/KolbasovPD/docs/data/1-st_exercise/results.csv @@ -0,0 +1,109 @@ +Структура,Режим,Операция,Повторение,Время (сек) +LinkedList,случайный,вставка,1,0.7588584999975865 +LinkedList,случайный,вставка,2,0.7769573999976274 +LinkedList,случайный,вставка,3,0.7729451000013796 +LinkedList,случайный,вставка,4,0.7535389000004216 +LinkedList,случайный,вставка,5,0.758188899999368 +LinkedList,случайный,вставка,СРЕДНЕЕ,0.7640977599992766 +LinkedList,случайный,поиск,1,0.023368899997876724 +LinkedList,случайный,поиск,2,0.023365799999737646 +LinkedList,случайный,поиск,3,0.02325380000183941 +LinkedList,случайный,поиск,4,0.02314950000072713 +LinkedList,случайный,поиск,5,0.023074000000633532 +LinkedList,случайный,поиск,СРЕДНЕЕ,0.023242400000162887 +LinkedList,случайный,удаление,1,0.014257400001952192 +LinkedList,случайный,удаление,2,0.01445149999926798 +LinkedList,случайный,удаление,3,0.014200800000253366 +LinkedList,случайный,удаление,4,0.013934900001913775 +LinkedList,случайный,удаление,5,0.013907599997764919 +LinkedList,случайный,удаление,СРЕДНЕЕ,0.014150440000230446 +LinkedList,отсортированный,вставка,1,0.7295501999979024 +LinkedList,отсортированный,вставка,2,0.733855800001038 +LinkedList,отсортированный,вставка,3,0.9446520999990753 +LinkedList,отсортированный,вставка,4,0.8595158000025549 +LinkedList,отсортированный,вставка,5,0.7685707000018738 +LinkedList,отсортированный,вставка,СРЕДНЕЕ,0.8072289200004888 +LinkedList,отсортированный,поиск,1,0.022782500000175787 +LinkedList,отсортированный,поиск,2,0.022431400000641588 +LinkedList,отсортированный,поиск,3,0.029615100000228267 +LinkedList,отсортированный,поиск,4,0.026672600000893 +LinkedList,отсортированный,поиск,5,0.02327099999820348 +LinkedList,отсортированный,поиск,СРЕДНЕЕ,0.024954520000028423 +LinkedList,отсортированный,удаление,1,0.015000699997472111 +LinkedList,отсортированный,удаление,2,0.014776500000152737 +LinkedList,отсортированный,удаление,3,0.019435000001976732 +LinkedList,отсортированный,удаление,4,0.0165975999989314 +LinkedList,отсортированный,удаление,5,0.015432599997438956 +LinkedList,отсортированный,удаление,СРЕДНЕЕ,0.016248479999194387 +HashTable,случайный,вставка,1,0.06989740000062739 +HashTable,случайный,вставка,2,0.06307899999956135 +HashTable,случайный,вставка,3,0.06795570000031148 +HashTable,случайный,вставка,4,0.0703618000006827 +HashTable,случайный,вставка,5,0.06456320000143023 +HashTable,случайный,вставка,СРЕДНЕЕ,0.06717142000052263 +HashTable,случайный,поиск,1,0.0011812000011559576 +HashTable,случайный,поиск,2,0.0011990000020887237 +HashTable,случайный,поиск,3,0.0014205000006768387 +HashTable,случайный,поиск,4,0.0018674999992072117 +HashTable,случайный,поиск,5,0.0012002000003121793 +HashTable,случайный,поиск,СРЕДНЕЕ,0.001373680000688182 +HashTable,случайный,удаление,1,0.0008298000029753894 +HashTable,случайный,удаление,2,0.0008296999985759612 +HashTable,случайный,удаление,3,0.0010115000004589092 +HashTable,случайный,удаление,4,0.0009790000003704336 +HashTable,случайный,удаление,5,0.0008373999989998993 +HashTable,случайный,удаление,СРЕДНЕЕ,0.0008974800002761185 +HashTable,отсортированный,вставка,1,0.06598000000303728 +HashTable,отсортированный,вставка,2,0.06413769999926444 +HashTable,отсортированный,вставка,3,0.14105009999912 +HashTable,отсортированный,вставка,4,0.07664300000033109 +HashTable,отсортированный,вставка,5,0.06128300000273157 +HashTable,отсортированный,вставка,СРЕДНЕЕ,0.08181876000089687 +HashTable,отсортированный,поиск,1,0.001160599997092504 +HashTable,отсортированный,поиск,2,0.004666700002417201 +HashTable,отсортированный,поиск,3,0.00214869999763323 +HashTable,отсортированный,поиск,4,0.0016174000011233147 +HashTable,отсортированный,поиск,5,0.0013016000011703 +HashTable,отсортированный,поиск,СРЕДНЕЕ,0.00217899999988731 +HashTable,отсортированный,удаление,1,0.0008883999980753288 +HashTable,отсортированный,удаление,2,0.002087799999571871 +HashTable,отсортированный,удаление,3,0.001078500001312932 +HashTable,отсортированный,удаление,4,0.0009227000009559561 +HashTable,отсортированный,удаление,5,0.000898300000699237 +HashTable,отсортированный,удаление,СРЕДНЕЕ,0.001175140000123065 +BST,случайный,вставка,1,0.01702110000042012 +BST,случайный,вставка,2,0.014504000002489192 +BST,случайный,вставка,3,0.013862499999959255 +BST,случайный,вставка,4,0.01633149999906891 +BST,случайный,вставка,5,0.015226100000290899 +BST,случайный,вставка,СРЕДНЕЕ,0.015389040000445674 +BST,случайный,поиск,1,0.000405700000555953 +BST,случайный,поиск,2,0.0002680999969015829 +BST,случайный,поиск,3,0.00024119999943650328 +BST,случайный,поиск,4,0.00027720000070985407 +BST,случайный,поиск,5,0.00030730000071343966 +BST,случайный,поиск,СРЕДНЕЕ,0.0002998999996634666 +BST,случайный,удаление,1,0.00024620000112918206 +BST,случайный,удаление,2,0.00016830000095069408 +BST,случайный,удаление,3,0.0001548000000184402 +BST,случайный,удаление,4,0.00018679999993764795 +BST,случайный,удаление,5,0.00022140000146464445 +BST,случайный,удаление,СРЕДНЕЕ,0.00019550000070012175 +BST,отсортированный,вставка,1,1.875409899999795 +BST,отсортированный,вставка,2,1.8619785999981104 +BST,отсортированный,вставка,3,1.8126238000004378 +BST,отсортированный,вставка,4,1.8052959000015107 +BST,отсортированный,вставка,5,1.7978389999989304 +BST,отсортированный,вставка,СРЕДНЕЕ,1.830629439999757 +BST,отсортированный,поиск,1,0.03237050000097952 +BST,отсортированный,поиск,2,0.03320049999820185 +BST,отсортированный,поиск,3,0.033564300003490644 +BST,отсортированный,поиск,4,0.031019200003356673 +BST,отсортированный,поиск,5,0.03037219999896479 +BST,отсортированный,поиск,СРЕДНЕЕ,0.0321053400009987 +BST,отсортированный,удаление,1,0.03479439999864553 +BST,отсортированный,удаление,2,0.0337948000014876 +BST,отсортированный,удаление,3,0.034445099998265505 +BST,отсортированный,удаление,4,0.03366980000282638 +BST,отсортированный,удаление,5,0.03821500000049127 +BST,отсортированный,удаление,СРЕДНЕЕ,0.03498382000034326 diff --git a/KolbasovPD/docs/data/2-nd_exercise/main.py b/KolbasovPD/docs/data/2-nd_exercise/main.py new file mode 100644 index 0000000..40ce82a --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/main.py @@ -0,0 +1,546 @@ +import heapq +from collections import deque +from abc import ABC, abstractmethod +import time +import csv +import os +from typing import List, Tuple, Optional, Dict, Set + +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 + self.weight = 1 + + def is_passable(self) -> bool: + return not self.is_wall + +class Maze: + def __init__(self, width: int = 0, height: int = 0): + self.width = width + self.height = height + self.cells: List[List[Cell]] = [] + self.start: Optional[Cell] = None + self.exit: Optional[Cell] = None + + 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 + +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 f: + lines = f.readlines() + + height = len(lines) + width = len(lines[0].strip()) if height > 0 else 0 + + maze = Maze(width, height) + maze.cells = [[Cell(x, y) for x in range(width)] for y in range(height)] + + for y, line in enumerate(lines): + line = line.rstrip('\n') + for x, ch in enumerate(line): + if x < width: + cell = maze.cells[y][x] + if ch == '#': + cell.is_wall = True + elif ch == 'S': + cell.is_start = True + maze.start = cell + elif ch == 'E': + cell.is_exit = True + maze.exit = cell + elif ch.isdigit(): + cell.weight = int(ch) + cell.is_wall = False + + if not maze.start or not maze.exit: + raise ValueError("Лабиринт должен содержать старт (S) и выход (E)") + + return maze + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pass + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + queue = deque([start]) + visited = {start} + parent = {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, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent[current] + path.reverse() + return path + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + 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): + def heuristic(self, a: Cell, b: Cell) -> int: + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + open_set = [(0, start)] + came_from = {} + g_score = {start: 0} + f_score = {start: self.heuristic(start, exit_cell)} + + while open_set: + _, current = heapq.heappop(open_set) + + 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] + neighbor.weight + + if neighbor not in g_score or tentative_g < g_score[neighbor]: + came_from[neighbor] = current + g_score[neighbor] = tentative_g + f_score[neighbor] = tentative_g + self.heuristic(neighbor, exit_cell) + heapq.heappush(open_set, (f_score[neighbor], neighbor)) + + return [] + + def reconstruct_path(self, came_from: Dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current != start: + path.append(current) + current = came_from[current] + path.append(start) + path.reverse() + return path + +class DijkstraStrategy(PathFindingStrategy): + def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]: + pq = [(0, start)] + distances = {start: 0} + parent = {start: None} + + while pq: + dist, current = heapq.heappop(pq) + + if current == exit_cell: + return self.reconstruct_path(parent, start, exit_cell) + + if dist > distances[current]: + continue + + for neighbor in maze.get_neighbors(current): + new_dist = dist + neighbor.weight + + if neighbor not in distances or new_dist < distances[neighbor]: + distances[neighbor] = new_dist + parent[neighbor] = current + heapq.heappush(pq, (new_dist, neighbor)) + + return [] + + def reconstruct_path(self, parent: Dict, start: Cell, exit_cell: Cell) -> List[Cell]: + path = [] + current = exit_cell + while current is not None: + path.append(current) + current = parent[current] + path.reverse() + return path + +class SearchStats: + def __init__(self, time_ms: float, visited_cells: int, path_length: int, path: List[Cell] = None): + self.time_ms = time_ms + self.visited_cells = visited_cells + self.path_length = path_length + self.path = path + +class MazeSolver: + def __init__(self, maze: Maze, strategy: PathFindingStrategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def set_strategy(self, strategy: PathFindingStrategy): + self.strategy = strategy + + def add_observer(self, observer): + self.observers.append(observer) + + def notify_observers(self, event: str, data=None): + for observer in self.observers: + observer.update(event, data) + + def solve(self) -> SearchStats: + self.notify_observers("search_started") + + 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 + visited_cells = self.count_visited_cells() + path_length = len(path) + + stats = SearchStats(time_ms, visited_cells, path_length, path) + + self.notify_observers("search_finished", stats) + + return stats + + def count_visited_cells(self) -> int: + if isinstance(self.strategy, BFSStrategy): + return len(self.bfs_visited) + elif isinstance(self.strategy, DFSStrategy): + return len(self.dfs_visited) + return 0 + +class Observer(ABC): + @abstractmethod + def update(self, event: str, data=None): + pass + +class ConsoleView(Observer): + def __init__(self): + self.maze = None + self.current_path = None + + def set_maze(self, maze: Maze): + self.maze = maze + + def update(self, event: str, data=None): + if event == "search_finished": + self.display_path(data.path) + elif event == "search_started": + print("\nПоиск пути начат...") + + def display_path(self, path: List[Cell]): + if not path: + print("\nПуть не найден!") + return + + print(f"\nПуть найден! Длина: {len(path)} шагов") + self.render(path) + + def render(self, path: List[Cell] = None): + if not self.maze: + return + + path_set = set(path) if path else set() + + for y in range(self.maze.height): + for x in range(self.maze.width): + cell = self.maze.get_cell(x, y) + if cell in path_set: + print('*', end='') + elif cell.is_start: + print('S', end='') + elif cell.is_exit: + print('E', end='') + elif cell.is_wall: + print('#', end='') + else: + print(' ', end='') + print() + +class Player: + def __init__(self, start_cell: Cell): + self.current = start_cell + self.history = [] + + def move_to(self, cell: Cell): + if cell and cell.is_passable(): + self.history.append(self.current) + self.current = cell + return True + return False + + def undo(self): + if self.history: + self.current = self.history.pop() + return True + return False + +class Command(ABC): + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self): + pass + +class MoveCommand(Command): + def __init__(self, player: Player, direction: str, maze: Maze): + self.player = player + self.direction = direction + self.maze = maze + self.previous_position = None + + def execute(self) -> bool: + self.previous_position = self.player.current + + dx, dy = 0, 0 + if self.direction == 'W': + dy = -1 + elif self.direction == 'S': + dy = 1 + elif self.direction == 'A': + dx = -1 + elif self.direction == 'D': + dx = 1 + + new_cell = self.maze.get_cell(self.player.current.x + dx, self.player.current.y + dy) + + if new_cell and new_cell.is_passable(): + return self.player.move_to(new_cell) + return False + + def undo(self): + if self.previous_position: + self.player.current = self.previous_position + +def generate_test_mazes(): + test_mazes = { + "tiny": [ + "########", + "#S #", + "# #### #", + "# E #", + "########" + ], + "empty": [ + "########", + "#S #", + "# #", + "# E#", + "########" + ], + "no_exit": [ + "########", + "#S #", + "# #### #", + "# # #", + "########" + ], + "weighted": [ + "########", + "#S2 #", + "# 5#3 #", + "# 2 E #", + "########" + ] + } + + os.makedirs("mazes", exist_ok=True) + + for name, maze_data in test_mazes.items(): + filename = f"mazes/{name}.txt" + with open(filename, 'w', encoding='utf-8') as f: + f.write('\n'.join(maze_data)) + print(f"Создан лабиринт: {filename}") + +def run_experiments(): + strategies = { + "BFS": BFSStrategy(), + "DFS": DFSStrategy(), + "A*": AStarStrategy(), + "Dijkstra": DijkstraStrategy() + } + + mazes_list = ["tiny", "empty", "no_exit", "weighted"] + results = [] + + for maze_name in mazes_list: + filename = f"mazes/{maze_name}.txt" + + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(filename) + + print(f"\nТестирование лабиринта: {maze_name}") + + for strategy_name, strategy in strategies.items(): + print(f" Стратегия: {strategy_name}") + + times = [] + visited_counts = [] + path_lengths = [] + + for i in range(5): + solver = MazeSolver(maze, strategy) + stats = solver.solve() + times.append(stats.time_ms) + visited_counts.append(stats.visited_cells if stats.visited_cells else 0) + path_lengths.append(stats.path_length) + + avg_time = sum(times) / len(times) + avg_visited = sum(visited_counts) / len(visited_counts) + avg_path_len = sum(path_lengths) / len(path_lengths) + + results.append([ + maze_name, strategy_name, avg_time, avg_visited, avg_path_len + ]) + + print(f" Время: {avg_time:.3f} мс, Посещено: {avg_visited:.1f}, Путь: {avg_path_len:.1f}") + + except Exception as e: + print(f"Ошибка загрузки {maze_name}: {e}") + + with open("maze_results.csv", 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(["Лабиринт", "Стратегия", "Время_мс", "Посещено_клеток", "Длина_пути"]) + writer.writerows(results) + + print("\nРезультаты сохранены в maze_results.csv") + +def interactive_mode(): + print("\n=== ИНТЕРАКТИВНЫЙ РЕЖИМ ===") + filename = input("Введите имя файла с лабиринтом: ") + + try: + builder = TextFileMazeBuilder() + maze = builder.build_from_file(filename) + + print("\nВыберите стратегию:") + print("1. BFS (кратчайший путь)") + print("2. DFS (быстрый, не обязательно кратчайший)") + print("3. A* (оптимальный с эвристикой)") + print("4. Dijkstra (для взвешенных лабиринтов)") + + choice = input("Ваш выбор (1-4): ") + + strategies = { + '1': BFSStrategy(), + '2': DFSStrategy(), + '3': AStarStrategy(), + '4': DijkstraStrategy() + } + + if choice not in strategies: + print("Неверный выбор!") + return + + solver = MazeSolver(maze, strategies[choice]) + view = ConsoleView() + view.set_maze(maze) + solver.add_observer(view) + + stats = solver.solve() + + print(f"\nСтатистика:") + print(f"Время выполнения: {stats.time_ms:.3f} мс") + print(f"Длина пути: {stats.path_length}") + + input("\nНажмите Enter для ручного режима...") + + player = Player(maze.start) + + while player.current != maze.exit: + os.system('cls' if os.name == 'nt' else 'clear') + view.render() + print(f"\nТекущая позиция: ({player.current.x}, {player.current.y})") + print("Управление: W/A/S/D для движения, Z для отмены, Q для выхода") + + cmd = input("> ").upper() + + if cmd == 'Q': + break + elif cmd == 'Z': + command = MoveCommand(player, 'U', maze) + command.undo() + print("Отмена последнего хода") + elif cmd in ['W', 'A', 'S', 'D']: + command = MoveCommand(player, cmd, maze) + if command.execute(): + print("Перемещение выполнено") + else: + print("Нельзя пройти в этом направлении") + else: + print("Неизвестная команда") + + if player.current == maze.exit: + print("\nПОЗДРАВЛЯЮ! ВЫ НАШЛИ ВЫХОД!") + break + + except Exception as e: + print(f"Ошибка: {e}") + +def main(): + print("="*80) + print("ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА") + print("Применённые паттерны: Builder, Strategy, Observer, Command") + print("="*80) + + generate_test_mazes() + + print("\n1. Запустить эксперименты") + print("2. Интерактивный режим") + + choice = input("\nВыберите режим (1-2): ") + + if choice == '1': + run_experiments() + elif choice == '2': + interactive_mode() + else: + print("Неверный выбор!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv b/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv new file mode 100644 index 0000000..7b2d832 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/maze_results.csv @@ -0,0 +1 @@ +Лабиринт,Стратегия,Время_мс,Посещено_клеток,Длина_пути diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt new file mode 100644 index 0000000..f165a4a --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/empty.txt @@ -0,0 +1,5 @@ +######## +#S # +# # +# E# +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt new file mode 100644 index 0000000..733ea0b --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/no_exit.txt @@ -0,0 +1,5 @@ +######## +#S # +# #### # +# # # +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt new file mode 100644 index 0000000..d08f1c2 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/tiny.txt @@ -0,0 +1,5 @@ +######## +#S # +# #### # +# E # +######## \ No newline at end of file diff --git a/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt b/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt new file mode 100644 index 0000000..2aff7f7 --- /dev/null +++ b/KolbasovPD/docs/data/2-nd_exercise/mazes/weighted.txt @@ -0,0 +1,5 @@ +######## +#S2 # +# 5#3 # +# 2 E # +######## \ No newline at end of file