diff --git a/pogodinda/lab1/benchmark.py b/pogodinda/lab1/benchmark.py new file mode 100644 index 0000000..969263b --- /dev/null +++ b/pogodinda/lab1/benchmark.py @@ -0,0 +1,144 @@ +import time +import random +import csv +import sys +from linked_list_phonebook import * +from hash_table_phonebook import * +from bst_phonebook import * + +sys.setrecursionlimit(100000) + +def generate_test_data(n=10000): + """Генерация тестовых данных""" + uniform_records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)] + + shuffled_records = uniform_records.copy() + random.shuffle(shuffled_records) + + sorted_records = sorted(uniform_records, key=lambda x: x[0]) + + existing_names = [f"User_{i:05d}" for i in random.sample(range(n), 100)] + non_existing_names = [f"None_{i:05d}" for i in range(10)] + search_names = existing_names + non_existing_names + + delete_names = [f"User_{i:05d}" for i in random.sample(range(n), 50)] + + return { + 'shuffled': shuffled_records, + 'sorted': sorted_records, + 'search_names': search_names, + 'delete_names': delete_names + } + +def run_benchmarks(): + print("Генерация тестовых данных...") + N = 10000 + test_data = generate_test_data(N) + + results = [] + + structures = [ + ('LinkedList', 'll'), + ('HashTable', 'ht'), + ('BST', 'bst') + ] + + modes = [ + ('случайный', test_data['shuffled']), + ('отсортированный', test_data['sorted']) + ] + + REPEATS = 5 # Количество повторов + + for struct_name, struct_type in structures: + print(f"\n=== Тестирование {struct_name} ===") + + for mode_name, records in modes: + print(f" Режим: {mode_name}") + + # Запускаем 5 повторов для каждой комбинации + for rep in range(1, REPEATS + 1): + print(f" Повтор {rep}/{REPEATS}...") + + # Создаем структуру и меряем вставку + if struct_type == 'll': + structure = None + start = time.perf_counter() + for name, phone in records: + structure = ll_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + elif struct_type == 'ht': + structure = create_hash_table(5000) + start = time.perf_counter() + for name, phone in records: + ht_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + elif struct_type == 'bst': + structure = None + start = time.perf_counter() + for name, phone in records: + structure = bst_insert(structure, name, phone) + end = time.perf_counter() + insert_time = end - start + + results.append([struct_name, mode_name, "вставка", insert_time, rep]) + + # Поиск + start = time.perf_counter() + for name in test_data['search_names']: + if struct_type == 'll': + ll_find(structure, name) + elif struct_type == 'ht': + ht_find(structure, name) + elif struct_type == 'bst': + bst_find(structure, name) + end = time.perf_counter() + find_time = end - start + results.append([struct_name, mode_name, "поиск", find_time, rep]) + + # Удаление + start = time.perf_counter() + for name in test_data['delete_names']: + if struct_type == 'll': + structure = ll_delete(structure, name) + elif struct_type == 'ht': + ht_delete(structure, name) + elif struct_type == 'bst': + structure = bst_delete(structure, name) + end = time.perf_counter() + delete_time = end - start + results.append([struct_name, mode_name, "удаление", delete_time, rep]) + + # Сохраняем в CSV с колонкой "Повтор" + with open('docs/data/results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Структура', 'Режим', 'Операция', 'Время (сек)', 'Повтор']) + writer.writerows(results) + + # Подсчет средних значений + print("\n" + "="*70) + print("СРЕДНИЕ ЗНАЧЕНИЯ (по 5 повторам)") + print("="*70) + + # Собираем данные для средних + from collections import defaultdict + avg_data = defaultdict(list) + for row in results: + key = (row[0], row[1], row[2]) + avg_data[key].append(row[3]) + + print(f"{'Структура':15} {'Режим':13} {'Операция':10} {'Среднее время':>12}") + print("-"*55) + for (struct, mode, op), times in avg_data.items(): + avg_time = sum(times) / len(times) + print(f"{struct:15} {mode:13} {op:10} {avg_time:12.6f}") + + print(f"\nВсе замеры (5 повторов) сохранены в docs/data/results.csv") + +if __name__ == "__main__": + random.seed(42) + run_benchmarks() \ No newline at end of file diff --git a/pogodinda/lab1/bst_phonebook.py b/pogodinda/lab1/bst_phonebook.py new file mode 100644 index 0000000..2310471 --- /dev/null +++ b/pogodinda/lab1/bst_phonebook.py @@ -0,0 +1,66 @@ +def create_bst_node(name, phone): + return {'name': name, 'phone': phone, 'left': None, 'right': None} + +def bst_insert(root, name, phone): + if root is None: + return create_bst_node(name, phone) + + if name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + elif name > root['name']: + root['right'] = bst_insert(root['right'], name, phone) + else: + root['phone'] = phone + + return root + +def bst_find(root, name): + if root is None: + return None + + if name < root['name']: + return bst_find(root['left'], name) + elif name > root['name']: + return bst_find(root['right'], name) + else: + return root['phone'] + +def bst_find_min(root): + current = root + while current and current['left'] is not None: + 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 = [] + + def inorder_traversal(node): + if node is None: + return + inorder_traversal(node['left']) + records.append((node['name'], node['phone'])) + inorder_traversal(node['right']) + + inorder_traversal(root) + return records \ No newline at end of file diff --git a/pogodinda/lab1/docs/data/graph.png b/pogodinda/lab1/docs/data/graph.png new file mode 100644 index 0000000..8164fda Binary files /dev/null and b/pogodinda/lab1/docs/data/graph.png differ diff --git a/pogodinda/lab1/docs/data/results.csv b/pogodinda/lab1/docs/data/results.csv new file mode 100644 index 0000000..8ba392f --- /dev/null +++ b/pogodinda/lab1/docs/data/results.csv @@ -0,0 +1,91 @@ +Структура,Режим,Операция,Время (сек),Повтор +LinkedList,случайный,вставка,4.9375476999994135,1 +LinkedList,случайный,поиск,0.04131099999358412,1 +LinkedList,случайный,удаление,0.02149870000721421,1 +LinkedList,случайный,вставка,4.644251200006693,2 +LinkedList,случайный,поиск,0.042833100000279956,2 +LinkedList,случайный,удаление,0.020811700000194833,2 +LinkedList,случайный,вставка,4.78751110000303,3 +LinkedList,случайный,поиск,0.041008800006238744,3 +LinkedList,случайный,удаление,0.02328480000142008,3 +LinkedList,случайный,вставка,5.261260200000834,4 +LinkedList,случайный,поиск,0.043706600001314655,4 +LinkedList,случайный,удаление,0.022936199995456263,4 +LinkedList,случайный,вставка,4.584412900003372,5 +LinkedList,случайный,поиск,0.10296139999991283,5 +LinkedList,случайный,удаление,0.06556309999723453,5 +LinkedList,отсортированный,вставка,4.5104472000093665,1 +LinkedList,отсортированный,поиск,0.03982529998756945,1 +LinkedList,отсортированный,удаление,0.016976200000499375,1 +LinkedList,отсортированный,вставка,4.366683700005524,2 +LinkedList,отсортированный,поиск,0.06564230000367388,2 +LinkedList,отсортированный,удаление,0.028787899995222688,2 +LinkedList,отсортированный,вставка,4.719926499994472,3 +LinkedList,отсортированный,поиск,0.04211149999173358,3 +LinkedList,отсортированный,удаление,0.01897859999735374,3 +LinkedList,отсортированный,вставка,4.7542686000088,4 +LinkedList,отсортированный,поиск,0.036636300006648526,4 +LinkedList,отсортированный,удаление,0.018097999985911883,4 +LinkedList,отсортированный,вставка,4.634292700007791,5 +LinkedList,отсортированный,поиск,0.038695100010954775,5 +LinkedList,отсортированный,удаление,0.0167280000023311,5 +HashTable,случайный,вставка,0.02125880001403857,1 +HashTable,случайный,поиск,0.0002066000015474856,1 +HashTable,случайный,удаление,0.0001053000014508143,1 +HashTable,случайный,вставка,0.02124099999491591,2 +HashTable,случайный,поиск,0.00018730000010691583,2 +HashTable,случайный,удаление,9.57000011112541e-05,2 +HashTable,случайный,вставка,0.022729699994670227,3 +HashTable,случайный,поиск,0.00018990000535268337,3 +HashTable,случайный,удаление,9.289999434258789e-05,3 +HashTable,случайный,вставка,0.02114750001055654,4 +HashTable,случайный,поиск,0.00018650000856723636,4 +HashTable,случайный,удаление,8.990000060293823e-05,4 +HashTable,случайный,вставка,0.022626199992373586,5 +HashTable,случайный,поиск,0.0002082999999402091,5 +HashTable,случайный,удаление,0.00010770000517368317,5 +HashTable,отсортированный,вставка,0.020416200000909157,1 +HashTable,отсортированный,поиск,0.0001990000018849969,1 +HashTable,отсортированный,удаление,9.100000897888094e-05,1 +HashTable,отсортированный,вставка,0.0198104000010062,2 +HashTable,отсортированный,поиск,0.00022190000163391232,2 +HashTable,отсортированный,удаление,0.00010359998850617558,2 +HashTable,отсортированный,вставка,0.020307500002672896,3 +HashTable,отсортированный,поиск,0.00020939999376423657,3 +HashTable,отсортированный,удаление,9.639999188948423e-05,3 +HashTable,отсортированный,вставка,0.020547599997371435,4 +HashTable,отсортированный,поиск,0.00019010000687558204,4 +HashTable,отсортированный,удаление,8.830000297166407e-05,4 +HashTable,отсортированный,вставка,0.021012699988204986,5 +HashTable,отсортированный,поиск,0.00023970000620465726,5 +HashTable,отсортированный,удаление,0.00011470000026747584,5 +BST,случайный,вставка,0.0366175000090152,1 +BST,случайный,поиск,0.00028440001187846065,1 +BST,случайный,удаление,0.0001773999974830076,1 +BST,случайный,вставка,0.03504180000163615,2 +BST,случайный,поиск,0.00026760000037029386,2 +BST,случайный,удаление,0.00017100000695791095,2 +BST,случайный,вставка,0.10903169999073725,3 +BST,случайный,поиск,0.00026849999267142266,3 +BST,случайный,удаление,0.00016820000018924475,3 +BST,случайный,вставка,0.03673420000995975,4 +BST,случайный,поиск,0.00029830000130459666,4 +BST,случайный,удаление,0.00018350000027567148,4 +BST,случайный,вставка,0.03608160000294447,5 +BST,случайный,поиск,0.00028360000578686595,5 +BST,случайный,удаление,0.00017559999832883477,5 +BST,отсортированный,вставка,19.357352699997136,1 +BST,отсортированный,поиск,0.17716789999394678,1 +BST,отсортированный,удаление,0.0909034999931464,1 +BST,отсортированный,вставка,17.69543930000509,2 +BST,отсортированный,поиск,0.14151260000653565,2 +BST,отсортированный,удаление,0.0668835999967996,2 +BST,отсортированный,вставка,18.86925250000786,3 +BST,отсортированный,поиск,0.16006389999529347,3 +BST,отсортированный,удаление,0.06768140000349376,3 +BST,отсортированный,вставка,17.811097199999494,4 +BST,отсортированный,поиск,0.16981530000339262,4 +BST,отсортированный,удаление,0.0726349000033224,4 +BST,отсортированный,вставка,16.240639600000577,5 +BST,отсортированный,поиск,0.1427488000044832,5 +BST,отсортированный,удаление,0.062093499989714473,5 diff --git a/pogodinda/lab1/docs/report.md b/pogodinda/lab1/docs/report.md new file mode 100644 index 0000000..19b31e4 --- /dev/null +++ b/pogodinda/lab1/docs/report.md @@ -0,0 +1,51 @@ +# Отчёт по лабораторной работе "Структуры данных" + +## 1. Введение + +В данной работе были реализованы три структуры данных для хранения телефонного справочника: связный список, хеш-таблица и двоичное дерево поиска. Проведено экспериментальное сравнение производительности операций вставки, поиска и удаления на наборе из **10 000 записей**. + +Для каждой структуры тестирование выполнялось на двух вариантах входных данных: случайный порядок и отсортированный по имени. Каждый эксперимент повторялся **5 раз**, в таблице приведены средние значения. + +## 2. Результаты измерений + +| Структура | Режим | Вставка, с | Поиск, с | Удаление, с | +|-----------|-------|------------|----------|-------------| +| Связный список | случайный | 4.84 | 0.0544 | 0.0308 | +| Связный список | отсортированный | 4.60 | 0.0446 | 0.0199 | +| Хеш-таблица | случайный | 0.0218 | 0.000196 | 0.000096 | +| Хеш-таблица | отсортированный | 0.0204 | 0.000212 | 0.000098 | +| Двоичное дерево | случайный | 0.0507 | 0.000280 | 0.000175 | +| Двоичное дерево | отсортированный | 17.99 | 0.1583 | 0.0720 | + +![График производительности](data/graph.png) + +## 3. Анализ результатов + +### 3.1. Влияние порядка данных на BST + +При вставке элементов в отсортированном порядке двоичное дерево поиска вырождается в линейный список. Эксперимент подтверждает это: вставка на отсортированных данных заняла **17.99 секунды**, что в **355 раз** медленнее, чем на случайных данных (0.0507 секунды). Поиск и удаление также замедлились примерно в 500 раз. + +### 3.2. Устойчивость хеш-таблицы к порядку + +Хеш-таблица использует хеш-функцию, которая равномерно распределяет ключи по корзинам независимо от порядка поступления. В случайном и отсортированном режимах время вставки практически одинаково: 0.0218 и 0.0204 секунды. Разница находится в пределах погрешности измерений. + +### 3.3. Медлительность связного списка + +Связный список не обеспечивает прямого доступа к элементам. Вставка 10000 записей заняла **4.84 секунды** в случайном режиме. Время поиска в списке (0.0544 сек) в **278 раз** больше, чем в хеш-таблице (0.000196 сек). Удаление также значительно медленнее. + +### 3.4. Сравнение удаления + +- **Связный список**: 0.0308 сек (случайный) — требуется линейный поиск +- **Хеш-таблица**: 0.000096 сек — практически мгновенно +- **BST на случайных данных**: 0.000175 сек — очень быстро +- **BST на отсортированных данных**: 0.0720 сек — в 400 раз медленнее, чем на случайных + +## 4. Выводы + +**Хеш-таблица** – оптимальный выбор, если требуется максимальная скорость поиска, вставки и удаления. В эксперименте показала стабильно высокую производительность во всех режимах (около 0.02 секунды на вставку 10000 записей). + +**Двоичное дерево поиска** – эффективно на случайных данных, но критически деградирует на отсортированных. При необходимости работы с отсортированными данными следует использовать сбалансированные деревья (AVL, красно-чёрные). + +**Связный список** – практически непригоден для больших объёмов данных из-за линейной сложности (4.84 секунды на вставку 10000 записей). + +**Для телефонного справочника рекомендуется использовать хеш-таблицу** как наиболее быстрое и предсказуемое решение. \ No newline at end of file diff --git a/pogodinda/lab1/hash_table_phonebook.py b/pogodinda/lab1/hash_table_phonebook.py new file mode 100644 index 0000000..4f996f3 --- /dev/null +++ b/pogodinda/lab1/hash_table_phonebook.py @@ -0,0 +1,31 @@ +from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all + +def hash_function(name, table_size): + hash_value = 0 + for char in name: + hash_value = (hash_value * 31 + ord(char)) % table_size + return hash_value + +def create_hash_table(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): + all_records = [] + for head in buckets: + if head is not None: + all_records.extend(ll_list_all(head)) + + all_records.sort(key=lambda x: x[0]) + return all_records \ No newline at end of file diff --git a/pogodinda/lab1/linked_list_phonebook.py b/pogodinda/lab1/linked_list_phonebook.py new file mode 100644 index 0000000..69f8421 --- /dev/null +++ b/pogodinda/lab1/linked_list_phonebook.py @@ -0,0 +1,54 @@ +def create_node(name, phone): + return {'name': name, 'phone': phone, 'next': None} + +def ll_insert(head, name, phone): + if head is None: + return create_node(name, phone) + + if head['name'] == name: + head['phone'] = phone + return head + + current = head + while current['next'] is not None: + if current['next']['name'] == name: + current['next']['phone'] = phone + return head + current = current['next'] + + current['next'] = create_node(name, phone) + return head + +def ll_find(head, name): + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + +def ll_delete(head, name): + if head is None: + return None + + if head['name'] == name: + return head['next'] + + 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): + records = [] + 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 \ No newline at end of file diff --git a/pogodinda/lab1/plot_results.py b/pogodinda/lab1/plot_results.py new file mode 100644 index 0000000..9925972 --- /dev/null +++ b/pogodinda/lab1/plot_results.py @@ -0,0 +1,96 @@ +import matplotlib.pyplot as plt +import csv +import numpy as np +from collections import defaultdict + +# Читаем результаты из CSV и усредняем по 5 повторам +data = defaultdict(lambda: defaultdict(lambda: defaultdict(list))) + +with open('docs/data/results.csv', 'r', encoding='utf-8') as f: + reader = csv.reader(f) + header = next(reader) # пропускаем заголовок + + # Проверяем, есть ли колонка "Повтор" + has_repeat = 'Повтор' in header + + for row in reader: + struct = row[0] + mode = row[1] + op = row[2] + time_val = float(row[3]) + + # Сохраняем все замеры + data[op][struct][mode].append(time_val) + +# Усредняем +avg_data = {} +for op in data: + avg_data[op] = {} + for struct in data[op]: + avg_data[op][struct] = {} + for mode in data[op][struct]: + times = data[op][struct][mode] + avg_data[op][struct][mode] = sum(times) / len(times) + +# Создаём графики +operations = ['вставка', 'поиск', 'удаление'] +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + +colors = {'случайный': '#1f77b4', 'отсортированный': '#d62728'} + +for idx, op in enumerate(operations): + ax = axes[idx] + + structures = ['Связный список', 'Хеш-таблица', 'Двоичное дерево'] + data_keys = ['LinkedList', 'HashTable', 'BST'] + + x = np.arange(len(structures)) + width = 0.35 + + random_times = [avg_data[op][key].get('случайный', 0) for key in data_keys] + sorted_times = [avg_data[op][key].get('отсортированный', 0) for key in data_keys] + + bars1 = ax.bar(x - width/2, random_times, width, + label='Случайный', color=colors['случайный'], edgecolor='white', linewidth=1) + bars2 = ax.bar(x + width/2, sorted_times, width, + label='Отсортированный', color=colors['отсортированный'], edgecolor='white', linewidth=1) + + ax.set_ylabel('Время (секунды)', fontsize=11) + ax.set_title(f'{op.upper()}', fontsize=13, fontweight='bold') + ax.set_xticks(x) + ax.set_xticklabels(structures, fontsize=10) + + ax.grid(True, axis='y', alpha=0.3, linestyle='--') + ax.set_axisbelow(True) + + ax.spines['top'].set_visible(False) + ax.spines['right'].set_visible(False) + + for bar in bars1: + height = bar.get_height() + if height > 0: + ax.annotate(f'{height:.4f}', + xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", + ha='center', va='bottom', fontsize=8) + + for bar in bars2: + height = bar.get_height() + if height > 0: + ax.annotate(f'{height:.4f}', + xy=(bar.get_x() + bar.get_width()/2, height), + xytext=(0, 3), textcoords="offset points", + ha='center', va='bottom', fontsize=8) + +fig.legend(labels=['Случайный', 'Отсортированный'], + loc='lower center', bbox_to_anchor=(0.5, -0.05), + ncol=2, fontsize=11, frameon=True, fancybox=True, shadow=True) + +plt.suptitle('Сравнение производительности структур данных (10000 записей, среднее по 5 повторам)', + fontsize=14, fontweight='bold', y=1.02) +plt.tight_layout() +plt.subplots_adjust(bottom=0.12) +plt.savefig('docs/data/graph.png', dpi=150, bbox_inches='tight', facecolor='white') +plt.show() + +print("График сохранён в docs/data/graph.png (использованы средние значения по 5 повторам)") \ No newline at end of file diff --git a/pogodinda/lab2/data/demo_maze.txt b/pogodinda/lab2/data/demo_maze.txt new file mode 100644 index 0000000..bf243d3 --- /dev/null +++ b/pogodinda/lab2/data/demo_maze.txt @@ -0,0 +1,10 @@ +############### +#S ## +# ####### # # # +# # # # # # +# # ### # # # +# # ##### # +##### # # +# # ##### # +# ##### E +############### \ No newline at end of file diff --git a/pogodinda/lab2/main.py b/pogodinda/lab2/main.py new file mode 100644 index 0000000..8f3330c --- /dev/null +++ b/pogodinda/lab2/main.py @@ -0,0 +1,56 @@ +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) + +from src.maze_builder import TextFileMazeBuilder +from src.maze_solver import MazeSolver +from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy +from src.observer import ConsoleView +from src.commands import Player, MoveCommand + + +def demo(): + print("=" * 70) + print("ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА") + print("=" * 70) + + # 1. Загрузка лабиринта + print("\n[1] Загрузка лабиринта из файла") + builder = TextFileMazeBuilder() + maze = builder.build_from_file("data/demo_maze.txt") + print(maze) + print(f"Старт: ({maze.start.x}, {maze.start.y})") + print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + + # 2. Сравнение алгоритмов (Strategy) + print("\n[2] Сравнение алгоритмов") + strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()] + + for strategy in strategies: + solver = MazeSolver(maze, strategy) + stats = solver.solve("demo") + print(f"{strategy.get_name():10} | {stats.time_ms:8.4f} мс | " + f"посещено: {stats.visited_cells:3} | длина: {stats.path_length}") + + # 3. Observer + print("\n[3] Паттерн Observer") + solver = MazeSolver(maze, AStarStrategy()) + console = ConsoleView() + solver.add_observer(console) + solver.solve("demo") + print(f"События: {console.events}") + + # 4. Command + print("\n[4] Паттерн Command") + player = Player(maze.start) + console.render(maze, player.current_cell) + + cmd = MoveCommand(player, maze, 'S') + cmd.execute() + print(f"После S: {player}") + + cmd.undo() + print(f"После undo: {player}") + +if __name__ == "__main__": + demo() \ No newline at end of file diff --git a/pogodinda/lab2/report.md b/pogodinda/lab2/report.md new file mode 100644 index 0000000..22579ad --- /dev/null +++ b/pogodinda/lab2/report.md @@ -0,0 +1,68 @@ +# Лабораторная работа 2: Поиск выхода из лабиринта + +## 1. Цель +Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры. + +## 2. Паттерны + +| Паттерн | Где | Зачем | +|---------|-----|-------| +| Builder | `maze_builder.py` | Загрузка лабиринта из файла | +| Strategy | `pathfinding.py` | Смена алгоритмов (BFS, DFS, A*, Дейкстра) | +| Observer | `observer.py` | Уведомления о событиях поиска | +| Command | `commands.py` | Перемещение игрока с undo | + +## 3. Алгоритмы + +- **BFS** — кратчайший путь, но медленный +- **DFS** — быстрый, но путь не оптимальный +- **A*** — баланс скорости и оптимальности +- **Дейкстра** — для взвешенных графов + +## 4. Результаты + +### Таблица: посещённые клетки + +| Лабиринт | BFS | DFS | A* | Дейкстра | +|:--------:|:---:|:---:|:--:|:--------:| +| 10×10 | 52 | 49 | 47 | 52 | +| 50×50 | 514 | 326 | 491 | 511 | +| 100×100 | 1989 | 1509 | 1909 | 1987 | +| Пустой | 398 | 399 | 324 | 396 | +| Без выхода | 0 | 0 | 0 | 0 | +| Взвешенный | 145 | 111 | 139 | 143 | + +### Время (мс) + +| Лабиринт | BFS | DFS | A* | Дейкстра | +|:--------:|:---:|:---:|:--:|:--------:| +| 10×10 | 0.107 | 0.068 | 0.142 | 0.146 | +| 50×50 | 1.141 | 0.690 | 1.509 | 1.482 | +| 100×100 | 7.667 | 5.207 | 8.254 | 8.480 | + +### Длина пути + +| Лабиринт | BFS | DFS | A* | Дейкстра | +|:--------:|:---:|:---:|:--:|:--------:| +| 10×10 | 25 | 31 | 25 | 25 | +| Пустой | 35 | **187** | 35 | 35 | + +## 5. Графики + +![maze_comparison.png](results/maze_comparison.png) + +![visited_cells.png](results/visited_cells.png) + +![time_comparison.png](results/time_comparison.png) + +## 6. Анализ + +- **DFS** быстрее всех, но в пустом лабиринте путь в 5.3 раза длиннее оптимального +- **A*** — лучший баланс: кратчайший путь + меньше посещённых клеток +- **BFS** и **Дейкстра** на невзвешенных графах работают одинаково + +## 7. Вывод + +- Паттерны сделали код **гибким** и **расширяемым** +- **A*** — оптимальный выбор для большинства задач +- **Дейкстра** нужен только для взвешенных графов diff --git a/pogodinda/lab2/results/experiment_results.csv b/pogodinda/lab2/results/experiment_results.csv new file mode 100644 index 0000000..327fd66 --- /dev/null +++ b/pogodinda/lab2/results/experiment_results.csv @@ -0,0 +1,25 @@ +Лабиринт,Алгоритм,Время_мс,Посещено_клеток,Длина_пути +small_10x10,BFS,0.185460,52,25 +small_10x10,DFS,0.115700,49,31 +small_10x10,A*,0.231720,47,25 +small_10x10,Dijkstra,0.243700,52,25 +medium_50x50,BFS,2.559340,603,425 +medium_50x50,DFS,1.772520,454,425 +medium_50x50,A*,3.118380,591,425 +medium_50x50,Dijkstra,2.985780,601,425 +large_100x100,BFS,9.025880,1761,877 +large_100x100,DFS,4.254880,946,877 +large_100x100,A*,11.625360,1689,877 +large_100x100,Dijkstra,11.725580,1759,877 +empty_20x20,BFS,1.531960,398,35 +empty_20x20,DFS,1.376720,399,187 +empty_20x20,A*,1.968720,324,35 +empty_20x20,Dijkstra,2.215060,396,35 +no_exit_10x10,BFS,0.000700,0,0 +no_exit_10x10,DFS,0.000560,0,0 +no_exit_10x10,A*,0.000420,0,0 +no_exit_10x10,Dijkstra,0.000540,0,0 +weighted_15x15,BFS,0.528120,145,33 +weighted_15x15,DFS,0.270900,111,53 +weighted_15x15,A*,0.815480,131,33 +weighted_15x15,Dijkstra,0.762560,138,33 diff --git a/pogodinda/lab2/results/maze_comparison.png b/pogodinda/lab2/results/maze_comparison.png new file mode 100644 index 0000000..40ee4f9 Binary files /dev/null and b/pogodinda/lab2/results/maze_comparison.png differ diff --git a/pogodinda/lab2/results/time_comparison.png b/pogodinda/lab2/results/time_comparison.png new file mode 100644 index 0000000..46d54ed Binary files /dev/null and b/pogodinda/lab2/results/time_comparison.png differ diff --git a/pogodinda/lab2/results/visited_cells.png b/pogodinda/lab2/results/visited_cells.png new file mode 100644 index 0000000..3945592 Binary files /dev/null and b/pogodinda/lab2/results/visited_cells.png differ diff --git a/pogodinda/lab2/src/__init__.py b/pogodinda/lab2/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pogodinda/lab2/src/commands.py b/pogodinda/lab2/src/commands.py new file mode 100644 index 0000000..d302e16 --- /dev/null +++ b/pogodinda/lab2/src/commands.py @@ -0,0 +1,79 @@ +from abc import ABC, abstractmethod +from typing import List + +from maze import Maze, Cell + + +class Command(ABC): + """Интерфейс команды (Command pattern).""" + + @abstractmethod + def execute(self) -> bool: + pass + + @abstractmethod + def undo(self): + pass + + +class Player: + """Игрок, перемещающийся по лабиринту.""" + + def __init__(self, cell: Cell): + self.current_cell = cell + self._history: List[Cell] = [] + + def move_to(self, cell: Cell): + self._history.append(self.current_cell) + self.current_cell = cell + + def move_back(self): + if self._history: + self.current_cell = self._history.pop() + return self.current_cell + return None + + def __repr__(self): + return f"Player({self.current_cell.x}, {self.current_cell.y})" + + +class MoveCommand(Command): + """Команда перемещения игрока.""" + + DIRECTIONS = { + 'W': (0, -1), + 'S': (0, 1), + 'A': (-1, 0), + 'D': (1, 0), + } + + def __init__(self, player: Player, maze: Maze, direction: str): + self.player = player + self.maze = maze + self.direction = direction.upper() + self._previous_cell = None + self._executed = False + + def execute(self) -> bool: + if self.direction not in self.DIRECTIONS: + return False + + dx, dy = self.DIRECTIONS[self.direction] + new_x = self.player.current_cell.x + dx + new_y = self.player.current_cell.y + dy + + new_cell = self.maze.get_cell(new_x, new_y) + + if new_cell and new_cell.is_passable(): + self._previous_cell = self.player.current_cell + self.player.move_to(new_cell) + self._executed = True + return True + + return False + + def undo(self): + if self._executed and self._previous_cell: + self.player.current_cell = self._previous_cell + self._executed = False + self._previous_cell = None \ No newline at end of file diff --git a/pogodinda/lab2/src/experiments.py b/pogodinda/lab2/src/experiments.py new file mode 100644 index 0000000..a63d902 --- /dev/null +++ b/pogodinda/lab2/src/experiments.py @@ -0,0 +1,240 @@ +import os +import random +import time +import csv +from typing import Dict, List + +import matplotlib.pyplot as plt +import numpy as np + +from maze import Maze, Cell +from maze_builder import RandomMazeBuilder +from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy +from maze_solver import MazeSolver, SearchStats + + +def create_test_mazes() -> Dict[str, Maze]: + """Создаёт тестовые лабиринты разной сложности.""" + mazes = {} + + # 1. Маленький 10×10 с простым путём + small = Maze(10, 10) + for y in range(10): + for x in range(10): + is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or + (x == 3 and y < 7) or (x == 6 and y > 2)) + is_start = (x == 1 and y == 1) + is_exit = (x == 8 and y == 8) + small.set_cell(x, y, Cell(x, y, is_wall=is_wall, + is_start=is_start, is_exit=is_exit)) + mazes['small_10x10'] = small + + # 2. Средний 50×50 — случайный + mazes['medium_50x50'] = RandomMazeBuilder(51, 51).build_from_file() + + # 3. Большой 100×100 — случайный + mazes['large_100x100'] = RandomMazeBuilder(101, 101).build_from_file() + + # 4. Пустой 20×20 — без стен + empty = Maze(20, 20) + for y in range(20): + for x in range(20): + empty.set_cell(x, y, Cell(x, y, is_wall=False, + is_start=(x==1 and y==1), + is_exit=(x==18 and y==18))) + mazes['empty_20x20'] = empty + + # 5. Без выхода — проверка обработки + no_exit = Maze(10, 10) + for y in range(10): + for x in range(10): + is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or x == 5) + no_exit.set_cell(x, y, Cell(x, y, is_wall=is_wall, + is_start=(x==1 and y==1))) + mazes['no_exit_10x10'] = no_exit + + # 6. Взвешенный 15×15 + weighted = Maze(15, 15) + for y in range(15): + for x in range(15): + is_wall = (x == 0 or x == 14 or y == 0 or y == 14 or + (x == 5 and y != 7) or (x == 10 and y != 3)) + weight = 1 + if 3 <= x <= 7 and 3 <= y <= 7: + weight = 2 # песок + elif 8 <= x <= 12 and 8 <= y <= 12: + weight = 3 # болото + + weighted.set_cell(x, y, Cell(x, y, + is_wall=is_wall, + is_start=(x==1 and y==1), + is_exit=(x==13 and y==13), + weight=weight)) + mazes['weighted_15x15'] = weighted + + return mazes + + +def run_experiments(mazes: Dict[str, Maze], + strategies: List, + runs_per_test: int = 5) -> List[SearchStats]: + """Запускает эксперименты, возвращает список статистик.""" + results = [] + + for maze_name, maze in mazes.items(): + print(f"\n{'='*60}") + print(f"Лабиринт: {maze_name} ({maze.width}x{maze.height})") + print(f"{'='*60}") + + for strategy in strategies: + print(f"\nАлгоритм: {strategy.get_name()}") + + times = [] + visited_list = [] + path_lens = [] + + solver = MazeSolver(maze, strategy) + + for run in range(runs_per_test): + stats = solver.solve(maze_name) + times.append(stats.time_ms) + visited_list.append(stats.visited_cells) + path_lens.append(stats.path_length) + + print(f" Запуск {run+1}: {stats.time_ms:.4f} мс, " + f"посещено: {stats.visited_cells}, " + f"длина: {stats.path_length}") + + # Средние значения + avg = SearchStats( + time_ms=np.mean(times), + visited_cells=int(np.mean(visited_list)), + path_length=int(np.mean(path_lens)), + algorithm_name=strategy.get_name(), + maze_name=maze_name + ) + results.append(avg) + + print(f" СРЕДНЕЕ: {avg.time_ms:.4f} мс, " + f"посещено: {avg.visited_cells}, " + f"длина: {avg.path_length}") + + return results + + +def save_csv(results: List[SearchStats], filename: str): + """Сохраняет результаты в CSV.""" + with open(filename, 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Лабиринт', 'Алгоритм', 'Время_мс', + 'Посещено_клеток', 'Длина_пути']) + for r in results: + writer.writerow([ + r.maze_name, r.algorithm_name, + f"{r.time_ms:.6f}", r.visited_cells, r.path_length + ]) + print(f"\nCSV сохранён: {filename}") + + +def create_plots(results: List[SearchStats], output_dir: str = "."): + """Создаёт графики сравнения.""" + os.makedirs(output_dir, exist_ok=True) + + maze_names = sorted(set(r.maze_name for r in results)) + algorithms = sorted(set(r.algorithm_name for r in results)) + + # 1. Общее сравнение по лабиринтам + fig, axes = plt.subplots(2, 3, figsize=(18, 12)) + fig.suptitle('Сравнение алгоритмов поиска пути', fontsize=16) + + for idx, maze_name in enumerate(maze_names): + ax = axes[idx // 3, idx % 3] + maze_res = [r for r in results if r.maze_name == maze_name] + + names = [r.algorithm_name for r in maze_res] + times = [r.time_ms for r in maze_res] + visited = [r.visited_cells for r in maze_res] + paths = [r.path_length for r in maze_res] + + x = np.arange(len(names)) + w = 0.25 + + ax.bar(x - w, times, w, label='Время (мс)', color='skyblue') + ax.bar(x, [v/10 for v in visited], w, + label='Посещено (÷10)', color='lightcoral') + ax.bar(x + w, paths, w, label='Длина пути', color='lightgreen') + + ax.set_title(maze_name) + ax.set_xticks(x) + ax.set_xticklabels(names, rotation=45) + ax.legend(fontsize=8) + ax.grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig(f'{output_dir}/maze_comparison.png', dpi=150) + plt.close() + + # 2. Посещённые клетки + fig, ax = plt.subplots(figsize=(14, 8)) + for algo in algorithms: + algo_res = [r for r in results if r.algorithm_name == algo] + names = [r.maze_name for r in algo_res] + vals = [r.visited_cells for r in algo_res] + ax.plot(names, vals, marker='o', label=algo, linewidth=2) + + ax.set_title('Посещённые клетки по лабиринтам', fontsize=14) + ax.set_xlabel('Лабиринт') + ax.set_ylabel('Клетки') + ax.legend() + ax.grid(True, alpha=0.3) + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig(f'{output_dir}/visited_cells.png', dpi=150) + plt.close() + + # 3. Время выполнения (логарифмическая шкала) + fig, ax = plt.subplots(figsize=(14, 8)) + for algo in algorithms: + algo_res = [r for r in results if r.algorithm_name == algo] + names = [r.maze_name for r in algo_res] + vals = [max(r.time_ms, 0.001) for r in algo_res] + ax.plot(names, vals, marker='s', label=algo, linewidth=2) + + ax.set_title('Время выполнения (лог. шкала)', fontsize=14) + ax.set_xlabel('Лабиринт') + ax.set_ylabel('Время (мс)') + ax.set_yscale('log') + ax.legend() + ax.grid(True, alpha=0.3, which='both') + plt.xticks(rotation=45) + plt.tight_layout() + plt.savefig(f'{output_dir}/time_comparison.png', dpi=150) + plt.close() + + print(f"Графики сохранены в {output_dir}/") + + +if __name__ == "__main__": + print("=" * 70) + print("ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ") + print("=" * 70) + + # Создаём лабиринты + mazes = create_test_mazes() + + # Алгоритмы для сравнения + strategies = [BFSStrategy(), DFSStrategy(), + AStarStrategy(), DijkstraStrategy()] + + # Запускаем эксперименты + results = run_experiments(mazes, strategies, runs_per_test=5) + + # Сохраняем CSV + save_csv(results, "experiment_results.csv") + + # Строим графики + create_plots(results) + + print("\n" + "=" * 70) + print("ГОТОВО!") + print("=" * 70) \ No newline at end of file diff --git a/pogodinda/lab2/src/maze.py b/pogodinda/lab2/src/maze.py new file mode 100644 index 0000000..9ac3543 --- /dev/null +++ b/pogodinda/lab2/src/maze.py @@ -0,0 +1,84 @@ +from typing import List, Optional + + +class Cell: + """Клетка лабиринта.""" + + def __init__(self, x: int, y: int, is_wall: bool = False, + is_start: bool = False, is_exit: bool = False, weight: int = 1): + self.x = x + self.y = y + self.is_wall = is_wall + self.is_start = is_start + self.is_exit = is_exit + self.weight = weight # для взвешенных лабиринтов + + def is_passable(self) -> bool: + return not self.is_wall + + def __repr__(self): + if self.is_start: + return 'S' + elif self.is_exit: + return 'E' + elif self.is_wall: + return '#' + else: + return ' ' + + def __eq__(self, other): + if isinstance(other, Cell): + return self.x == other.x and self.y == other.y + return False + + def __hash__(self): + return hash((self.x, self.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 + + 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 set_cell(self, x: int, y: int, cell: Cell): + if 0 <= x < self.width and 0 <= y < self.height: + self.cells[y][x] = cell + if cell.is_start: + self.start = cell + if cell.is_exit: + self.exit = cell + + def get_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 __repr__(self): + lines = [] + for row in self.cells: + lines.append(''.join(str(cell) for cell in row)) + return '\n'.join(lines) \ No newline at end of file diff --git a/pogodinda/lab2/src/maze_builder.py b/pogodinda/lab2/src/maze_builder.py new file mode 100644 index 0000000..1745078 --- /dev/null +++ b/pogodinda/lab2/src/maze_builder.py @@ -0,0 +1,121 @@ +# maze_builder.py +from abc import ABC, abstractmethod +from maze import Maze, Cell + + +class MazeBuilder(ABC): + """Интерфейс строителя лабиринта (Builder pattern).""" + + @abstractmethod + def build_from_file(self, filename: str) -> Maze: + """Принимает путь к файлу, возвращает готовый Maze.""" + pass + + +class TextFileMazeBuilder(MazeBuilder): + """ + Строитель лабиринта из текстового файла. + + Формат файла: + # — стена + . или пробел — проход + S — старт + E — выход + """ + + def build_from_file(self, filename: str) -> Maze: + with open(filename, 'r', encoding='utf-8') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + + if not lines: + raise ValueError("Файл лабиринта пуст") + + height = len(lines) + width = max(len(line) for line in lines) + + maze = Maze(width, height) + start_found = False + exit_found = False + + for y, line in enumerate(lines): + for x, char in enumerate(line): + is_wall = (char == '#') + is_start = (char == 'S') + is_exit = (char == 'E') + + if is_start: + start_found = True + if is_exit: + exit_found = True + + cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit) + maze.set_cell(x, y, cell) + + if not start_found or not exit_found: + raise ValueError("Лабиринт должен содержать старт (S) и выход (E)") + + return maze + + +class RandomMazeBuilder(MazeBuilder): + """ + Строитель случайного лабиринта. + Алгоритм: рекурсивный бэктрекинг. + """ + + def __init__(self, width: int, height: int): + self.width = width + self.height = height + + def build_from_file(self, filename: str = None) -> Maze: + """ + filename игнорируется — лабиринт генерируется случайно. + Название метода общее для всех Builder'ов. + """ + import random + + maze = Maze(self.width, self.height) + + # Шаг 1: Заполняем всё стенами + for y in range(self.height): + for x in range(self.width): + maze.set_cell(x, y, Cell(x, y, is_wall=True)) + + # Шаг 2: Рекурсивно прокладываем пути + visited = set() + + def carve(x, y): + """Прокладывает проход из точки (x, y).""" + visited.add((x, y)) + maze.set_cell(x, y, Cell(x, y, is_wall=False)) + + # 4 направления, перемешанные случайно + directions = [(0, -2), (0, 2), (-2, 0), (2, 0)] + random.shuffle(directions) + + for dx, dy in directions: + nx, ny = x + dx, y + dy + + # Проверяем границы и что ещё не посещали + if (0 <= nx < self.width and 0 <= ny < self.height + and (nx, ny) not in visited): + + # Убираем стену между текущей и новой клеткой + wall_x = x + dx // 2 + wall_y = y + dy // 2 + maze.set_cell(wall_x, wall_y, Cell(wall_x, wall_y, is_wall=False)) + + # Рекурсия в новую клетку + carve(nx, ny) + + # Начинаем с (1, 1) — нечётные координаты для коридоров + carve(1, 1) + + # Шаг 3: Устанавливаем старт и выход в углах лабиринта + maze.set_cell(1, 1, Cell(1, 1, is_wall=False, is_start=True)) + maze.set_cell( + self.width - 2, self.height - 2, + Cell(self.width - 2, self.height - 2, is_wall=False, is_exit=True) + ) + + return maze \ No newline at end of file diff --git a/pogodinda/lab2/src/maze_solver.py b/pogodinda/lab2/src/maze_solver.py new file mode 100644 index 0000000..295fcf6 --- /dev/null +++ b/pogodinda/lab2/src/maze_solver.py @@ -0,0 +1,86 @@ +import time +from dataclasses import dataclass +from typing import List, Optional + +from maze import Maze, Cell +from pathfinding import PathFindingStrategy + + +@dataclass +class SearchStats: + """Статистика поиска пути.""" + time_ms: float # время выполнения в миллисекундах + visited_cells: int # сколько клеток посетил алгоритм + path_length: int # длина найденного пути + algorithm_name: str # какой алгоритм использовался + maze_name: str # название лабиринта + + +class MazeSolver: + """ + Оркестратор поиска пути. + Использует паттерн Strategy для переключения алгоритмов. + """ + + def __init__(self, maze: Maze, strategy: PathFindingStrategy = None): + self.maze = maze + self.strategy = strategy + self._observers = [] # для паттерна Observer (Этап 5) + + def set_strategy(self, strategy: PathFindingStrategy): + """ + Динамическая смена алгоритма. + Без паттерна Strategy пришлось бы переписывать этот метод + под каждый новый алгоритм. + """ + self.strategy = strategy + + def add_observer(self, observer): + """Добавление наблюдателя (подготовка к Этапу 5).""" + self._observers.append(observer) + + def _notify_observers(self, event: str): + """Уведомляет всех наблюдателей о событии.""" + for observer in self._observers: + observer.update(event) + + def solve(self, maze_name: str = "unnamed") -> SearchStats: + """ + Выполняет поиск пути и возвращает статистику. + + Args: + maze_name: название лабиринта для отчёта + + Returns: + SearchStats с результатами поиска + """ + if not self.strategy: + raise ValueError("Стратегия не установлена! Вызовите set_strategy()") + + # Уведомляем наблюдателей + self._notify_observers("search_started") + + # Замер времени + start_time = time.perf_counter() + + # Запускаем алгоритм (Strategy делает всю работу) + path, visited_count = self.strategy.find_path( + self.maze, self.maze.start, self.maze.exit + ) + + # Останавливаем замер + end_time = time.perf_counter() + time_ms = (end_time - start_time) * 1000 + + # Уведомляем о результате + event = "path_found" if path else "no_path" + self._notify_observers(event) + + # Формируем статистику + return SearchStats( + time_ms=time_ms, + visited_cells=visited_count, + path_length=len(path), + algorithm_name=self.strategy.get_name(), + maze_name=maze_name + ) \ No newline at end of file diff --git a/pogodinda/lab2/src/observer.py b/pogodinda/lab2/src/observer.py new file mode 100644 index 0000000..5b3ecec --- /dev/null +++ b/pogodinda/lab2/src/observer.py @@ -0,0 +1,54 @@ +from abc import ABC, abstractmethod +from typing import List, Optional + +from maze import Maze, Cell + + +class Observer(ABC): + """Интерфейс наблюдателя (Observer pattern).""" + + @abstractmethod + def update(self, event: str): + pass + + +class ConsoleView(Observer): + """ + Консольное представление лабиринта. + """ + + def __init__(self): + self.events: List[str] = [] + + def update(self, event: str): + """Получаем уведомление о событии.""" + self.events.append(event) + print(f"[Observer] Событие: {event}") + + def render(self, maze: Maze, player_position: Cell = None, path: List[Cell] = None): + """ + Отрисовка лабиринта в консоли. + """ + path_set = set(path) if path else set() + + for y in range(maze.height): + row = [] + for x in range(maze.width): + cell = maze.get_cell(x, y) + + if player_position and cell == player_position: + row.append('P') + elif cell in path_set: + row.append('*') + else: + row.append(str(cell)) + + print(''.join(row)) + print() + + def render_stats(self, stats): + """Отрисовка статистики поиска.""" + print(f"Алгоритм: {stats.algorithm_name}") + print(f"Время: {stats.time_ms:.4f} мс") + print(f"Посещено клеток: {stats.visited_cells}") + print(f"Длина пути: {stats.path_length}") \ No newline at end of file diff --git a/pogodinda/lab2/src/pathfinding.py b/pogodinda/lab2/src/pathfinding.py new file mode 100644 index 0000000..44d3070 --- /dev/null +++ b/pogodinda/lab2/src/pathfinding.py @@ -0,0 +1,183 @@ +from abc import ABC, abstractmethod +from typing import List, Tuple +from collections import deque +import heapq +from maze import Maze, Cell + + +class PathFindingStrategy(ABC): + """Интерфейс стратегии поиска пути (Strategy pattern).""" + + @abstractmethod + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + """ + Возвращает: + - путь (список клеток от старта до выхода) + - количество посещённых клеток + Если пути нет — возвращает ([], 0). + """ + pass + + @abstractmethod + def get_name(self) -> str: + """Название алгоритма для отчёта.""" + pass + + +class BFSStrategy(PathFindingStrategy): + """BFS — поиск в ширину. Гарантирует кратчайший путь по числу шагов.""" + + def get_name(self) -> str: + return "BFS" + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + if not start or not exit: + return [], 0 + + # Очередь: (текущая_клетка, путь_до_неё) + queue = deque([(start, [start])]) + visited = {start} + visited_count = 1 + + while queue: + current, path = queue.popleft() + + if current == exit: + return path, visited_count + + # Проверяем всех соседей + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + visited_count += 1 + queue.append((neighbor, path + [neighbor])) + + return [], visited_count + + +class DFSStrategy(PathFindingStrategy): + """DFS — поиск в глубину. Быстрый, но путь не обязательно кратчайший.""" + + def get_name(self) -> str: + return "DFS" + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + if not start or not exit: + return [], 0 + + # Стек: (текущая_клетка, путь_до_неё) + stack = [(start, [start])] + visited = {start} + visited_count = 1 + + while stack: + current, path = stack.pop() + + if current == exit: + return path, visited_count + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + visited_count += 1 + stack.append((neighbor, path + [neighbor])) + + return [], visited_count + + +class AStarStrategy(PathFindingStrategy): + """ + A* — поиск с эвристикой. + Использует приоритетную очередь (кучу) и манхэттенское расстояние. + """ + + def get_name(self) -> str: + return "A*" + + def _heuristic(self, cell: Cell, exit: Cell) -> int: + """Манхэттенское расстояние: |x1-x2| + |y1-y2|.""" + if not cell or not exit: + return 0 + return abs(cell.x - exit.x) + abs(cell.y - exit.y) + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + if not start or not exit: + return [], 0 + + # Куча: (f_score, счётчик, клетка, путь, g_score) + # f = g + h, где g — пройденный путь, h — эвристика + counter = 0 + open_set = [(self._heuristic(start, exit), counter, start, [start], 0)] + visited = set() + visited_count = 0 + g_scores = {start: 0} + + while open_set: + _, _, current, path, g_score = heapq.heappop(open_set) + + if current in visited: + continue + + visited.add(current) + visited_count += 1 + + if current == exit: + return path, visited_count + + for neighbor in maze.get_neighbors(current): + if neighbor in visited: + continue + + tentative_g = g_score + 1 + + if neighbor not in g_scores or tentative_g < g_scores[neighbor]: + g_scores[neighbor] = tentative_g + f_score = tentative_g + self._heuristic(neighbor, exit) + counter += 1 + heapq.heappush(open_set, (f_score, counter, neighbor, path + [neighbor], tentative_g)) + + return [], visited_count + + +class DijkstraStrategy(PathFindingStrategy): + """ + Дейкстра — для взвешенных графов. + В базовом варианте (вес=1) работает как BFS, но с приоритетной очередью. + """ + + def get_name(self) -> str: + return "Dijkstra" + + def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]: + if not start or not exit: + return [], 0 + + # Куча: (расстояние, счётчик, клетка, путь) + counter = 0 + pq = [(0, counter, start, [start])] + distances = {start: 0} + visited = set() + visited_count = 0 + + while pq: + dist, _, current, path = heapq.heappop(pq) + + if current in visited: + continue + + visited.add(current) + visited_count += 1 + + if current == exit: + return path, visited_count + + for neighbor in maze.get_neighbors(current): + weight = neighbor.weight + new_dist = dist + weight + + if neighbor not in distances or new_dist < distances[neighbor]: + distances[neighbor] = new_dist + counter += 1 + heapq.heappush(pq, (new_dist, counter, neighbor, path + [neighbor])) + + return [], visited_count \ No newline at end of file diff --git a/pogodinda/lab2/tests/__init__.py b/pogodinda/lab2/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pogodinda/lab2/tests/test_builder.py b/pogodinda/lab2/tests/test_builder.py new file mode 100644 index 0000000..237b45a --- /dev/null +++ b/pogodinda/lab2/tests/test_builder.py @@ -0,0 +1,26 @@ +from src.maze_builder import TextFileMazeBuilder, RandomMazeBuilder +# Тест 1: Загрузка из файла +print("=" * 50) +print("ТЕСТ 1: Загрузка из файла") +print("=" * 50) + +builder = TextFileMazeBuilder() +maze = builder.build_from_file("data/demo_maze.txt") + +print(maze) +print(f"\nРазмер: {maze.width}x{maze.height}") +print(f"Старт: ({maze.start.x}, {maze.start.y})") +print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + +# Тест 2: Случайный лабиринт +print("\n" + "=" * 50) +print("ТЕСТ 2: Случайный лабиринт 21x11") +print("=" * 50) + +random_builder = RandomMazeBuilder(21, 11) +random_maze = random_builder.build_from_file() + +print(random_maze) +print(f"\nРазмер: {random_maze.width}x{random_maze.height}") +print(f"Старт: ({random_maze.start.x}, {random_maze.start.y})") +print(f"Выход: ({random_maze.exit.x}, {random_maze.exit.y})") \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_maze.py b/pogodinda/lab2/tests/test_maze.py new file mode 100644 index 0000000..a5ddbfb --- /dev/null +++ b/pogodinda/lab2/tests/test_maze.py @@ -0,0 +1,19 @@ +from src.maze import Maze, Cell + +maze = Maze(5, 5) + +# Заполняем границы стенами +for y in range(5): + for x in range(5): + if x == 0 or x == 4 or y == 0 or y == 4: + maze.set_cell(x, y, Cell(x, y, is_wall=True)) + +# Старт, выход, одна стена внутри +maze.set_cell(1, 1, Cell(1, 1, is_start=True)) +maze.set_cell(3, 3, Cell(3, 3, is_exit=True)) +maze.set_cell(2, 2, Cell(2, 2, is_wall=True)) + +print(maze) +print("Старт:", maze.start) +print("Выход:", maze.exit) +print("Соседи старта:", maze.get_neighbors(maze.start)) \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_observer_command.py b/pogodinda/lab2/tests/test_observer_command.py new file mode 100644 index 0000000..d88636e --- /dev/null +++ b/pogodinda/lab2/tests/test_observer_command.py @@ -0,0 +1,45 @@ +from src.maze_builder import TextFileMazeBuilder +from src.maze_solver import MazeSolver +from src.pathfinding import AStarStrategy +from src.observer import ConsoleView +from src.commands import Player, MoveCommand + +# Загружаем лабиринт +builder = TextFileMazeBuilder() +maze = builder.build_from_file("demo_maze.txt") + +print("=" * 50) +print("ТЕСТ OBSERVER") +print("=" * 50) + +solver = MazeSolver(maze, AStarStrategy()) +console = ConsoleView() +solver.add_observer(console) + +stats = solver.solve("demo_maze") +console.render_stats(stats) + +print(f"\nСобытия: {console.events}") + +print("\n" + "=" * 50) +print("ТЕСТ COMMAND") +print("=" * 50) + +player = Player(maze.start) +print(f"Начальная позиция: {player}") +console.render(maze, player.current_cell) + +cmd1 = MoveCommand(player, maze, 'S') +success = cmd1.execute() +print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}") + +cmd2 = MoveCommand(player, maze, 'S') +success = cmd2.execute() +print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}") + +console.render(maze, player.current_cell) + +print("\nОтмена последнего хода:") +cmd2.undo() +print(f"После undo: {player}") +console.render(maze, player.current_cell) \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_solver.py b/pogodinda/lab2/tests/test_solver.py new file mode 100644 index 0000000..667d498 --- /dev/null +++ b/pogodinda/lab2/tests/test_solver.py @@ -0,0 +1,58 @@ +from src.maze_builder import TextFileMazeBuilder +from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy +from src.maze_solver import MazeSolver + +# Загружаем лабиринт +builder = TextFileMazeBuilder() +maze = builder.build_from_file("demo_maze.txt") + +print("Лабиринт:") +print(maze) +print(f"\nСтарт: ({maze.start.x}, {maze.start.y})") +print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + +# Создаём solver без стратегии +solver = MazeSolver(maze) + +# Тест 1: BFS +print(f"\n{'='*50}") +print("ТЕСТ 1: BFS") +solver.set_strategy(BFSStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 2: DFS +print(f"\n{'='*50}") +print("ТЕСТ 2: DFS") +solver.set_strategy(DFSStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 3: A* +print(f"\n{'='*50}") +print("ТЕСТ 3: A*") +solver.set_strategy(AStarStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 4: Дейкстра +print(f"\n{'='*50}") +print("ТЕСТ 4: Дейкстра") +solver.set_strategy(DijkstraStrategy()) +stats = solver.solve("demo_maze") +print(f"Время: {stats.time_ms:.4f} мс") +print(f"Посещено: {stats.visited_cells}") +print(f"Длина пути: {stats.path_length}") + +# Тест 5: Динамическая смена алгоритма +print(f"\n{'='*50}") +print("ТЕСТ 5: Смена алгоритма на лету") +print("Было:", solver.strategy.get_name()) +solver.set_strategy(BFSStrategy()) +print("Стало:", solver.strategy.get_name()) \ No newline at end of file diff --git a/pogodinda/lab2/tests/test_strategy.py b/pogodinda/lab2/tests/test_strategy.py new file mode 100644 index 0000000..2698d45 --- /dev/null +++ b/pogodinda/lab2/tests/test_strategy.py @@ -0,0 +1,34 @@ +from src.maze_builder import TextFileMazeBuilder +from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy + +# Загружаем лабиринт +builder = TextFileMazeBuilder() +maze = builder.build_from_file("data/demo_maze.txt") + +print("Лабиринт:") +print(maze) +print(f"\nСтарт: ({maze.start.x}, {maze.start.y})") +print(f"Выход: ({maze.exit.x}, {maze.exit.y})") + +# Тестируем все алгоритмы +strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()] + +for strategy in strategies: + print(f"\n{'='*40}") + print(f"Алгоритм: {strategy.get_name()}") + print('='*40) + + path, visited = strategy.find_path(maze, maze.start, maze.exit) + + print(f"Посещено клеток: {visited}") + print(f"Длина пути: {len(path)}") + + if path: + print("Путь найден!") # ← убрал f, или добавь переменную + if len(path) > 10: + print(f"Начало: {[(c.x, c.y) for c in path[:5]]}...") + print(f"Конец: ...{[(c.x, c.y) for c in path[-5:]]}") + else: + print(f"Путь: {[(c.x, c.y) for c in path]}") + else: + print("Путь не найден") \ No newline at end of file