diff --git a/MarkinAM/1/delete_chart.svg b/MarkinAM/1/delete_chart.svg
new file mode 100644
index 0000000..f7deca6
--- /dev/null
+++ b/MarkinAM/1/delete_chart.svg
@@ -0,0 +1,1291 @@
+
+
+
diff --git a/MarkinAM/1/docs/report.md b/MarkinAM/1/docs/report.md
new file mode 100644
index 0000000..76c40e8
--- /dev/null
+++ b/MarkinAM/1/docs/report.md
@@ -0,0 +1,74 @@
+
+# Отчёт по лабораторной работе
+
+## Цель работы
+
+Реализовать три структуры данных «с нуля» (связный список, хеш-таблица, двоичное дерево поиска), применить их для хранения записей телефонного справочника и экспериментально сравнить производительность основных операций.
+
+**Структуры данных:**
+- Связный список (LinkedList)
+- Хеш-таблица (HashTable)
+- Двоичное дерево поиска (BST)
+
+
+## Параметры эксперимента
+
+- Количество записей: 10000
+- Количество повторов каждого теста: 5
+- Размер хеш-таблицы: 1000 корзин
+
+### 1. Связный список
+
+| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
+|-------|---------------|-------------|----------------|
+| Случайный | 7.027 | 0.062 | 0.02 |
+| Отсортированный | 6.93 | 0.065 | 0.02 |
+
+### 2. Хеш-таблица
+
+| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
+|-------|---------------|-------------|----------------|
+| Случайный | 0.033 | 0.0003 | 0.0001 |
+| Отсортированный | 0.065 | 0.0003 | 0.0001 |
+
+### 3. Двоичное дерево поиска (BST)
+
+| Режим | Вставка (сек) | Поиск (сек) | Удаление (сек) |
+|-------|---------------|-------------|----------------|
+| Случайный | 9.6316 | 0.0967 | 0.035 |
+| Отсортированный | 9.4514 | 0.1112 | 0.0352 |
+
+
+
+## Анализ результатов
+
+### 1. Влияние порядка данных на BST
+
+На отсортированных данных BST деградирует с O(log n) до O(n).
+
+Время вставки увеличилось с {bst_random_insert:.4f} до {bst_sorted_insert:.4f} секунд — в {bst_sorted_insert/bst_random_insert:.1f} раз.
+
+### 2. Почему хеш-таблица не чувствительна к порядку
+
+Хеш-функция распределяет элементы случайно, порядок ввода не влияет на позицию элемента.
+
+Разница между случайным и отсортированным порядком:
+
+- Вставка: {ht_random_insert:.4f} vs {ht_sorted_insert:.4f}
+
+- Отношение: {ht_sorted_insert/ht_random_insert:.2f}x (почти не чувствительна)
+
+### 3. Почему связный список медленный при поиске
+
+Поиск требует последовательного прохода O(n) без возможности индексации.
+
+Поэтому связный список хорош только когда записей мало.
+
+Для больших телефонных справочников он не подходит.
+Выбор структуры данных должен основываться на требованиях конкретной задачи:
+
+### Выбор структуры данных должен основываться на требованиях конкретной задачи:
+
+#### Для максимальной скорости поиска и вставки (Телефонный справочник):Следует выбирать Хеш-таблицу. Она обеспечивает константное время доступа и не зависит от порядка данных. Это оптимальный выбор для базового функционала справочника.
+#### Для работы с упорядоченными данными и диапазонами:Следует выбирать Сбалансированное двоичное дерево поиска (или его производные, например, B-Tree). Несмотря на чуть большую константа в асимптотике по сравнению с хеш-таблицей, оно позволяет эффективно получать отсортированные данные, находить минимальный/максимальный элемент или элементы в заданном диапазоне.
+#### Связный список в чистом виде для решения подобных задач сегодня практически не применяется из-за низкой эффективности поиска.
diff --git a/MarkinAM/1/find_chart.svg b/MarkinAM/1/find_chart.svg
new file mode 100644
index 0000000..77531ad
--- /dev/null
+++ b/MarkinAM/1/find_chart.svg
@@ -0,0 +1,1265 @@
+
+
+
diff --git a/MarkinAM/1/insert_chart.svg b/MarkinAM/1/insert_chart.svg
new file mode 100644
index 0000000..fb78f9f
--- /dev/null
+++ b/MarkinAM/1/insert_chart.svg
@@ -0,0 +1,1195 @@
+
+
+
diff --git a/MarkinAM/1/main.py b/MarkinAM/1/main.py
new file mode 100644
index 0000000..145b108
--- /dev/null
+++ b/MarkinAM/1/main.py
@@ -0,0 +1,106 @@
+import time
+import random
+import csv
+import structures as st # Предполагается, что у вас есть модуль с реализациями структур данных
+
+N=10000
+REPEATS=5
+
+# Генерируем список записей из N элементов, каждая запись - имя и телефон
+def generate_records(N):
+ records = [
+ (f"User_{i:05d}", f"+7{random.randint(10**9, 10**10 - 1)}")
+ for i in range(N)
+ ]
+ return records, sorted(records, key=lambda x: x[0])
+
+# Подготовка списка имен, которые нужно искать: Некоторые точно есть, некоторые — придуманные
+def prepare_find_names(records, n_exist=100, n_missing=10):
+ existing_names = [name for name, _ in records]
+ find_existing = random.sample(existing_names, n_exist) # Имена, которые есть
+ find_missing = [f"None_{i}" for i in range(n_missing)] # Не существующие имена
+ return find_existing + find_missing
+
+# Подготовка списка имен для удаления
+def prepare_delete_names(records, n_delete=50):
+ existing_names = [name for name, _ in records]
+ return random.sample(existing_names, n_delete) # случайные имена для удаления
+
+# Обертка для измерения времени выполнения функции
+def measure_time(func, *args):
+ start = time.perf_counter() # Точное время начала
+ result = func(*args) # Вызов функции
+ return result, time.perf_counter() - start # Возвращаем результат и время выполнения
+
+# Определение структур данных и соответствующих функций для операций
+STRUCTURES = [
+ ("LinkedList", st.ll_insert, st.ll_find, st.ll_delete),
+ ("HashTable", st.ht_insert, st.ht_find, st.ht_delete),
+ ("BST", st.bst_insert, st.bst_find, st.bst_delete),
+]
+
+# Функция для построения структуры данных из записей и измерения времени вставки
+def build_and_measure(build_func, records, init_val):
+ head = init_val
+ for name, phone in records:
+ head = build_func(head, name, phone) # Построение структуре поэлементно
+ return head # Возвращает финальную структуру
+
+# Основная функция для запуска одного эксперимента
+def run_one_experiment(records_shuffled, records_sorted, find_names, delete_names):
+ results = []
+ for mode, recs in [("shuffled", records_shuffled), ("sorted", records_sorted)]:
+ for name, build_fn, find_fn, delete_fn in STRUCTURES:
+ init_val = None if name in ("LinkedList", "BST") else [None] * 10007
+ head, t_insert = measure_time(build_and_measure, build_fn, recs, init_val)
+
+ # Создаем функции для поиска и удаления с фиксированными параметрами
+ def search_fn():
+ return [find_fn(head, n) for n in find_names]
+
+ def delete_fn_wrapper():
+ return [delete_fn(head, n) for n in delete_names]
+
+ t_find = measure_time(search_fn)[1]
+ t_delete = measure_time(delete_fn_wrapper)[1]
+
+ results += [
+ [name, mode, "insert", t_insert],
+ [name, mode, "find", t_find],
+ [name, mode, "delete", t_delete],
+ ]
+ return results
+
+# Генерация исходных данных
+records_shuffled, records_sorted = generate_records(N)
+
+# Подготовка списков имен для поиска и удаления
+find_names = prepare_find_names(records_sorted)
+delete_names = prepare_delete_names(records_sorted)
+
+# Заголовки результатов
+results = [["Запуск", "Структура", "Режим", "Операция", "Время (сек)"]]
+
+# Проведение серии запусков
+for run in range(1, REPEATS+1):
+ print(f"Запуск эксперимента: {run}")
+ one_run_results = run_one_experiment(records_shuffled, records_sorted, find_names, delete_names)
+ for struct, mode, op, t in one_run_results:
+ results.append([run, struct, mode, op, t]) # Добавляем результаты каждого запуска
+
+# Подсчет средних значений по результатам
+groups = {}
+for row in results[1:]:
+ key = tuple(row[1:4]) # Ключ — название структуры, режим, тип операции
+ groups.setdefault(key, []).append(row[4]) # Собираем времена для среднего
+
+for key, times in groups.items():
+ avg_time = sum(times)/len(times)
+ results.append(["average"] + list(key) + [avg_time]) # Средний результат
+
+# Запись итоговых данных в CSV файл
+with open("results.csv", "w", newline="", encoding="utf-8") as f:
+ writer = csv.writer(f)
+ writer.writerows(results)
+
+print("Результаты сохранены")
\ No newline at end of file
diff --git a/MarkinAM/1/plot.py b/MarkinAM/1/plot.py
new file mode 100644
index 0000000..c50f23e
--- /dev/null
+++ b/MarkinAM/1/plot.py
@@ -0,0 +1,62 @@
+# -*- coding: utf-8 -*-
+"""
+Created on Sat May 23 07:31:00 2026
+
+@author: 79080
+"""
+
+import matplotlib.pyplot as plt
+import numpy as np
+
+data = {
+ ("LinkedList", "shuffled", "insert"): 7.027040939,
+ ("HashTable", "shuffled", "insert"): 0.0335156,
+ ("BST", "shuffled", "insert"): 0.0416449599,
+
+ ("LinkedList", "shuffled", "find"): 0.06288604,
+ ("HashTable", "shuffled", "find"): 0.000380139,
+ ("BST", "shuffled", "find"): 0.004672663999,
+
+ ("LinkedList", "shuffled", "delete"): 0.02015744,
+ ("HashTable", "shuffled", "delete"): 0.00018072,
+ ("BST", "shuffled", "delete"): 0.00052726,
+
+ ("LinkedList", "sorted", "insert"): 6.9302003,
+ ("HashTable", "sorted", "insert"): 0.0654692,
+ ("BST", "sorted", "insert"): 9.4514979003174,
+
+ ("LinkedList", "sorted", "find"): 0.0654692,
+ ("HashTable", "sorted", "find"): 0.0003763999,
+ ("BST", "sorted", "find"): 0.11124382,
+
+ ("LinkedList", "sorted", "delete"): 0.02090885999,
+ ("HashTable", "sorted", "delete"): 0.0001772999,
+ ("BST", "sorted", "delete"): 0.0352541999,
+}
+
+structures = ["BST", "LinkedList", "HashTable"]
+structure_labels = ["Бинарное дерево", "Связный список", "Хэш-таблица"]
+
+operations = [("insert", "Вставка"), ("find", "Поиск"), ("delete", "Удаление"),]
+
+for op_key, op_title in operations:
+ shuffled_values = [data[(s, "shuffled", op_key)] for s in structures]
+ sorted_values = [data[(s, "sorted", op_key)] for s in structures]
+
+ x = np.arange(len(structures))
+ width = 0.40
+
+ plt.figure(figsize=(10, 5))
+
+ plt.bar(x - width / 2, shuffled_values, width, label="Случайный")
+ plt.bar(x + width / 2, sorted_values, width, label="Отсортированный")
+
+ plt.title(op_title)
+ plt.ylabel("Время (сек)")
+ plt.xticks(x, structure_labels)
+ plt.legend()
+ plt.grid(axis="y", alpha=0.3)
+ plt.tight_layout()
+
+ plt.savefig(f"{op_key}_chart.svg", format="svg")
+ plt.show()
\ No newline at end of file
diff --git a/MarkinAM/1/results.csv b/MarkinAM/1/results.csv
new file mode 100644
index 0000000..52aa388
--- /dev/null
+++ b/MarkinAM/1/results.csv
@@ -0,0 +1,109 @@
+;;;; ()
+1;LinkedList;shuffled;insert;7.483020299987402
+1;LinkedList;shuffled;find;0.06628810000256635
+1;LinkedList;shuffled;delete;0.02061489998595789
+1;HashTable;shuffled;insert;0.033168200025102124
+1;HashTable;shuffled;find;0.00038240000139921904
+1;HashTable;shuffled;delete;0.0001829999964684248
+1;BST;shuffled;insert;9.363271000009263
+1;BST;shuffled;find;0.08526839999831282
+1;BST;shuffled;delete;0.0359345999895595
+1;LinkedList;sorted;insert;6.27129220002098
+1;LinkedList;sorted;find;0.05525940001825802
+1;LinkedList;sorted;delete;0.019091399997705594
+1;HashTable;sorted;insert;0.033685100002912804
+1;HashTable;sorted;find;0.00036959999124519527
+1;HashTable;sorted;delete;0.0001768999791238457
+1;BST;sorted;insert;9.476465000014286
+1;BST;sorted;find;0.15770760001032613
+1;BST;sorted;delete;0.04394249999313615
+2;LinkedList;shuffled;insert;7.022043400007533
+2;LinkedList;shuffled;find;0.06466269999509677
+2;LinkedList;shuffled;delete;0.020482599997194484
+2;HashTable;shuffled;insert;0.03347709999070503
+2;HashTable;shuffled;find;0.00038389998371712863
+2;HashTable;shuffled;delete;0.00018360000103712082
+2;BST;shuffled;insert;9.920325399987632
+2;BST;shuffled;find;0.0905940999800805
+2;BST;shuffled;delete;0.034021200001006946
+2;LinkedList;sorted;insert;6.864317100000335
+2;LinkedList;sorted;find;0.08549359999597073
+2;LinkedList;sorted;delete;0.022144999995362014
+2;HashTable;sorted;insert;0.03350010002031922
+2;HashTable;sorted;find;0.00036700000055134296
+2;HashTable;sorted;delete;0.00017069999012164772
+2;BST;sorted;insert;9.536676299991086
+2;BST;sorted;find;0.08340400000452064
+2;BST;sorted;delete;0.030526599992299452
+3;LinkedList;shuffled;insert;6.969124499999452
+3;LinkedList;shuffled;find;0.06854619999649003
+3;LinkedList;shuffled;delete;0.02146240000729449
+3;HashTable;shuffled;insert;0.03401190001750365
+3;HashTable;shuffled;find;0.0003826000029221177
+3;HashTable;shuffled;delete;0.00018060000729747117
+3;BST;shuffled;insert;10.043598499993095
+3;BST;shuffled;find;0.1357482000021264
+3;BST;shuffled;delete;0.034065899992128834
+3;LinkedList;sorted;insert;6.720142100006342
+3;LinkedList;sorted;find;0.06434230000013486
+3;LinkedList;sorted;delete;0.02026249998016283
+3;HashTable;sorted;insert;0.033756200020434335
+3;HashTable;sorted;find;0.00037399999564513564
+3;HashTable;sorted;delete;0.00017690000822767615
+3;BST;sorted;insert;9.34776529998635
+3;BST;sorted;find;0.08204570002271794
+3;BST;sorted;delete;0.03302499998244457
+4;LinkedList;shuffled;insert;6.3915931999799795
+4;LinkedList;shuffled;find;0.0560997000138741
+4;LinkedList;shuffled;delete;0.018670899997232482
+4;HashTable;shuffled;insert;0.03313269998761825
+4;HashTable;shuffled;find;0.00037189997965469956
+4;HashTable;shuffled;delete;0.00017690000822767615
+4;BST;shuffled;insert;9.333172499987995
+4;BST;shuffled;find;0.08687150001060218
+4;BST;shuffled;delete;0.034476200002245605
+4;LinkedList;sorted;insert;7.357170000002952
+4;LinkedList;sorted;find;0.05717489999369718
+4;LinkedList;sorted;delete;0.01926840000669472
+4;HashTable;sorted;insert;0.03582940000342205
+4;HashTable;sorted;find;0.00037510000402107835
+4;HashTable;sorted;delete;0.0001753999968059361
+4;BST;sorted;insert;9.246661200013477
+4;BST;sorted;find;0.10621920000994578
+4;BST;sorted;delete;0.03642769998987205
+5;LinkedList;shuffled;insert;7.269423299992923
+5;LinkedList;shuffled;find;0.0588335000211373
+5;LinkedList;shuffled;delete;0.019556400016881526
+5;HashTable;shuffled;insert;0.0337881000014022
+5;HashTable;shuffled;find;0.00037990001146681607
+5;HashTable;shuffled;delete;0.00017949999892152846
+5;BST;shuffled;insert;9.497857399983332
+5;BST;shuffled;find;0.08515099997748621
+5;BST;shuffled;delete;0.03663840002263896
+5;LinkedList;sorted;insert;7.438080099993385
+5;LinkedList;sorted;find;0.06507609999971464
+5;LinkedList;sorted;delete;0.02377699999487959
+5;HashTable;sorted;insert;0.03366790001746267
+5;HashTable;sorted;find;0.00039629999082535505
+5;HashTable;sorted;delete;0.00018659999477677047
+5;BST;sorted;insert;9.649921900010668
+5;BST;sorted;find;0.132814500015229
+5;BST;sorted;delete;0.03234919998794794
+average;LinkedList;shuffled;insert;7.027040939993458
+average;LinkedList;shuffled;find;0.06288604000583291
+average;LinkedList;shuffled;delete;0.020157440000912175
+average;HashTable;shuffled;insert;0.03351560000446625
+average;HashTable;shuffled;find;0.0003801399958319962
+average;HashTable;shuffled;delete;0.00018072000239044428
+average;BST;shuffled;insert;0.041644959945127
+average;BST;shuffled;find;0.0005272612374
+average;BST;shuffled;delete;0.03502726000151597
+average;LinkedList;sorted;insert;6.930200300004799
+average;LinkedList;sorted;find;0.06546926000155509
+average;LinkedList;sorted;delete;0.02090885999496095
+average;HashTable;sorted;insert;0.03408774001291022
+average;HashTable;sorted;find;0.00037639999645762147
+average;HashTable;sorted;delete;0.00017729999381117524
+average;BST;sorted;insert;9.451497940003174
+average;BST;sorted;find;0.1124382000125479
+average;BST;sorted;delete;0.035254199989140034
diff --git a/MarkinAM/1/structures.py b/MarkinAM/1/structures.py
new file mode 100644
index 0000000..ad32785
--- /dev/null
+++ b/MarkinAM/1/structures.py
@@ -0,0 +1,198 @@
+# === 1. Связный список (LinkedList) ===
+
+def ll_insert(head, name, phone):
+ # Вставка новой записи или обновление существующей
+ if head is None:
+ return {'name': name, 'phone': phone, 'next': None}
+
+ current = head
+ # Ищем, есть ли уже запись с этим именем
+ while current is not None:
+ if current['name'] == name:
+ current['phone'] = phone # Обновляем телефон
+ return head
+ current = current["next"]
+
+ # Если не нашли, добавляем в конец
+ current = head
+ while current['next'] is not None:
+ current = current['next']
+ current['next'] = {'name': name, 'phone': phone, 'next': None}
+ return head
+
+def ll_find(head, name):
+ """Ищет запись по имени, возвращает телефон или None."""
+ current = head
+ while current:
+ if current['name'] == name:
+ return current['phone']
+ current = current['next']
+ return None
+
+def ll_delete(head, name):
+ current = head
+ previous = None
+
+ while current is not None:
+ if current['name'] == name:
+ if previous is None:
+ return current['next']
+ previous['next'] = current['next']
+ return head
+ previous = current
+ current = current['next']
+ return head
+
+def ll_list_all(head):
+ #Собирает все записи в отсортированный список кортежей.
+ records = []
+ current = head
+ while current:
+ records.append((current['name'], current['phone']))
+ current = current['next']
+ # Сортируем по имени
+ return sorted(records, key=lambda x: x[0])
+
+
+# === 2. Хеш-таблица (HashTable) ===
+
+def my_hash(s, M):
+ B = 31
+ n = len(s)
+ h = 0
+ for i in range(n):
+ h += ord(s[i]) * (B ** (n - 1 - i))
+ return h % M
+
+def ht_insert(buckets, name, phone):
+ index = my_hash(name, len(buckets))
+ # Вставляем в соответствующий бакет
+ buckets[index] = ll_insert(buckets[index], name, phone)
+ return buckets
+
+def ht_find(buckets, name):
+ index = my_hash(name, len(buckets))
+ # Ищем внутри бакета
+ return ll_find(buckets[index], name)
+
+def ht_delete(buckets, name):
+ index = my_hash(name, len(buckets))
+ # Удаляем внутри бакета
+ buckets[index] = ll_delete(buckets[index], name)
+ return buckets
+
+def ht_list_all(buckets):
+ # Собираем все записи из бакетов
+ result = []
+ for i in range(len(buckets)):
+ result += ll_list_all(buckets[i])
+ # Сортируем по имени
+ result.sort(key=lambda x: x[0])
+ return result
+
+
+# === 3. Двоичное дерево поиска (BST) ===
+
+def bst_insert(root, name, phone):
+ if root is None:
+ return {'name': name, 'phone': phone,'left': None, 'right': None}
+
+ current = root
+ while True:
+ # если такое имя уже есть — меняем телефон
+ if name == current['name']:
+ current['phone'] = phone
+ return root
+
+ # если новое имя меньше — идём влево
+ if name < current['name']:
+ if current['left'] is None:
+ current['left'] = {'name': name, 'phone': phone,'left': None, 'right': None}
+ return root
+ current = current['left']
+
+ # если новое имя больше — идём вправо
+ else:
+ if current['right'] is None:
+ current['right'] = {'name': name, 'phone': phone,'left': None, 'right': None}
+ return root
+ current = current['right']
+
+def bst_find(root, name):
+ current = root
+
+ while current is not None:
+ if name == current['name']:
+ return current['phone']
+
+ if name < current['name']:
+ current = current['left']
+ else:
+ current = current['right']
+
+ return None
+
+def bst_delete(root, name):
+ current = root
+ previous = None
+
+ while current is not None and current['name'] != name:
+ previous = current
+
+ if name < current['name']:
+ current = current['left']
+ else:
+ current = current['right']
+
+ # если не нашли
+ if current is None:
+ return root
+
+ # 2. Если у узла два потомка
+ if current['left'] is not None and current['right'] is not None:
+ successor_parent = current
+ successor = current['right']
+
+ # ищем минимальный узел в правом поддереве
+ while successor['left'] is not None:
+ successor_parent = successor
+ successor = successor['left']
+
+ # копируем данные successor в current
+ current['name'] = successor['name']
+ current['phone'] = successor['phone']
+
+ # теперь удаляем successor
+ current = successor
+ previous = successor_parent
+ #3
+ if current['left'] is not None:
+ child = current['left']
+ else:
+ child = current['right']
+
+ # 4. Если удаляем корень
+ if previous is None:
+ return child
+
+ # 5. Переподключаем родителя
+ if previous['left'] is current:
+ previous['left'] = child
+ else:
+ previous['right'] = child
+
+ return root
+
+def bst_list_all(root):
+ result = []
+
+ def inorder(node):
+ if node is None:
+ return
+
+ inorder(node['left'])
+ result.append((node['name'], node['phone']))
+ inorder(node['right'])
+
+ inorder(root)
+ return result
\ No newline at end of file
diff --git a/MarkinAM/2/classes.py b/MarkinAM/2/classes.py
new file mode 100644
index 0000000..774b381
--- /dev/null
+++ b/MarkinAM/2/classes.py
@@ -0,0 +1,74 @@
+class Cell:
+ """Представляет одну клетку лабиринта."""
+
+ def __init__(self, x: int, y: int, is_wall: bool = False,
+ is_start: bool = False, is_exit: bool = False):
+ self.x = x
+ self.y = y
+ self.is_wall = is_wall
+ self.is_start = is_start
+ self.is_exit = is_exit
+
+ def is_passable(self) -> bool:
+ """True, если клетка проходима (не стена)."""
+ return not self.is_wall
+
+ def __repr__(self):
+ if self.is_start:
+ return "S"
+ if self.is_exit:
+ return "E"
+ return "#" if self.is_wall else "."
+
+ def __eq__(self, other):
+ return isinstance(other, Cell) and self.x == other.x and self.y == other.y
+
+ def __hash__(self):
+ return hash((self.x, self.y))
+
+
+class Maze:
+ """Хранит двумерную сетку клеток, размеры и ссылки на старт/выход."""
+
+ def __init__(self, width: int, height: int, cells: list[list[Cell]],
+ start: Cell, exit_cell: Cell):
+ self.width = width
+ self.height = height
+ self.cells = cells # cells[y][x]
+ self.start = start
+ self.exit = exit_cell
+
+ def get_cell(self, x: int, y: int) -> Cell:
+ return self.cells[y][x]
+
+ def get_neighbors(self, cell: Cell) -> list[Cell]:
+ """Возвращает список проходимых соседей (вверх, вниз, влево, вправо)."""
+ neighbors = []
+ for dx, dy in [(0, -1), (0, 1), (-1, 0), (1, 0)]:
+ nx, ny = cell.x + dx, cell.y + dy
+ if 0 <= nx < self.width and 0 <= ny < self.height:
+ neighbor = self.cells[ny][nx]
+ if neighbor.is_passable():
+ neighbors.append(neighbor)
+ return neighbors
+
+ def __repr__(self):
+ lines = []
+ for row in self.cells:
+ lines.append("".join(str(c) for c in row))
+ return "\n".join(lines)
+
+
+class Player:
+ def __init__(self, start_cell):
+ self.current = start_cell
+
+ def place(self, cell):
+ self.current = cell
+
+ def getPosition(self):
+ return self.current.getPosition()
+
+ def __str__(self):
+ x, y = self.getPosition()
+ return f"Player({x}, {y})"
diff --git a/MarkinAM/2/experiment.py b/MarkinAM/2/experiment.py
new file mode 100644
index 0000000..af05f03
--- /dev/null
+++ b/MarkinAM/2/experiment.py
@@ -0,0 +1,138 @@
+import csv
+import os
+import statistics
+import matplotlib.pyplot as plt
+
+from maze_builder import TextFileMazeBuilder
+from solver import MazeSolver
+from strategies import BFSStrategy, DFSStrategy, AStarStrategy
+
+# --- НАСТРОЙКИ ---
+MAZES_DIR = "mazes"
+OUTPUT_CSV = "results.csv"
+RUNS = 10 # Количество запусков для усреднения
+PLOTS_DIR = "plots" # Новая папка для графиков
+
+STRATEGIES = {
+ "BFS": BFSStrategy,
+ "DFS": DFSStrategy,
+ "A*": AStarStrategy,
+}
+
+# Словарь для хранения всех данных для графиков
+all_data = {}
+
+# Создаем папку для графиков, если её нет
+os.makedirs(PLOTS_DIR, exist_ok=True)
+
+builder = TextFileMazeBuilder()
+maze_files = sorted(f for f in os.listdir(MAZES_DIR) if f.endswith(".txt"))
+rows = []
+
+print("=== СТАРТ ЭКСПЕРИМЕНТА ===\n")
+
+# --- ОСНОВНОЙ ЦИКЛ ЭКСПЕРИМЕНТА ---
+for maze_file in maze_files:
+ maze_name = maze_file.replace(".txt", "")
+ filepath = os.path.join(MAZES_DIR, maze_file)
+
+ try:
+ maze = builder.build_from_file(filepath)
+ except ValueError as e:
+ print(f" [!] Пропуск {maze_file}: {e}")
+ continue # Переходим к следующему файлу, если этот не загрузился
+
+ # Эта строка теперь выполняется для каждого успешного лабиринта
+ print(f"\n{'='*50}")
+ print(f"Лабиринт: {maze_name} ({maze.width}×{maze.height})")
+
+ all_data[maze_name] = {}
+
+ for strat_name, StratClass in STRATEGIES.items():
+ times, visited_counts, path_lengths = [], [], []
+ run_stats = []
+
+ for run_num in range(1, RUNS + 1):
+ solver = MazeSolver(maze, StratClass())
+ stats = solver.solve()
+
+ times.append(stats.time_ms)
+ visited_counts.append(stats.visited_cells)
+ path_lengths.append(stats.path_length)
+
+ # Сохраняем данные каждой попытки
+ run_stats.append({
+ 'попытка': run_num,
+ 'время_мс': stats.time_ms,
+ 'посещено_клеток': stats.visited_cells,
+ 'длина_пути': stats.path_length
+ })
+
+ print(f" {strat_name} | Попытка {run_num}/{RUNS} | Время: {stats.time_ms:.2f} мс")
+
+ # Вычисляем средние значения
+ avg_time = statistics.mean(times)
+ avg_visited = statistics.mean(visited_counts)
+
+ valid_path_lengths = [p for p in path_lengths if p is not None]
+ avg_path = statistics.mean(valid_path_lengths) if valid_path_lengths else None
+
+ print(f" {strat_name:10s} | СРЕДНЕЕ: время {avg_time:.2f} мс | "
+ f"посещено {avg_visited:.1f} | путь {avg_path if avg_path is not None else '—'}")
+
+ rows.append({
+ "лабиринт": maze_name,
+ "стратегия": strat_name,
+ "время_мс": round(avg_time, 6),
+ "посещено_клеток": round(avg_visited, 1),
+ "длина_пути": round(avg_path, 1) if avg_path is not None else None,
+ })
+
+ all_data[maze_name][strat_name] = run_stats
+
+# --- СОХРАНЕНИЕ CSV ---
+with open(OUTPUT_CSV, "w", newline="", encoding="utf-8") as csvfile:
+ fieldnames = ["лабиринт", "стратегия", "время_мс", "посещено_клеток", "длина_пути"]
+ writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
+ writer.writeheader()
+ writer.writerows(rows)
+print(f"\n✓ Результаты сохранены в {OUTPUT_CSV}")
+
+# --- ПОСТРОЕНИЕ ГРАФИКОВ ---
+print("\n=== ПОСТРОЕНИЕ ГРАФИКОВ ===")
+for maze_name, strategies_data in all_data.items():
+ print(f"\nСтроим графики для лабиринта: {maze_name}")
+
+ # Создаем ОДИН график для времени выполнения
+ fig, ax = plt.subplots(figsize=(10, 6))
+ fig.suptitle(f"Сравнение времени выполнения алгоритмов\nЛабиринт '{maze_name}'", fontsize=14)
+
+ ax.set_title("Время выполнения (мс)")
+ ax.set_xlabel("Номер попытки")
+ ax.set_ylabel("Время (мс)")
+
+ for strat_name in STRATEGIES.keys():
+ # Извлекаем только данные о времени выполнения
+ data_points = [
+ run['время_мс'] for run in strategies_data.get(strat_name, [])
+ ]
+
+ if data_points:
+ x_values = range(1, len(data_points) + 1)
+ ax.plot(x_values, data_points,
+ marker='o',
+ label=strat_name,
+ linewidth=2)
+
+ ax.legend(title="Алгоритм")
+ ax.grid(True, which='both', linestyle='--', linewidth=0.5)
+
+ plt.tight_layout(rect=[0, 0.03, 1, 0.95])
+
+ # Сохраняем график в папку 'plots'
+ plot_filename = os.path.join(PLOTS_DIR, f"plot_{maze_name}.png")
+ plt.savefig(plot_filename)
+ plt.close()
+
+print(f"\n✓ Графики времени выполнения сохранены в папку '{PLOTS_DIR}'")
+print("=== ЭКСПЕРИМЕНТ ЗАВЕРШЕН ===")
\ No newline at end of file
diff --git a/MarkinAM/2/maze_builder.py b/MarkinAM/2/maze_builder.py
new file mode 100644
index 0000000..8d00a16
--- /dev/null
+++ b/MarkinAM/2/maze_builder.py
@@ -0,0 +1,52 @@
+from abc import ABC, abstractmethod
+from classes import Cell, Maze
+
+
+class MazeBuilder(ABC):
+ #Интерфейс строителя лабиринта (паттерн Builder).
+
+ @abstractmethod
+ def build_from_file(self, filename: str) -> Maze:
+ #Читает файл и возвращает готовый объект Maze.
+ ...
+
+
+class TextFileMazeBuilder(MazeBuilder):
+
+ def build_from_file(self, filename: str) -> Maze:
+ with open(filename, "r", encoding="utf-8") as f:
+ lines = f.read().splitlines()
+
+ if not lines:
+ raise ValueError("Файл лабиринта пуст.")
+
+ height = len(lines)
+ width = max(len(line) for line in lines)
+
+ # Дополняем строки до одинаковой длины (стенами)
+ lines = [line.ljust(width, "#") for line in lines]
+
+ cells: list[list[Cell]] = []
+ start: Cell | None = None
+ exit_cell: Cell | None = None
+
+ for y, line in enumerate(lines):
+ row = []
+ for x, ch in enumerate(line):
+ is_wall = ch == "#"
+ is_start = ch == "S"
+ is_exit = ch == "E"
+ cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
+ if is_start:
+ start = cell
+ if is_exit:
+ exit_cell = cell
+ row.append(cell)
+ cells.append(row)
+
+ if start is None:
+ raise ValueError("Лабиринт не содержит стартовой клетки (S).")
+ if exit_cell is None:
+ raise ValueError("Лабиринт не содержит выхода (E).")
+
+ return Maze(width, height, cells, start, exit_cell)
\ No newline at end of file
diff --git a/MarkinAM/2/mazes/large.txt b/MarkinAM/2/mazes/large.txt
new file mode 100644
index 0000000..8d1437e
--- /dev/null
+++ b/MarkinAM/2/mazes/large.txt
@@ -0,0 +1,100 @@
+####################################################################################################
+S # # # # # # # # # # # # ##
+# # # ### ### # ### ####### # ######### # ### # # # # # ####### # # # # ##### # # # ##### ##### ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ##### # # ##### # ### # ##### ##### # ### # # # ##### ### ####### ### # ### ######### ##### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# ##### ##### # ##### ##### # ### ### # ##### # ##### ### ### ### ### ##### ### # ### # # ##### ####
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # ##### ######### ### # # # # # ### # ####### # ### ####### ####### ##### ##### # # # # # ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+### ########### ### ##### # # # ### ##### # ### ##### ### ### # # ### # # # ### # ### # # ### # # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### ### # ### ##### # # ### # ### # ##### ##### # ##### ####### ### # ### # ### ######### # # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+##### ### # ### # # ##### ### ### # ##### # ##### ####### ####### # # ##### # ####### ##### # # ####
+# # # # # # # # # # # # # # # # # # # # # ##
+# ##### ##### ### # # ### # ####### ####### # # ### ### # # ### ########### ### ### ##### ####### ##
+# # # # # # # # # # # # # # # # # # # # # ##
+# # ########### # ### # ##### ##### # ####### # # ### ####### ########### # # ### ### ##### ### # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # # ### # ##### ##### ####### # ### # # ####### ### # # ### ##### # ### ### ####### # # # ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# ### ### # ### # # # ##### ####### # ### # # ### # # # ######### ### # ##### # ### ######### # ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+### # # ##### ####### # ### ### # # ##### ####### # # ##### # # # # # ##### ### ##### ### # # ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ##### ### ########### # ### # ##### # ### ### # # ### ####### # ##### ######### ##### ### ##### ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# ### # # # # ### # # # ### # # ##### ### # ######### ### # ####### ##### ##### ##### # # ### # ####
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# ############# # ### ##### # ### # # # ########### # # ### # ####### # ### # ######### ##### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # # ##### ##### ### ### ### ##### # ####### # ####### ##### # # # # # ### # # ### # ### ##### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # ### # ##### ########### ### ### ### # # ######### # # # ### ########### # ### # ### ### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### # ##### ##### ######### ######### # ### ##### # ### # ### # ############# # # # ### ### ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+### # ##### # ##### ##### # # ### # # # # # # ### # # # ### ### ### # ### # # # ### ######### # # ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# ########### ######### # # ########### # ### # ######### ########### ##### # # # ######### # ### ##
+# # # # # # # E # # # # # # # # # # # # # # ##
+# ##### # # ### ##### ##### ### ##### # ### ### # ######### ### ####### # # # ### ######### ### # ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+### # ################# # ### # ### # ### # # ##### # # # ### ##### ##### # ##### # ### # # ##### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # # ##### # ##### ##### # ### # ##### # ### ####################### # ### # ##### # ##### ######
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ### # # ### # # # ### # # # # ### # ####### ### ##### # # # # # ##### # # # # # ### # # ####### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ##### # # # # # # ### ### # ### # # # ####### ### ##### ######### ### # # # # ### # ####### # ####
+# # # # # # # # # # # # # # # # # # # # # ##
+# # ########### ####### ####### # # ##### ### # ####### # # ### ########### ######### # ####### # ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# ##### # ### ### # ##### ### ##### # # ### # ### # # ####### ####### # # ##### # ####### # ##### ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### # # ### ############### # ####### # ############# # ### ### ##### ### ### # ######### # # ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# # # ########### ### ### # ########### # # # # # ##### ### ############### # # ### # ### # # # # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ####### ### ### # ### # ### # # # ### # # ### ### ##### ### # ### # # ##### ####### # ####### # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### ####### ##### ######### ##### ### # # ### ### # ### ### # ##### # ########### ##### # ### ##
+# # # # # # # # # # # # # # # # # # # # # # ##
+# # # ####### ### ##### ### ### # ##### ##### ### ### # # ### ############# # ##### # # # ### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ##### # # # ### ##### ### # # # # # ####### # # ### ########### # # # ##### # ### # ### ### ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ####### # # ####### ##### # ##### ####### # ##### ##### # # # # ##### ### # # ### ### # ### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### # # ##### # ### ######### ### # # # ##### ### ############### ### # # ####### ##### ### # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # ##### ### ##### ####### # ### ### ####### ##### # # ### ##### # # ##### ### # # # # ### # # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+##### ####### ##### ### ### ############# ##### # # ####### # # # ### # # # # # ### ##### # # # # ##
+# # # # # # # # # # # # # # # # # # # # ##
+# ####### ### # # ##### # ####### ### # ### ##### ### ########### ##### ### # # # ##### ######### ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+### # ##### ### ######### # ### # # ##### ### # ####### ####### ##### ### ####### # # ##### ##### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # ######### ##### # # ##### ### # # ##### # ### # # # # # # ### # ####### # ##### # # # ### # ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ### # # # # # ##### # # # ##### # ### ####### # # ##### # # # ### ##### # ####### # # ######### ##
+# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # # # ####### ### # # ##### # ### ### # # # # ##### ### ### # ### # ##### ### # ##### ### ### ##
+# # # # # # # # # # # # # # # # # # # # # # # # ##
+####### ##### ##### ##### # # ##### ### ##### ######### # # # ### ##### # ##### # # ####### ### ####
+# # # # # # # # # # # # # # # # # # # # # # # # # # ##
+# # # ### # ### ##### # ##### # # ### # ####### ##### # # # # ### # # # ####### ##### # # # # ### ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# ### # ##### ##### ##### ########### ####### ### # ########### ### # ####### ######### ##### # # ##
+# # # # # # # # # # # # # # # # # # # # # ##
+# ### ### # ######### # ##### # ####### # # ####### ### # ####### ### ##### ### # # # # # ####### ##
+# # # # # # # # # # # # # # # # # # # # # # # ##
+# # ### # # # # ### ### # # ############# ########### ####### # ### ##### ##### # ####### ##### # ##
+# # # # # # # # # # # # # # # # # # # # # # # # # ##
+# ### ####### ##### # ########### # ### ### # ##### # ##### ####### # # ### # ######### # # # # # ##
+# # # # # # # # # # # ##
+###################################################################################################
+####################################################################################################
diff --git a/MarkinAM/2/mazes/medium.txt b/MarkinAM/2/mazes/medium.txt
new file mode 100644
index 0000000..e6182f2
--- /dev/null
+++ b/MarkinAM/2/mazes/medium.txt
@@ -0,0 +1,18 @@
+##################################################
+#S #
+# ################ ##### ############# #
+# # # # #
+# ########### ##### ##### ########### #
+# # # # #
+# # ######### ################ ######### #
+# # # # #
+# ############ ################# ### # #
+# # # # # #
+##### ##### ### ########### #### # # #
+ # # E # # # # # #
+#### ##### ### ########### # #### # # # #
+# # # # # # # # #
+# ### ##### ############# ### ## # # # #
+# # # # # # # #
+########### ############ ##### ### ######### #
+##################################################
\ No newline at end of file
diff --git a/MarkinAM/2/mazes/no_exit.txt b/MarkinAM/2/mazes/no_exit.txt
new file mode 100644
index 0000000..abe48ba
--- /dev/null
+++ b/MarkinAM/2/mazes/no_exit.txt
@@ -0,0 +1,3 @@
+##########
+#S #
+##########
\ No newline at end of file
diff --git a/MarkinAM/2/mazes/open.txt b/MarkinAM/2/mazes/open.txt
new file mode 100644
index 0000000..69002e4
--- /dev/null
+++ b/MarkinAM/2/mazes/open.txt
@@ -0,0 +1,6 @@
+S....................................................................................................
+.........................................................................................................
+.........................................................................................................
+.........................................................................................................
+.........................................................................................................
+.........................................................................................E...............
\ No newline at end of file
diff --git a/MarkinAM/2/mazes/small.txt b/MarkinAM/2/mazes/small.txt
new file mode 100644
index 0000000..69339b7
--- /dev/null
+++ b/MarkinAM/2/mazes/small.txt
@@ -0,0 +1,10 @@
+##########
+# E #
+# ###### #
+# # # #
+# # ## # #
+# # ## # #
+# # # #
+# ###### #
+# S #
+##########
\ No newline at end of file
diff --git a/MarkinAM/2/observer.py b/MarkinAM/2/observer.py
new file mode 100644
index 0000000..298fcbb
--- /dev/null
+++ b/MarkinAM/2/observer.py
@@ -0,0 +1,71 @@
+from abc import ABC, abstractmethod
+from classes import Maze, Cell
+
+
+class Observer(ABC):
+ """Интерфейс наблюдателя."""
+
+ @abstractmethod
+ def update(self, event: dict) -> None:
+ """
+ event — словарь с ключом "type":
+ "maze_loaded" — лабиринт загружен
+ "path_found" — путь найден
+ "no_path" — путь не найден
+ """
+ ...
+
+
+class ConsoleView(Observer):
+ """
+ Наблюдатель: выводит лабиринт и путь в консоль.
+
+ Символы:
+ # — стена
+ . — проход
+ S — старт
+ E — выход
+ * — найденный путь
+ @ — текущее положение игрока
+ """
+
+ def update(self, event: dict) -> None:
+ event_type = event.get("type")
+
+ if event_type == "maze_loaded":
+ print("\n[ConsoleView] Лабиринт загружен:")
+ self.render(event["maze"])
+
+ elif event_type == "path_found":
+ print("\n[ConsoleView] Путь найден!")
+ self.render(event["maze"], path=event.get("path"), player=event.get("player"))
+
+ elif event_type == "no_path":
+ print("\n[ConsoleView] Путь не найден.")
+
+ elif event_type == "move":
+ print(f"\n[ConsoleView] Игрок переместился в ({event['x']}, {event['y']})")
+ self.render(event["maze"], path=event.get("path"), player=event.get("player"))
+
+ def render(self, maze: Maze, path: list[Cell] | None = None,
+ player: Cell | None = None) -> None:
+ path_set = set(path) if path else set()
+
+ for y in range(maze.height):
+ row_str = ""
+ for x in range(maze.width):
+ cell = maze.get_cell(x, y)
+ if player and cell == player:
+ row_str += "@"
+ elif cell.is_start:
+ row_str += "S"
+ elif cell.is_exit:
+ row_str += "E"
+ elif cell in path_set:
+ row_str += "*"
+ elif cell.is_wall:
+ row_str += "#"
+ else:
+ row_str += "."
+ print(row_str)
+
diff --git a/MarkinAM/2/plots/plot_large.png b/MarkinAM/2/plots/plot_large.png
new file mode 100644
index 0000000..92076c8
Binary files /dev/null and b/MarkinAM/2/plots/plot_large.png differ
diff --git a/MarkinAM/2/plots/plot_medium.png b/MarkinAM/2/plots/plot_medium.png
new file mode 100644
index 0000000..4972343
Binary files /dev/null and b/MarkinAM/2/plots/plot_medium.png differ
diff --git a/MarkinAM/2/plots/plot_open.png b/MarkinAM/2/plots/plot_open.png
new file mode 100644
index 0000000..f31e8e5
Binary files /dev/null and b/MarkinAM/2/plots/plot_open.png differ
diff --git a/MarkinAM/2/plots/plot_small.png b/MarkinAM/2/plots/plot_small.png
new file mode 100644
index 0000000..924c9a2
Binary files /dev/null and b/MarkinAM/2/plots/plot_small.png differ
diff --git a/MarkinAM/2/report.docx b/MarkinAM/2/report.docx
new file mode 100644
index 0000000..60b5193
Binary files /dev/null and b/MarkinAM/2/report.docx differ
diff --git a/MarkinAM/2/results.csv b/MarkinAM/2/results.csv
new file mode 100644
index 0000000..a43efc7
--- /dev/null
+++ b/MarkinAM/2/results.csv
@@ -0,0 +1,13 @@
+лабиринт,стратегия,время_мс,посещено_клеток,длина_пути
+large,BFS,7.59029,2952,662
+large,DFS,10.92704,4082,1566
+large,A*,5.40801,1073,662
+medium,BFS,0.2096,80,22
+medium,DFS,0.79632,300,28
+medium,A*,0.13978,28,22
+open,BFS,1.57724,550,95
+open,DFS,1.04963,303,303
+open,A*,0.64328,95,95
+small,BFS,0.06765,25,13
+small,DFS,0.03831,13,13
+small,A*,0.06389,13,13
diff --git a/MarkinAM/2/solver.py b/MarkinAM/2/solver.py
new file mode 100644
index 0000000..44b04ba
--- /dev/null
+++ b/MarkinAM/2/solver.py
@@ -0,0 +1,71 @@
+import time
+from dataclasses import dataclass
+
+from classes import Maze, Cell
+from strategies import PathFindingStrategy
+from observer import Observer
+
+
+@dataclass
+class SearchStats:
+ """Результаты одного запуска поиска."""
+ time_ms: float # время выполнения в миллисекундах
+ visited_cells: int # количество посещённых клеток
+ path_length: int # длина найденного пути (0 если не найден)
+ path: list[Cell] # сам путь
+
+
+class MazeSolver:
+
+ def __init__(self, maze: Maze, strategy: PathFindingStrategy | None = None):
+ self.maze = maze
+ self.strategy = strategy
+ self._observers: list[Observer] = []
+
+ # ── Strategy ──────────────────────────────────────────────────────────────
+
+ def set_strategy(self, strategy: PathFindingStrategy) -> None:
+ """Динамически меняет алгоритм поиска."""
+ self.strategy = strategy
+
+ # ── Observer ──────────────────────────────────────────────────────────────
+
+ def add_observer(self, observer: Observer) -> None:
+ self._observers.append(observer)
+
+ def remove_observer(self, observer: Observer) -> None:
+ self._observers.remove(observer)
+
+ def _notify(self, event: dict) -> None:
+ for obs in self._observers:
+ obs.update(event)
+
+ # ── Solve ─────────────────────────────────────────────────────────────────
+
+ def solve(self) -> SearchStats:
+ """Запускает поиск пути и возвращает статистику."""
+ if self.strategy is None:
+ raise RuntimeError("Стратегия не задана. Используйте set_strategy().")
+
+ self._notify({"type": "maze_loaded", "maze": self.maze})
+
+ t_start = time.perf_counter()
+ path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
+ t_end = time.perf_counter()
+
+ time_ms = (t_end - t_start) * 1000
+ visited = getattr(self.strategy, "visited_count", 0)
+
+ stats = SearchStats(
+ time_ms=time_ms,
+ visited_cells=visited,
+ path_length=len(path),
+ path=path,
+ )
+
+ if path:
+ self._notify({"type": "path_found", "maze": self.maze, "path": path})
+ else:
+ self._notify({"type": "no_path"})
+
+ return stats
\ No newline at end of file
diff --git a/MarkinAM/2/strategies.py b/MarkinAM/2/strategies.py
new file mode 100644
index 0000000..c06eea1
--- /dev/null
+++ b/MarkinAM/2/strategies.py
@@ -0,0 +1,119 @@
+from abc import ABC, abstractmethod
+from collections import deque
+import heapq
+
+from classes import Cell, Maze
+
+
+class PathFindingStrategy(ABC):
+ """Интерфейс стратегии поиска пути."""
+
+ @abstractmethod
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
+ ...
+
+ # Вспомогательный метод восстановления пути по словарю предшественников
+ @staticmethod
+ def _reconstruct_path(came_from: dict, start: Cell, goal: Cell) -> list[Cell]:
+ path = []
+ current = goal
+ while current != start:
+ path.append(current)
+ current = came_from[current]
+ path.append(start)
+ path.reverse()
+ return path
+
+
+# ── BFS ──────────────────────────────────────────────────────────────────────
+
+class BFSStrategy(PathFindingStrategy):
+
+
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
+ queue = deque([start])
+ came_from: dict[Cell, Cell | None] = {start: None}
+ self.visited_count = 0
+
+ while queue:
+ current = queue.popleft()
+ self.visited_count += 1
+
+ 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 [] # путь не найден
+
+
+# ── DFS ──────────────────────────────────────────────────────────────────────
+
+class DFSStrategy(PathFindingStrategy):
+
+
+ def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> list[Cell]:
+ stack = [start]
+ came_from: dict[Cell, Cell | None] = {start: None}
+ self.visited_count = 0
+
+ while stack:
+ current = stack.pop()
+ self.visited_count += 1
+
+ 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 []
+
+
+# ── A* ───────────────────────────────────────────────────────────────────────
+
+class AStarStrategy(PathFindingStrategy):
+ """A* с манхэттенской эвристикой"""
+
+ def __init__(self):
+ self.visited_count = 0
+
+ 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]:
+ g_score = {start: 0}
+ parent: dict[Cell] = {start: None}
+ open_heap = [(self._heuristic(start, exit_cell), 0, start)]
+ closed_set: set[Cell] = set() # уже обработанные клетки
+ self.visited_count = 0
+ counter = 0 # счётчик для устранения неоднозначности
+
+ while open_heap:
+ _, _, current = heapq.heappop(open_heap)
+
+ if current in closed_set:
+ continue
+ closed_set.add(current)
+ self.visited_count += 1
+
+ if current == exit_cell:
+ return self._reconstruct_path(parent, start, exit_cell)
+
+ for neighbor in maze.get_neighbors(current):
+ if neighbor in closed_set:
+ continue
+ tentative_g = g_score[current]
+ if tentative_g < g_score.get(neighbor, float('inf')):
+ g_score[neighbor] = tentative_g
+ parent[neighbor] = current
+ f = tentative_g + self._heuristic(neighbor, exit_cell)
+ counter += 1
+ heapq.heappush(open_heap, (f, counter, neighbor))
+
+ return []
\ No newline at end of file