Merge pull request '[12] test пуш' (#339) from shapovalovka/2026-rff_mp:ShapovalovKA into develop
Reviewed-on: UNN/2026-rff_mp#339
0
ShapovalovKA/425.md
Normal file
BIN
ShapovalovKA/docs/1st_task_analysis.docx
Normal file
BIN
ShapovalovKA/docs/2nd_task_analysis.docx
Normal file
48
ShapovalovKA/docs/data/1Task/res.py
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import csv
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
|
array_arr = []
|
||||||
|
array_list = []
|
||||||
|
array_hash = []
|
||||||
|
array_bin = []
|
||||||
|
|
||||||
|
with open('results.csv', 'r', encoding='utf-8-sig') as file:
|
||||||
|
reader = csv.reader(file, delimiter=';')
|
||||||
|
next(reader) # пропускаем заголовок
|
||||||
|
|
||||||
|
values = []
|
||||||
|
for row in reader:
|
||||||
|
values.append(float(row[3]))
|
||||||
|
|
||||||
|
array_arr = values[0:4]
|
||||||
|
array_list = values[4:8]
|
||||||
|
array_hash = values[8:12]
|
||||||
|
array_bin = values[12:16]
|
||||||
|
|
||||||
|
print(f"array_arr : {array_arr}")
|
||||||
|
print(f"array_list: {array_list}")
|
||||||
|
print(f"array_hash: {array_hash}")
|
||||||
|
print(f"array_bin : {array_bin}")
|
||||||
|
|
||||||
|
l = [1, 2, 3, 4]
|
||||||
|
|
||||||
|
#визуализация без дерева
|
||||||
|
plt.plot(l, array_arr, label = 'Array', c='black')
|
||||||
|
plt.plot(l, array_list, label = 'Linked list', c='blue')
|
||||||
|
plt.plot(l, array_hash, label = 'Hash table', c='orange')
|
||||||
|
plt.ylabel('array') #название по y
|
||||||
|
plt.xlabel('l') #название по x
|
||||||
|
plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15), ncol = 3)
|
||||||
|
plt.savefig('t1p1.png', dpi=300, bbox_inches='tight') #сохранение в файле
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
#визуализация с деревом
|
||||||
|
plt.plot(l, array_arr, label = 'Array', c='black')
|
||||||
|
plt.plot(l, array_list, label = 'Linked list', c='blue')
|
||||||
|
plt.plot(l, array_hash, label = 'Hash table', c='orange')
|
||||||
|
plt.plot(l, array_bin, label = 'Binary tree', c='red')
|
||||||
|
plt.ylabel('array') #название по y
|
||||||
|
plt.xlabel('l') #название по x
|
||||||
|
plt.legend(loc='upper center', bbox_to_anchor=(0.5, 1.15), ncol = 4)
|
||||||
|
plt.savefig('t1p2.png', dpi=300, bbox_inches='tight') #сохранение в файле
|
||||||
|
plt.show()
|
||||||
9
ShapovalovKA/docs/data/1Task/results.csv
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
Структура;Режим;Операция;Время (сек)
|
||||||
|
Array;случайный;вставка (в начало);0.06431880006566644
|
||||||
|
Array;отсортированный;вставка (в начало);0.06380272014066576
|
||||||
|
Array;любой;поиск 110 записей;0.07721293987706304
|
||||||
|
Array;любой;удаление 50 записей (среднее);0.0018548803813755513
|
||||||
|
Linked list;случайный;вставка (в начало);0.01246960014104843
|
||||||
|
Linked list;отсортированный;вставка (в начало);0.007890580128878355
|
||||||
|
Linked list;любой;поиск 110 записей;0.23582311999052763
|
||||||
|
Linked list;любой;удаление 50 записей (среднее);0.0023578427862375973
|
||||||
|
212
ShapovalovKA/docs/data/1Task/t1_1.py
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
|
||||||
|
# ---------- Реализация связного списка ----------
|
||||||
|
def ll_insert_begin(head, name, phone):
|
||||||
|
# Вставка узла в начало списка. Возвращает новую голову.
|
||||||
|
new_node = {'name': name, 'phone': phone, 'next': head}
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
def ll_find(head, name):
|
||||||
|
# Поиск телефона по имени. Возвращает phone или None.
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
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']:
|
||||||
|
if current['next']['name'] == name:
|
||||||
|
current['next'] = current['next']['next']
|
||||||
|
return head
|
||||||
|
current = current['next']
|
||||||
|
return head
|
||||||
|
|
||||||
|
def ll_list_all(head):
|
||||||
|
# Собирает все записи в список и сортирует по имени.
|
||||||
|
records = []
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
records.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
records.sort(key=lambda x: x[0])
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Измерения для массива ----------
|
||||||
|
def array_insert_measure(records, sorted_flag=False):
|
||||||
|
# Вставка записей в начало массива. Возвращает время.
|
||||||
|
arr = []
|
||||||
|
start = time.perf_counter()
|
||||||
|
if sorted_flag:
|
||||||
|
# records уже отсортированы
|
||||||
|
for item in records:
|
||||||
|
arr.insert(0, item)
|
||||||
|
else:
|
||||||
|
for item in records:
|
||||||
|
arr.insert(0, item)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def array_find_measure(records, test_names):
|
||||||
|
# Поиск в массиве: линейный перебор.
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in test_names:
|
||||||
|
for rec in records:
|
||||||
|
if rec[0] == name:
|
||||||
|
break
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def array_delete_measure(records, delete_names):
|
||||||
|
# Удаление из массива через создание нового списка (как в оригинале).
|
||||||
|
times = []
|
||||||
|
for name in delete_names:
|
||||||
|
start = time.perf_counter()
|
||||||
|
records = [rec for rec in records if rec[0] != name]
|
||||||
|
end = time.perf_counter()
|
||||||
|
times.append(end - start)
|
||||||
|
return sum(times) / len(times) if times else 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Измерения для связного списка ----------
|
||||||
|
def linked_insert_measure(records, sorted_flag=False):
|
||||||
|
# Вставка записей в начало связного списка. Возвращает время.
|
||||||
|
head = None
|
||||||
|
start = time.perf_counter()
|
||||||
|
# Если sorted_flag == True, records уже отсортированы, но для связного списка
|
||||||
|
# вставка в начало всегда O(1), порядок не влияет на время.
|
||||||
|
for name, phone in records:
|
||||||
|
head = ll_insert_begin(head, name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def linked_find_measure(head, test_names):
|
||||||
|
# Поиск в связном списке.
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in test_names:
|
||||||
|
ll_find(head, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def linked_delete_measure(head, delete_names):
|
||||||
|
# Удаление из связного списка.
|
||||||
|
times = []
|
||||||
|
for name in delete_names:
|
||||||
|
start = time.perf_counter()
|
||||||
|
head = ll_delete(head, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
times.append(end - start)
|
||||||
|
return sum(times) / len(times) if times else 0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Основная функция эксперимента ----------
|
||||||
|
def main():
|
||||||
|
N = 10000
|
||||||
|
# Генерация тестовых данных
|
||||||
|
records = []
|
||||||
|
for i in range(N):
|
||||||
|
name = f"User_{i:05d}"
|
||||||
|
phone = f"8{random.randint(9000000000, 9999999999)}"
|
||||||
|
records.append((name, phone))
|
||||||
|
|
||||||
|
records_shuffled = records.copy()
|
||||||
|
random.shuffle(records_shuffled)
|
||||||
|
records_sorted = sorted(records, key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Имена для поиска (100 существующих + 10 несуществующих)
|
||||||
|
existing_names = random.sample([rec[0] for rec in records], 100)
|
||||||
|
non_existing = [f"None_{i}" for i in range(10)]
|
||||||
|
test_names = existing_names + non_existing
|
||||||
|
|
||||||
|
# Имена для удаления (50 случайных)
|
||||||
|
delete_names = random.sample([rec[0] for rec in records], 50)
|
||||||
|
|
||||||
|
# Результаты будем собирать в список списков
|
||||||
|
results = [["Структура", "Режим", "Операция", "Время (сек)"]]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Массив -----
|
||||||
|
# Вставка (случайный порядок)
|
||||||
|
arr_time_shuffled = 0.0
|
||||||
|
arr_time_sorted = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
arr_time_shuffled += array_insert_measure(records_shuffled, sorted_flag=False)
|
||||||
|
arr_time_sorted += array_insert_measure(records_sorted, sorted_flag=True)
|
||||||
|
results.append(["Array", "случайный", "вставка (в начало)", arr_time_shuffled / 5])
|
||||||
|
results.append(["Array", "отсортированный", "вставка (в начало)", arr_time_sorted / 5])
|
||||||
|
|
||||||
|
# Поиск
|
||||||
|
find_time = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
find_time += array_find_measure(records, test_names)
|
||||||
|
results.append(["Array", "любой", "поиск 110 записей", find_time / 5])
|
||||||
|
|
||||||
|
# Удаление
|
||||||
|
del_time = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
del_time += array_delete_measure(records.copy(), delete_names)
|
||||||
|
results.append(["Array", "любой", "удаление 50 записей (среднее)", del_time / 5])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# ----- Связный список -----
|
||||||
|
# Вставка
|
||||||
|
ll_time_shuffled = 0.0
|
||||||
|
ll_time_sorted = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
ll_time_shuffled += linked_insert_measure(records_shuffled)
|
||||||
|
ll_time_sorted += linked_insert_measure(records_sorted)
|
||||||
|
results.append(["Linked list", "случайный", "вставка (в начало)", ll_time_shuffled / 5])
|
||||||
|
results.append(["Linked list", "отсортированный", "вставка (в начало)", ll_time_sorted / 5])
|
||||||
|
|
||||||
|
# Поиск (предварительно строим список)
|
||||||
|
head = None
|
||||||
|
for name, phone in records:
|
||||||
|
head = ll_insert_begin(head, name, phone)
|
||||||
|
find_time_ll = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
find_time_ll += linked_find_measure(head, test_names)
|
||||||
|
results.append(["Linked list", "любой", "поиск 110 записей", find_time_ll / 5])
|
||||||
|
|
||||||
|
# Удаление (копируем список для каждого замера)
|
||||||
|
del_time_ll = 0.0
|
||||||
|
for _ in range(5):
|
||||||
|
# Строим новую копию списка
|
||||||
|
h = None
|
||||||
|
for name, phone in records:
|
||||||
|
h = ll_insert_begin(h, name, phone)
|
||||||
|
del_time_ll += linked_delete_measure(h, delete_names)
|
||||||
|
results.append(["Linked list", "любой", "удаление 50 записей (среднее)", del_time_ll / 5])
|
||||||
|
|
||||||
|
# ----- Вывод результатов в единый столбец -----
|
||||||
|
print("\nРезультаты экспериментов (время в секундах):\n")
|
||||||
|
# Определяем максимальную ширину первого столбца для красивого выравнивания
|
||||||
|
col_widths = [max(len(str(row[i])) for row in results) for i in range(4)]
|
||||||
|
for row in results:
|
||||||
|
print(f"{row[0]:<{col_widths[0]}} {row[1]:<{col_widths[1]}} "
|
||||||
|
f"{row[2]:<{col_widths[2]}} {row[3]:<{col_widths[3]}}")
|
||||||
|
|
||||||
|
# ----- Запись результатов в CSV-файл -----
|
||||||
|
with open('results.csv', 'w', newline='', encoding='utf-8-sig') as csvfile:
|
||||||
|
writer = csv.writer(csvfile, delimiter = ';')
|
||||||
|
writer.writerows(results)
|
||||||
|
|
||||||
|
print("\nРезультаты сохранены в файл 'results.csv'.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
185
ShapovalovKA/docs/data/1Task/t1_2.py
Normal file
|
|
@ -0,0 +1,185 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
# --------------------- Реализация связного списка (взята из t1_1) ---------------------
|
||||||
|
def ll_insert_begin(head, name, phone):
|
||||||
|
# Вставка узла в начало списка. Возвращает новую голову.
|
||||||
|
new_node = {'name': name, 'phone': phone, 'next': head}
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
def ll_find(head, name):
|
||||||
|
# Поиск телефона по имени. Возвращает phone или None.
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
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']:
|
||||||
|
if current['next']['name'] == name:
|
||||||
|
current['next'] = current['next']['next']
|
||||||
|
return head
|
||||||
|
current = current['next']
|
||||||
|
return head
|
||||||
|
|
||||||
|
def ll_list_all(head):
|
||||||
|
# Собирает все записи в список и сортирует по имени.
|
||||||
|
records = []
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
records.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
records.sort(key=lambda x: x[0])
|
||||||
|
return records
|
||||||
|
|
||||||
|
# --------------------- Реализация хеш-таблицы ---------------------
|
||||||
|
class HashTable:
|
||||||
|
def __init__(self, size=2000):
|
||||||
|
self.size = size
|
||||||
|
self.buckets = [None] * size # каждый bucket — голова связного списка
|
||||||
|
|
||||||
|
def _hash(self, name):
|
||||||
|
# Простая хеш-функция: сумма кодов символов по модулю размера.
|
||||||
|
return sum(ord(ch) for ch in name) % self.size
|
||||||
|
|
||||||
|
def insert(self, name, phone):
|
||||||
|
index = self._hash(name)
|
||||||
|
# Вставляем в начало связного списка в данном bucket'е
|
||||||
|
self.buckets[index] = ll_insert_begin(self.buckets[index], name, phone)
|
||||||
|
|
||||||
|
def find(self, name):
|
||||||
|
index = self._hash(name)
|
||||||
|
return ll_find(self.buckets[index], name)
|
||||||
|
|
||||||
|
def delete(self, name):
|
||||||
|
index = self._hash(name)
|
||||||
|
self.buckets[index] = ll_delete(self.buckets[index], name)
|
||||||
|
|
||||||
|
def list_all(self):
|
||||||
|
# Собирает все записи из всех bucket'ов и сортирует по имени.
|
||||||
|
all_records = []
|
||||||
|
for head in self.buckets:
|
||||||
|
current = head
|
||||||
|
while current:
|
||||||
|
all_records.append((current['name'], current['phone']))
|
||||||
|
current = current['next']
|
||||||
|
all_records.sort(key=lambda x: x[0])
|
||||||
|
return all_records
|
||||||
|
|
||||||
|
# --------------------- Функции измерений ---------------------
|
||||||
|
def generate_data(N=10000):
|
||||||
|
records = []
|
||||||
|
for i in range(N):
|
||||||
|
name = f"User_{i:05d}"
|
||||||
|
phone = f"8{random.randint(9000000000, 9999999999)}"
|
||||||
|
records.append((name, phone))
|
||||||
|
return records
|
||||||
|
|
||||||
|
def measure_insert(records, sort_order='random'):
|
||||||
|
# Измеряет время вставки в хеш-таблицу.
|
||||||
|
# sort_order: 'random' или 'sorted' — порядок передаваемых записей.
|
||||||
|
ht = HashTable(size=2000)
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
ht.insert(name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def measure_find(records, test_names):
|
||||||
|
# Поиск 110 записей в уже заполненной хеш-таблице.
|
||||||
|
ht = HashTable(size=2000)
|
||||||
|
for name, phone in records:
|
||||||
|
ht.insert(name, phone)
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in test_names:
|
||||||
|
ht.find(name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def measure_delete(records, delete_names):
|
||||||
|
# Удаление 50 записей из хеш-таблицы (среднее время одного удаления).
|
||||||
|
times = []
|
||||||
|
for name in delete_names:
|
||||||
|
ht = HashTable(size=2000)
|
||||||
|
for n, p in records:
|
||||||
|
ht.insert(n, p)
|
||||||
|
start = time.perf_counter()
|
||||||
|
ht.delete(name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
times.append(end - start)
|
||||||
|
return sum(times) / len(times)
|
||||||
|
|
||||||
|
# --------------------- Основная функция ---------------------
|
||||||
|
def main():
|
||||||
|
N = 10000
|
||||||
|
records = generate_data(N)
|
||||||
|
|
||||||
|
# Перемешанные и отсортированные копии
|
||||||
|
records_shuffled = records.copy()
|
||||||
|
random.shuffle(records_shuffled)
|
||||||
|
records_sorted = sorted(records, key=lambda x: x[0])
|
||||||
|
|
||||||
|
# Имена для поиска (100 существующих + 10 несуществующих)
|
||||||
|
existing_names = random.sample([rec[0] for rec in records], 100)
|
||||||
|
non_existing = [f"None_{i}" for i in range(10)]
|
||||||
|
test_names = existing_names + non_existing
|
||||||
|
|
||||||
|
# Имена для удаления (50 случайных)
|
||||||
|
delete_names = random.sample([rec[0] for rec in records], 50)
|
||||||
|
|
||||||
|
# Замеры (по 5 повторений)
|
||||||
|
insert_shuffled_avg = 0.0
|
||||||
|
insert_sorted_avg = 0.0
|
||||||
|
find_avg = 0.0
|
||||||
|
delete_avg = 0.0
|
||||||
|
|
||||||
|
repeats = 5
|
||||||
|
for _ in range(repeats):
|
||||||
|
insert_shuffled_avg += measure_insert(records_shuffled, 'random')
|
||||||
|
insert_sorted_avg += measure_insert(records_sorted, 'sorted')
|
||||||
|
find_avg += measure_find(records, test_names)
|
||||||
|
delete_avg += measure_delete(records, delete_names)
|
||||||
|
|
||||||
|
insert_shuffled_avg /= repeats
|
||||||
|
insert_sorted_avg /= repeats
|
||||||
|
find_avg /= repeats
|
||||||
|
delete_avg /= repeats
|
||||||
|
|
||||||
|
# Подготовка строк для CSV
|
||||||
|
new_rows = [
|
||||||
|
["Hash table", "случайный", "вставка (в начало)", insert_shuffled_avg],
|
||||||
|
["Hash table", "отсортированный", "вставка (в начало)", insert_sorted_avg],
|
||||||
|
["Hash table", "любой", "поиск 110 записей", find_avg],
|
||||||
|
["Hash table", "любой", "удаление 50 записей (среднее)", delete_avg]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Определяем имя CSV-файла (там же, где и t1_1.py)
|
||||||
|
csv_filename = "results.csv"
|
||||||
|
file_exists = os.path.isfile(csv_filename)
|
||||||
|
|
||||||
|
# Запись в CSV (добавление)
|
||||||
|
with open(csv_filename, 'a', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.writer(f, delimiter=';')
|
||||||
|
# Если файл только что создан, сначала запишем заголовок
|
||||||
|
if not file_exists:
|
||||||
|
writer.writerow(["Структура", "Режим", "Операция", "Время (сек)"])
|
||||||
|
writer.writerows(new_rows)
|
||||||
|
|
||||||
|
print("Результаты для хеш-таблицы добавлены в", csv_filename)
|
||||||
|
print(f"Среднее время вставки (случ. порядок): {insert_shuffled_avg:.6f} сек")
|
||||||
|
print(f"Среднее время вставки (отсорт.): {insert_sorted_avg:.6f} сек")
|
||||||
|
print(f"Среднее время поиска 110 записей: {find_avg:.6f} сек")
|
||||||
|
print(f"Среднее время удаления 50 записей: {delete_avg:.6f} сек")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
211
ShapovalovKA/docs/data/1Task/t1_3.py
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import os
|
||||||
|
|
||||||
|
# --------------------- Реализация бинарного дерева поиска (итеративная) ---------------------
|
||||||
|
def bst_insert(root, name, phone):
|
||||||
|
#Итеративная вставка. Возвращает корень.
|
||||||
|
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||||
|
if root is None:
|
||||||
|
return new_node
|
||||||
|
|
||||||
|
current = root
|
||||||
|
while True:
|
||||||
|
if name < current['name']:
|
||||||
|
if current['left'] is None:
|
||||||
|
current['left'] = new_node
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
current = current['left']
|
||||||
|
elif name > current['name']:
|
||||||
|
if current['right'] is None:
|
||||||
|
current['right'] = new_node
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
current = current['right']
|
||||||
|
else: # имя уже существует — обновляем телефон
|
||||||
|
current['phone'] = phone
|
||||||
|
break
|
||||||
|
return root
|
||||||
|
|
||||||
|
def bst_find(root, name):
|
||||||
|
#Итеративный поиск. Возвращает phone или None.
|
||||||
|
current = root
|
||||||
|
while current:
|
||||||
|
if name == current['name']:
|
||||||
|
return current['phone']
|
||||||
|
elif name < current['name']:
|
||||||
|
current = current['left']
|
||||||
|
else:
|
||||||
|
current = current['right']
|
||||||
|
return None
|
||||||
|
|
||||||
|
def bst_find_min(node):
|
||||||
|
#Возвращает узел с минимальным ключом в поддереве.
|
||||||
|
while node['left']:
|
||||||
|
node = node['left']
|
||||||
|
return node
|
||||||
|
|
||||||
|
def bst_delete(root, name):
|
||||||
|
#Итеративное удаление. Возвращает новый корень.
|
||||||
|
# Сначала найдём удаляемый узел и его родителя
|
||||||
|
parent = None
|
||||||
|
current = root
|
||||||
|
while current and current['name'] != name:
|
||||||
|
parent = current
|
||||||
|
if name < current['name']:
|
||||||
|
current = current['left']
|
||||||
|
else:
|
||||||
|
current = current['right']
|
||||||
|
if current is None: # узел не найден
|
||||||
|
return root
|
||||||
|
|
||||||
|
# Случай 1: нет левого потомка
|
||||||
|
if current['left'] is None:
|
||||||
|
child = current['right']
|
||||||
|
# Случай 2: нет правого потомка
|
||||||
|
elif current['right'] is None:
|
||||||
|
child = current['left']
|
||||||
|
# Случай 3: два потомка
|
||||||
|
else:
|
||||||
|
# Находим минимальный узел в правом поддереве (преемник)
|
||||||
|
min_parent = current
|
||||||
|
min_node = current['right']
|
||||||
|
while min_node['left']:
|
||||||
|
min_parent = min_node
|
||||||
|
min_node = min_node['left']
|
||||||
|
# Копируем данные из min_node в current
|
||||||
|
current['name'], current['phone'] = min_node['name'], min_node['phone']
|
||||||
|
# Удаляем min_node (у него нет левого потомка)
|
||||||
|
if min_parent['left'] == min_node:
|
||||||
|
min_parent['left'] = min_node['right']
|
||||||
|
else:
|
||||||
|
min_parent['right'] = min_node['right']
|
||||||
|
return root
|
||||||
|
|
||||||
|
# Подсоединяем child к parent
|
||||||
|
if parent is None:
|
||||||
|
return child
|
||||||
|
if parent['left'] == current:
|
||||||
|
parent['left'] = child
|
||||||
|
else:
|
||||||
|
parent['right'] = child
|
||||||
|
return root
|
||||||
|
|
||||||
|
def bst_list_all(root):
|
||||||
|
#Итеративный симметричный обход (inorder) без рекурсии, используя стек.
|
||||||
|
result = []
|
||||||
|
stack = []
|
||||||
|
current = root
|
||||||
|
while stack or current:
|
||||||
|
while current:
|
||||||
|
stack.append(current)
|
||||||
|
current = current['left']
|
||||||
|
current = stack.pop()
|
||||||
|
result.append((current['name'], current['phone']))
|
||||||
|
current = current['right']
|
||||||
|
return result
|
||||||
|
|
||||||
|
# --------------------- Функции измерений ---------------------
|
||||||
|
def generate_data(N=10000):
|
||||||
|
records = []
|
||||||
|
for i in range(N):
|
||||||
|
name = f"User_{i:05d}"
|
||||||
|
phone = f"8{random.randint(9000000000, 9999999999)}"
|
||||||
|
records.append((name, phone))
|
||||||
|
return records
|
||||||
|
|
||||||
|
def measure_insert(records):
|
||||||
|
root = None
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name, phone in records:
|
||||||
|
root = bst_insert(root, name, phone)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def measure_find(records, test_names):
|
||||||
|
root = None
|
||||||
|
for name, phone in records:
|
||||||
|
root = bst_insert(root, name, phone)
|
||||||
|
start = time.perf_counter()
|
||||||
|
for name in test_names:
|
||||||
|
bst_find(root, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
return end - start
|
||||||
|
|
||||||
|
def measure_delete(records, delete_names):
|
||||||
|
times = []
|
||||||
|
for name in delete_names:
|
||||||
|
root = None
|
||||||
|
for n, p in records:
|
||||||
|
root = bst_insert(root, n, p)
|
||||||
|
start = time.perf_counter()
|
||||||
|
root = bst_delete(root, name)
|
||||||
|
end = time.perf_counter()
|
||||||
|
times.append(end - start)
|
||||||
|
return sum(times) / len(times)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
N = 10000
|
||||||
|
records = generate_data(N)
|
||||||
|
|
||||||
|
records_shuffled = records.copy()
|
||||||
|
random.shuffle(records_shuffled)
|
||||||
|
records_sorted = sorted(records, key=lambda x: x[0])
|
||||||
|
|
||||||
|
existing_names = random.sample([rec[0] for rec in records], 100)
|
||||||
|
non_existing = [f"None_{i}" for i in range(10)]
|
||||||
|
test_names = existing_names + non_existing
|
||||||
|
|
||||||
|
delete_names = random.sample([rec[0] for rec in records], 50)
|
||||||
|
|
||||||
|
insert_shuffled_avg = 0.0
|
||||||
|
insert_sorted_avg = 0.0
|
||||||
|
find_avg = 0.0
|
||||||
|
delete_avg = 0.0
|
||||||
|
|
||||||
|
repeats = 5
|
||||||
|
for _ in range(repeats):
|
||||||
|
insert_shuffled_avg += measure_insert(records_shuffled)
|
||||||
|
insert_sorted_avg += measure_insert(records_sorted)
|
||||||
|
find_avg += measure_find(records, test_names)
|
||||||
|
delete_avg += measure_delete(records, delete_names)
|
||||||
|
|
||||||
|
insert_shuffled_avg /= repeats
|
||||||
|
insert_sorted_avg /= repeats
|
||||||
|
find_avg /= repeats
|
||||||
|
delete_avg /= repeats
|
||||||
|
|
||||||
|
new_rows = [
|
||||||
|
["Binary tree", "случайный", "вставка (корень)", insert_shuffled_avg],
|
||||||
|
["Binary tree", "отсортированный", "вставка (корень)", insert_sorted_avg],
|
||||||
|
["Binary tree", "любой", "поиск 110 записей", find_avg],
|
||||||
|
["Binary tree", "любой", "удаление 50 записей (среднее)", delete_avg]
|
||||||
|
]
|
||||||
|
|
||||||
|
csv_filename = "results.csv"
|
||||||
|
file_exists = os.path.isfile(csv_filename)
|
||||||
|
need_header = False
|
||||||
|
if file_exists:
|
||||||
|
with open(csv_filename, 'r', encoding='utf-8-sig') as f:
|
||||||
|
first_line = f.readline()
|
||||||
|
if not first_line.startswith("Структура"):
|
||||||
|
need_header = True
|
||||||
|
else:
|
||||||
|
need_header = True
|
||||||
|
|
||||||
|
with open(csv_filename, 'a', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.writer(f, delimiter=';')
|
||||||
|
if need_header:
|
||||||
|
writer.writerow(["Структура", "Режим", "Операция", "Время (сек)"])
|
||||||
|
writer.writerows(new_rows)
|
||||||
|
|
||||||
|
print("Результаты для двоичного дерева поиска добавлены в", csv_filename)
|
||||||
|
print(f"Среднее время вставки (случ. порядок): {insert_shuffled_avg:.6f} сек")
|
||||||
|
print(f"Среднее время вставки (отсорт.): {insert_sorted_avg:.6f} сек")
|
||||||
|
print(f"Среднее время поиска 110 записей: {find_avg:.6f} сек")
|
||||||
|
print(f"Среднее время удаления 50 записей: {delete_avg:.6f} сек")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
ShapovalovKA/docs/data/1Task/t1p1.png
Normal file
|
After Width: | Height: | Size: 107 KiB |
BIN
ShapovalovKA/docs/data/1Task/t1p2.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
2
ShapovalovKA/docs/data/1Task/Порядок использования.txt
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
t1_1.py -> t1_2.py -> t1_3.py
|
||||||
|
-> res.py
|
||||||
BIN
ShapovalovKA/docs/data/2Task/efficiency_ratio.png
Normal file
|
After Width: | Height: | Size: 81 KiB |
21
ShapovalovKA/docs/data/2Task/experiment_results.csv
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
maze_type;strategy;avg_time_ms;std_time_ms;avg_visited;avg_path_len;path_found
|
||||||
|
small_10x10_simple;BFS;0.187180;0.026335;19.000000;19.000000;True
|
||||||
|
small_10x10_simple;DFS;0.167600;0.006841;19.000000;19.000000;True
|
||||||
|
small_10x10_simple;A*;0.262300;0.029262;19.000000;19.000000;True
|
||||||
|
small_10x10_simple;Dijkstra;0.260840;0.008608;19.000000;19.000000;True
|
||||||
|
medium_50x50_deadends;BFS;3.563500;0.053603;380.000000;99.000000;True
|
||||||
|
medium_50x50_deadends;DFS;3.618520;0.082922;270.000000;219.000000;True
|
||||||
|
medium_50x50_deadends;A*;4.865660;0.017732;334.000000;99.000000;True
|
||||||
|
medium_50x50_deadends;Dijkstra;6.019060;0.037679;380.000000;99.000000;True
|
||||||
|
large_100x100_complex;BFS;8.644360;0.236037;886.000000;199.000000;True
|
||||||
|
large_100x100_complex;DFS;13.781640;2.087117;697.000000;511.000000;True
|
||||||
|
large_100x100_complex;A*;12.167040;0.334660;774.000000;199.000000;True
|
||||||
|
large_100x100_complex;Dijkstra;14.365940;0.236778;886.000000;199.000000;True
|
||||||
|
empty_50x50;BFS;24.584480;0.184147;2500.000000;99.000000;True
|
||||||
|
empty_50x50;DFS;182.315780;4.196306;2451.000000;2451.000000;True
|
||||||
|
empty_50x50;A*;42.602980;0.184895;2500.000000;99.000000;True
|
||||||
|
empty_50x50;Dijkstra;43.213780;0.745780;2500.000000;99.000000;True
|
||||||
|
no_exit_50x50;BFS;25.037680;0.572634;2496.000000;0.000000;False
|
||||||
|
no_exit_50x50;DFS;191.040920;3.180626;2496.000000;0.000000;False
|
||||||
|
no_exit_50x50;A*;42.158280;0.396219;2496.000000;0.000000;False
|
||||||
|
no_exit_50x50;Dijkstra;42.499100;0.482887;2496.000000;0.000000;False
|
||||||
|
BIN
ShapovalovKA/docs/data/2Task/mermaid_diagramm_task_2.png
Normal file
|
After Width: | Height: | Size: 69 KiB |
BIN
ShapovalovKA/docs/data/2Task/path_length.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
272
ShapovalovKA/docs/data/2Task/res2.py
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
# Настройка русских шрифтов
|
||||||
|
plt.rcParams['font.family'] = 'DejaVu Sans'
|
||||||
|
plt.rcParams['axes.unicode_minus'] = False
|
||||||
|
|
||||||
|
def load_and_prepare_data(filename='experiment_results.csv'):
|
||||||
|
"""Загрузка данных из CSV и подготовка."""
|
||||||
|
df = pd.read_csv(filename, delimiter=';')
|
||||||
|
|
||||||
|
# Преобразование типов (если нужно)
|
||||||
|
numeric_cols = ['avg_time_ms', 'std_time_ms', 'avg_visited', 'avg_path_len']
|
||||||
|
for col in numeric_cols:
|
||||||
|
df[col] = pd.to_numeric(df[col], errors='coerce')
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def plot_time_comparison(df):
|
||||||
|
"""График 1: Сравнение времени выполнения по лабиринтам."""
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 6))
|
||||||
|
|
||||||
|
maze_types = df['maze_type'].unique()
|
||||||
|
strategies = df['strategy'].unique()
|
||||||
|
|
||||||
|
x = np.arange(len(maze_types))
|
||||||
|
width = 0.2
|
||||||
|
|
||||||
|
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728']
|
||||||
|
|
||||||
|
for i, strategy in enumerate(strategies):
|
||||||
|
strategy_data = df[df['strategy'] == strategy]
|
||||||
|
times = []
|
||||||
|
errors = []
|
||||||
|
for maze in maze_types:
|
||||||
|
row = strategy_data[strategy_data['maze_type'] == maze]
|
||||||
|
if not row.empty:
|
||||||
|
times.append(row['avg_time_ms'].values[0])
|
||||||
|
errors.append(row['std_time_ms'].values[0])
|
||||||
|
else:
|
||||||
|
times.append(0)
|
||||||
|
errors.append(0)
|
||||||
|
|
||||||
|
bars = ax.bar(x + i*width, times, width, label=strategy,
|
||||||
|
color=colors[i], yerr=errors, capsize=3)
|
||||||
|
|
||||||
|
ax.set_xlabel('Тип лабиринта', fontsize=12)
|
||||||
|
ax.set_ylabel('Время выполнения (мс)', fontsize=12)
|
||||||
|
ax.set_title('Сравнение времени выполнения алгоритмов поиска пути', fontsize=14)
|
||||||
|
ax.set_xticks(x + width * 1.5)
|
||||||
|
ax.set_xticklabels(maze_types, rotation=45, ha='right')
|
||||||
|
ax.legend()
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('time_comparison.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_visited_cells(df):
|
||||||
|
"""График 2: Количество посещённых клеток."""
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 6))
|
||||||
|
|
||||||
|
maze_types = df['maze_type'].unique()
|
||||||
|
strategies = df['strategy'].unique()
|
||||||
|
|
||||||
|
x = np.arange(len(maze_types))
|
||||||
|
width = 0.2
|
||||||
|
|
||||||
|
for i, strategy in enumerate(strategies):
|
||||||
|
strategy_data = df[df['strategy'] == strategy]
|
||||||
|
visited = []
|
||||||
|
for maze in maze_types:
|
||||||
|
row = strategy_data[strategy_data['maze_type'] == maze]
|
||||||
|
if not row.empty:
|
||||||
|
visited.append(row['avg_visited'].values[0])
|
||||||
|
else:
|
||||||
|
visited.append(0)
|
||||||
|
|
||||||
|
ax.bar(x + i*width, visited, width, label=strategy)
|
||||||
|
|
||||||
|
ax.set_xlabel('Тип лабиринта', fontsize=12)
|
||||||
|
ax.set_ylabel('Количество посещённых клеток', fontsize=12)
|
||||||
|
ax.set_title('Сравнение количества посещённых клеток', fontsize=14)
|
||||||
|
ax.set_xticks(x + width * 1.5)
|
||||||
|
ax.set_xticklabels(maze_types, rotation=45, ha='right')
|
||||||
|
ax.legend()
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('visited_cells.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_path_length(df):
|
||||||
|
"""График 3: Длина найденного пути."""
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 6))
|
||||||
|
|
||||||
|
# Исключаем лабиринты без выхода (где путь = 0)
|
||||||
|
df_filtered = df[df['avg_path_len'] > 0]
|
||||||
|
|
||||||
|
maze_types = df_filtered['maze_type'].unique()
|
||||||
|
strategies = df_filtered['strategy'].unique()
|
||||||
|
|
||||||
|
x = np.arange(len(maze_types))
|
||||||
|
width = 0.2
|
||||||
|
|
||||||
|
for i, strategy in enumerate(strategies):
|
||||||
|
strategy_data = df_filtered[df_filtered['strategy'] == strategy]
|
||||||
|
path_lengths = []
|
||||||
|
for maze in maze_types:
|
||||||
|
row = strategy_data[strategy_data['maze_type'] == maze]
|
||||||
|
if not row.empty:
|
||||||
|
path_lengths.append(row['avg_path_len'].values[0])
|
||||||
|
else:
|
||||||
|
path_lengths.append(0)
|
||||||
|
|
||||||
|
ax.bar(x + i*width, path_lengths, width, label=strategy)
|
||||||
|
|
||||||
|
ax.set_xlabel('Тип лабиринта', fontsize=12)
|
||||||
|
ax.set_ylabel('Длина пути (количество клеток)', fontsize=12)
|
||||||
|
ax.set_title('Сравнение длины найденного пути', fontsize=14)
|
||||||
|
ax.set_xticks(x + width * 1.5)
|
||||||
|
ax.set_xticklabels(maze_types, rotation=45, ha='right')
|
||||||
|
ax.legend()
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('path_length.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_time_per_maze(df):
|
||||||
|
"""График 4: Для каждого лабиринта - сравнение стратегий."""
|
||||||
|
maze_types = df['maze_type'].unique()
|
||||||
|
strategies = df['strategy'].unique()
|
||||||
|
|
||||||
|
for maze in maze_types:
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
|
||||||
|
maze_data = df[df['maze_type'] == maze]
|
||||||
|
|
||||||
|
times = maze_data['avg_time_ms'].values
|
||||||
|
errors = maze_data['std_time_ms'].values
|
||||||
|
strategy_names = maze_data['strategy'].values
|
||||||
|
|
||||||
|
bars = ax.bar(strategy_names, times, yerr=errors, capsize=5,
|
||||||
|
color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'])
|
||||||
|
|
||||||
|
ax.set_xlabel('Алгоритм', fontsize=12)
|
||||||
|
ax.set_ylabel('Время выполнения (мс)', fontsize=12)
|
||||||
|
ax.set_title(f'Сравнение алгоритмов на лабиринте: {maze}', fontsize=14)
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
# Добавление значений на столбцы
|
||||||
|
for bar, time_val in zip(bars, times):
|
||||||
|
height = bar.get_height()
|
||||||
|
ax.text(bar.get_x() + bar.get_width()/2., height + max(errors)/2,
|
||||||
|
f'{time_val:.1f}', ha='center', va='bottom', fontsize=10)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(f'time_{maze}.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_visited_per_maze(df):
|
||||||
|
"""График 5: Для каждого лабиринта - посещённые клетки."""
|
||||||
|
maze_types = df['maze_type'].unique()
|
||||||
|
|
||||||
|
for maze in maze_types:
|
||||||
|
fig, ax = plt.subplots(figsize=(10, 6))
|
||||||
|
|
||||||
|
maze_data = df[df['maze_type'] == maze]
|
||||||
|
|
||||||
|
visited = maze_data['avg_visited'].values
|
||||||
|
strategy_names = maze_data['strategy'].values
|
||||||
|
|
||||||
|
bars = ax.bar(strategy_names, visited,
|
||||||
|
color=['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728'])
|
||||||
|
|
||||||
|
ax.set_xlabel('Алгоритм', fontsize=12)
|
||||||
|
ax.set_ylabel('Количество посещённых клеток', fontsize=12)
|
||||||
|
ax.set_title(f'Посещённые клетки на лабиринте: {maze}', fontsize=14)
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
# Добавление значений на столбцы
|
||||||
|
for bar, val in zip(bars, visited):
|
||||||
|
height = bar.get_height()
|
||||||
|
ax.text(bar.get_x() + bar.get_width()/2., height,
|
||||||
|
f'{int(val)}', ha='center', va='bottom', fontsize=10)
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig(f'visited_{maze}.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def plot_efficiency_ratio(df):
|
||||||
|
"""График 6: Эффективность (время на клетку пути)."""
|
||||||
|
fig, ax = plt.subplots(figsize=(12, 6))
|
||||||
|
|
||||||
|
# Исключаем лабиринты без пути
|
||||||
|
df_filtered = df[(df['avg_path_len'] > 0) & (df['avg_time_ms'] > 0)].copy()
|
||||||
|
df_filtered['efficiency'] = df_filtered['avg_time_ms'] / df_filtered['avg_path_len']
|
||||||
|
|
||||||
|
maze_types = df_filtered['maze_type'].unique()
|
||||||
|
strategies = df_filtered['strategy'].unique()
|
||||||
|
|
||||||
|
x = np.arange(len(maze_types))
|
||||||
|
width = 0.2
|
||||||
|
|
||||||
|
for i, strategy in enumerate(strategies):
|
||||||
|
strategy_data = df_filtered[df_filtered['strategy'] == strategy]
|
||||||
|
efficiency = []
|
||||||
|
for maze in maze_types:
|
||||||
|
row = strategy_data[strategy_data['maze_type'] == maze]
|
||||||
|
if not row.empty:
|
||||||
|
efficiency.append(row['efficiency'].values[0])
|
||||||
|
else:
|
||||||
|
efficiency.append(0)
|
||||||
|
|
||||||
|
ax.bar(x + i*width, efficiency, width, label=strategy)
|
||||||
|
|
||||||
|
ax.set_xlabel('Тип лабиринта', fontsize=12)
|
||||||
|
ax.set_ylabel('Время на клетку пути (мс/клетку)', fontsize=12)
|
||||||
|
ax.set_title('Эффективность алгоритмов (время на единицу длины пути)', fontsize=14)
|
||||||
|
ax.set_xticks(x + width * 1.5)
|
||||||
|
ax.set_xticklabels(maze_types, rotation=45, ha='right')
|
||||||
|
ax.legend()
|
||||||
|
ax.grid(True, alpha=0.3, axis='y')
|
||||||
|
|
||||||
|
plt.tight_layout()
|
||||||
|
plt.savefig('efficiency_ratio.png', dpi=150)
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Основная функция: загрузка данных и построение всех графиков."""
|
||||||
|
try:
|
||||||
|
df = load_and_prepare_data('experiment_results.csv')
|
||||||
|
print("Данные успешно загружены")
|
||||||
|
print(f"Найдено {len(df)} записей")
|
||||||
|
print("\nСтруктура данных:")
|
||||||
|
print(df.head())
|
||||||
|
|
||||||
|
print("\nПостроение графиков...")
|
||||||
|
|
||||||
|
# Базовые графики
|
||||||
|
plot_time_comparison(df)
|
||||||
|
plot_visited_cells(df)
|
||||||
|
plot_path_length(df)
|
||||||
|
|
||||||
|
# Детальные графики по каждому лабиринту
|
||||||
|
plot_time_per_maze(df)
|
||||||
|
plot_visited_per_maze(df)
|
||||||
|
|
||||||
|
# Аналитические графики
|
||||||
|
plot_efficiency_ratio(df)
|
||||||
|
|
||||||
|
print("\nВсе графики сохранены в текущей директории:")
|
||||||
|
print(" - time_comparison.png")
|
||||||
|
print(" - visited_cells.png")
|
||||||
|
print(" - path_length.png")
|
||||||
|
print(" - time_{maze}.png (для каждого лабиринта)")
|
||||||
|
print(" - visited_{maze}.png (для каждого лабиринта)")
|
||||||
|
print(" - efficiency_ratio.png")
|
||||||
|
print(" - summary_heatmap.png")
|
||||||
|
|
||||||
|
except FileNotFoundError:
|
||||||
|
print("Ошибка: файл experiment_results.csv не найден")
|
||||||
|
print("Сначала запустите основной скрипт для генерации результатов")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Ошибка: {e}")
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
843
ShapovalovKA/docs/data/2Task/t2.py
Normal file
|
|
@ -0,0 +1,843 @@
|
||||||
|
"""
|
||||||
|
Лабораторная работа: Применение паттернов проектирования
|
||||||
|
Этапы 1-6: Модель лабиринта, Builder, Strategy, MazeSolver, Observer/Command, эксперименты
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import csv
|
||||||
|
import random
|
||||||
|
from collections import deque
|
||||||
|
from typing import List, Tuple, Dict, Set, Optional
|
||||||
|
import heapq
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 1. Модель лабиринта
|
||||||
|
# ============================================================
|
||||||
|
class Cell:
|
||||||
|
"""Клетка лабиринта."""
|
||||||
|
def __init__(self, x: int, y: int, is_wall: bool = False, weight: int = 1):
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.is_wall = is_wall
|
||||||
|
self.is_start = False
|
||||||
|
self.is_exit = False
|
||||||
|
self.weight = weight # для взвешенных лабиринтов
|
||||||
|
|
||||||
|
def is_passable(self) -> bool:
|
||||||
|
return not self.is_wall
|
||||||
|
|
||||||
|
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))
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Cell({self.x},{self.y})"
|
||||||
|
|
||||||
|
|
||||||
|
class Maze:
|
||||||
|
"""Лабиринт, содержащий сетку клеток."""
|
||||||
|
def __init__(self, width: int, height: int):
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]
|
||||||
|
self.start_cell = None
|
||||||
|
self.exit_cell = None
|
||||||
|
|
||||||
|
def get_cell(self, x: int, y: int) -> Cell:
|
||||||
|
if 0 <= x < self.width and 0 <= y < self.height:
|
||||||
|
return self.cells[y][x]
|
||||||
|
raise IndexError("Координаты вне границ лабиринта")
|
||||||
|
|
||||||
|
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:
|
||||||
|
n = self.cells[ny][nx]
|
||||||
|
if n.is_passable():
|
||||||
|
neighbors.append(n)
|
||||||
|
return neighbors
|
||||||
|
|
||||||
|
def set_start(self, x: int, y: int):
|
||||||
|
cell = self.get_cell(x, y)
|
||||||
|
cell.is_start = True
|
||||||
|
self.start_cell = cell
|
||||||
|
|
||||||
|
def set_exit(self, x: int, y: int):
|
||||||
|
cell = self.get_cell(x, y)
|
||||||
|
cell.is_exit = True
|
||||||
|
self.exit_cell = cell
|
||||||
|
|
||||||
|
def copy(self):
|
||||||
|
"""Создаёт глубокую копию лабиринта (для взвешенных вариантов)."""
|
||||||
|
new_maze = Maze(self.width, self.height)
|
||||||
|
for y in range(self.height):
|
||||||
|
for x in range(self.width):
|
||||||
|
orig = self.cells[y][x]
|
||||||
|
new_maze.cells[y][x] = Cell(x, y, orig.is_wall, orig.weight)
|
||||||
|
if orig.is_start:
|
||||||
|
new_maze.set_start(x, y)
|
||||||
|
if orig.is_exit:
|
||||||
|
new_maze.set_exit(x, y)
|
||||||
|
return new_maze
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 2. Builder для загрузки из текстового файла
|
||||||
|
# ============================================================
|
||||||
|
class MazeBuilder(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def build_from_file(self, filename: str) -> Maze:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TextFileMazeBuilder(MazeBuilder):
|
||||||
|
"""Строитель лабиринта из текстового файла."""
|
||||||
|
def build_from_file(self, filename: str) -> Maze:
|
||||||
|
with open(filename, 'r', encoding='utf-8') as f:
|
||||||
|
lines = [line.rstrip('\n') for line in f]
|
||||||
|
|
||||||
|
if not lines:
|
||||||
|
raise ValueError("Файл пуст")
|
||||||
|
|
||||||
|
height = len(lines)
|
||||||
|
width = max(len(line) for line in lines)
|
||||||
|
|
||||||
|
grid = []
|
||||||
|
for y, line in enumerate(lines):
|
||||||
|
row = []
|
||||||
|
for x in range(width):
|
||||||
|
ch = line[x] if x < len(line) else ' '
|
||||||
|
row.append(ch)
|
||||||
|
grid.append(row)
|
||||||
|
|
||||||
|
maze = Maze(width, height)
|
||||||
|
start_found = exit_found = False
|
||||||
|
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
ch = grid[y][x]
|
||||||
|
cell = maze.get_cell(x, y)
|
||||||
|
|
||||||
|
if ch == '#':
|
||||||
|
cell.is_wall = True
|
||||||
|
elif ch == 'S':
|
||||||
|
if start_found:
|
||||||
|
raise ValueError("Обнаружено несколько стартовых клеток 'S'")
|
||||||
|
cell.is_start = True
|
||||||
|
maze.start_cell = cell
|
||||||
|
start_found = True
|
||||||
|
elif ch == 'E':
|
||||||
|
if exit_found:
|
||||||
|
raise ValueError("Обнаружено несколько выходных клеток 'E'")
|
||||||
|
cell.is_exit = True
|
||||||
|
maze.exit_cell = cell
|
||||||
|
exit_found = True
|
||||||
|
elif ch != ' ':
|
||||||
|
raise ValueError(f"Недопустимый символ '{ch}' в позиции ({x},{y})")
|
||||||
|
|
||||||
|
if not start_found:
|
||||||
|
raise ValueError("Отсутствует стартовая клетка 'S'")
|
||||||
|
if not exit_found:
|
||||||
|
raise ValueError("Отсутствует выходная клетка 'E'")
|
||||||
|
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 3. Стратегии поиска пути (возвращают путь и число посещённых)
|
||||||
|
# ============================================================
|
||||||
|
class PathFindingStrategy(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
||||||
|
"""Возвращает (путь, количество посещённых клеток)."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в ширину – гарантирует кратчайший путь."""
|
||||||
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
||||||
|
if start == exit_cell:
|
||||||
|
return [start], 1
|
||||||
|
|
||||||
|
queue = deque([start])
|
||||||
|
visited = {start}
|
||||||
|
parent = {start: None}
|
||||||
|
visited_count = 1
|
||||||
|
|
||||||
|
while queue:
|
||||||
|
cur = queue.popleft()
|
||||||
|
if cur == exit_cell:
|
||||||
|
path = []
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cur):
|
||||||
|
if nb not in visited:
|
||||||
|
visited.add(nb)
|
||||||
|
visited_count += 1
|
||||||
|
parent[nb] = cur
|
||||||
|
queue.append(nb)
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
class DFSStrategy(PathFindingStrategy):
|
||||||
|
"""Поиск в глубину – быстрый, но не гарантирует кратчайший путь."""
|
||||||
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
||||||
|
if start == exit_cell:
|
||||||
|
return [start], 1
|
||||||
|
|
||||||
|
stack = [(start, [start])]
|
||||||
|
visited = set()
|
||||||
|
visited_count = 0
|
||||||
|
|
||||||
|
while stack:
|
||||||
|
cur, path = stack.pop()
|
||||||
|
if cur in visited:
|
||||||
|
continue
|
||||||
|
visited.add(cur)
|
||||||
|
visited_count += 1
|
||||||
|
|
||||||
|
if cur == exit_cell:
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cur):
|
||||||
|
if nb not in visited:
|
||||||
|
stack.append((nb, path + [nb]))
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
class AStarStrategy(PathFindingStrategy):
|
||||||
|
"""А* с эвристикой Манхэттенского расстояния."""
|
||||||
|
def _heuristic(self, a: Cell, b: Cell) -> float:
|
||||||
|
return abs(a.x - b.x) + abs(a.y - b.y)
|
||||||
|
|
||||||
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
||||||
|
if start == exit_cell:
|
||||||
|
return [start], 1
|
||||||
|
|
||||||
|
open_set = []
|
||||||
|
counter = 0
|
||||||
|
heapq.heappush(open_set, (0, counter, start))
|
||||||
|
g_score = {start: 0}
|
||||||
|
f_score = {start: self._heuristic(start, exit_cell)}
|
||||||
|
parent = {start: None}
|
||||||
|
visited_count = 1
|
||||||
|
|
||||||
|
while open_set:
|
||||||
|
_, _, cur = heapq.heappop(open_set)
|
||||||
|
if cur == exit_cell:
|
||||||
|
path = []
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cur):
|
||||||
|
move_cost = nb.weight
|
||||||
|
tentative = g_score[cur] + move_cost
|
||||||
|
if nb not in g_score or tentative < g_score[nb]:
|
||||||
|
parent[nb] = cur
|
||||||
|
g_score[nb] = tentative
|
||||||
|
f_score[nb] = tentative + self._heuristic(nb, exit_cell)
|
||||||
|
counter += 1
|
||||||
|
heapq.heappush(open_set, (f_score[nb], counter, nb))
|
||||||
|
visited_count += 1
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
class DijkstraStrategy(PathFindingStrategy):
|
||||||
|
"""Алгоритм Дейкстры для взвешенных графов."""
|
||||||
|
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> Tuple[List[Cell], int]:
|
||||||
|
if start == exit_cell:
|
||||||
|
return [start], 1
|
||||||
|
|
||||||
|
pq = []
|
||||||
|
counter = 0
|
||||||
|
heapq.heappush(pq, (0, counter, start))
|
||||||
|
dist = {start: 0}
|
||||||
|
parent = {start: None}
|
||||||
|
visited_count = 1
|
||||||
|
|
||||||
|
while pq:
|
||||||
|
cur_dist, _, cur = heapq.heappop(pq)
|
||||||
|
if cur_dist > dist.get(cur, float('inf')):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cur == exit_cell:
|
||||||
|
path = []
|
||||||
|
while cur is not None:
|
||||||
|
path.append(cur)
|
||||||
|
cur = parent[cur]
|
||||||
|
path.reverse()
|
||||||
|
return path, visited_count
|
||||||
|
|
||||||
|
for nb in maze.get_neighbors(cur):
|
||||||
|
new_dist = cur_dist + nb.weight
|
||||||
|
if new_dist < dist.get(nb, float('inf')):
|
||||||
|
dist[nb] = new_dist
|
||||||
|
parent[nb] = cur
|
||||||
|
counter += 1
|
||||||
|
heapq.heappush(pq, (new_dist, counter, nb))
|
||||||
|
visited_count += 1
|
||||||
|
|
||||||
|
return [], visited_count
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 4. MazeSolver (оркестратор)
|
||||||
|
# ============================================================
|
||||||
|
@dataclass
|
||||||
|
class SearchStats:
|
||||||
|
path_length: int
|
||||||
|
visited_cells: int
|
||||||
|
time_ms: float
|
||||||
|
|
||||||
|
|
||||||
|
class MazeSolver:
|
||||||
|
"""Оркестратор: управляет лабиринтом и стратегией поиска."""
|
||||||
|
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
|
||||||
|
self.maze = maze
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def set_strategy(self, strategy: PathFindingStrategy):
|
||||||
|
self.strategy = strategy
|
||||||
|
|
||||||
|
def solve(self) -> SearchStats:
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
path, visited = self.strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
||||||
|
end_time = time.perf_counter()
|
||||||
|
return SearchStats(len(path), visited, (end_time - start_time) * 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 5. Observer и Command (визуализация и пошаговое управление)
|
||||||
|
# ============================================================
|
||||||
|
class Observer(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def update(self, event: str, data: dict = None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConsoleView(Observer):
|
||||||
|
"""Отображает лабиринт, позицию игрока и найденный путь."""
|
||||||
|
def __init__(self):
|
||||||
|
self.last_maze = None
|
||||||
|
self.last_player_pos = None
|
||||||
|
self.last_path = None
|
||||||
|
|
||||||
|
def update(self, event: str, data: dict = None):
|
||||||
|
if event == "maze_loaded":
|
||||||
|
self.last_maze = data["maze"]
|
||||||
|
self.render()
|
||||||
|
elif event == "player_moved":
|
||||||
|
self.last_maze = data["maze"]
|
||||||
|
self.last_player_pos = data["player_pos"]
|
||||||
|
self.render()
|
||||||
|
elif event == "path_found":
|
||||||
|
self.last_path = data["path"]
|
||||||
|
self.render()
|
||||||
|
elif event == "clear_path":
|
||||||
|
self.last_path = None
|
||||||
|
self.render()
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
if self.last_maze is None:
|
||||||
|
print("Нет лабиринта для отображения")
|
||||||
|
return
|
||||||
|
|
||||||
|
maze = self.last_maze
|
||||||
|
player = self.last_player_pos
|
||||||
|
path_set = set(self.last_path) if self.last_path else set()
|
||||||
|
|
||||||
|
for y in range(maze.height):
|
||||||
|
row = []
|
||||||
|
for x in range(maze.width):
|
||||||
|
cell = maze.get_cell(x, y)
|
||||||
|
if player and cell == player:
|
||||||
|
row.append('@')
|
||||||
|
elif cell == maze.start_cell:
|
||||||
|
row.append('S')
|
||||||
|
elif cell == maze.exit_cell:
|
||||||
|
row.append('E')
|
||||||
|
elif cell in path_set and cell.is_passable():
|
||||||
|
row.append('*')
|
||||||
|
elif cell.is_wall:
|
||||||
|
row.append('#')
|
||||||
|
else:
|
||||||
|
row.append(' ')
|
||||||
|
print(''.join(row))
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
class Player:
|
||||||
|
"""Игрок, перемещающийся по лабиринту."""
|
||||||
|
def __init__(self, start_cell: Cell):
|
||||||
|
self.position = start_cell
|
||||||
|
|
||||||
|
def move_to(self, new_cell: Cell):
|
||||||
|
self.position = new_cell
|
||||||
|
|
||||||
|
|
||||||
|
class Command(ABC):
|
||||||
|
@abstractmethod
|
||||||
|
def execute(self) -> bool:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def undo(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class MoveCommand(Command):
|
||||||
|
"""Команда перемещения игрока."""
|
||||||
|
def __init__(self, player: Player, maze: Maze, direction: str):
|
||||||
|
self.player = player
|
||||||
|
self.maze = maze
|
||||||
|
self.direction = direction
|
||||||
|
self.prev_position = player.position
|
||||||
|
self.new_position = None
|
||||||
|
|
||||||
|
def execute(self) -> bool:
|
||||||
|
dx, dy = 0, 0
|
||||||
|
if self.direction == 'W':
|
||||||
|
dy = -1
|
||||||
|
elif self.direction == 'S':
|
||||||
|
dy = 1
|
||||||
|
elif self.direction == 'A':
|
||||||
|
dx = -1
|
||||||
|
elif self.direction == 'D':
|
||||||
|
dx = 1
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
nx = self.player.position.x + dx
|
||||||
|
ny = self.player.position.y + dy
|
||||||
|
try:
|
||||||
|
target = self.maze.get_cell(nx, ny)
|
||||||
|
if target.is_passable():
|
||||||
|
self.new_position = target
|
||||||
|
self.player.move_to(target)
|
||||||
|
return True
|
||||||
|
except IndexError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.prev_position:
|
||||||
|
self.player.move_to(self.prev_position)
|
||||||
|
|
||||||
|
|
||||||
|
class GameController:
|
||||||
|
"""Управляет игрой, наблюдателями и командами."""
|
||||||
|
def __init__(self, maze: Maze):
|
||||||
|
self.maze = maze
|
||||||
|
self.player = Player(maze.start_cell)
|
||||||
|
self.observers = []
|
||||||
|
self.command_stack = []
|
||||||
|
|
||||||
|
def attach(self, observer: Observer):
|
||||||
|
self.observers.append(observer)
|
||||||
|
|
||||||
|
def detach(self, observer: Observer):
|
||||||
|
self.observers.remove(observer)
|
||||||
|
|
||||||
|
def notify(self, event: str, data: dict = None):
|
||||||
|
for obs in self.observers:
|
||||||
|
obs.update(event, data or {})
|
||||||
|
|
||||||
|
def load_maze(self, maze: Maze):
|
||||||
|
self.maze = maze
|
||||||
|
self.player = Player(maze.start_cell)
|
||||||
|
self.notify("maze_loaded", {"maze": maze})
|
||||||
|
|
||||||
|
def find_path(self, strategy: PathFindingStrategy) -> List[Cell]:
|
||||||
|
solver = MazeSolver(self.maze, strategy)
|
||||||
|
stats = solver.solve()
|
||||||
|
print(f"Длина пути: {stats.path_length}, посещено: {stats.visited_cells}, время: {stats.time_ms:.3f} мс")
|
||||||
|
path, _ = strategy.find_path(self.maze, self.maze.start_cell, self.maze.exit_cell)
|
||||||
|
self.notify("path_found", {"path": path})
|
||||||
|
return path
|
||||||
|
|
||||||
|
def clear_path(self):
|
||||||
|
self.notify("clear_path", {})
|
||||||
|
|
||||||
|
def execute_command(self, cmd: Command):
|
||||||
|
if cmd.execute():
|
||||||
|
self.command_stack.append(cmd)
|
||||||
|
self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})
|
||||||
|
|
||||||
|
def undo(self):
|
||||||
|
if self.command_stack:
|
||||||
|
cmd = self.command_stack.pop()
|
||||||
|
cmd.undo()
|
||||||
|
self.notify("player_moved", {"maze": self.maze, "player_pos": self.player.position})
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Этап 6. Генераторы тестовых лабиринтов
|
||||||
|
# ============================================================
|
||||||
|
def generate_simple_maze(width: int, height: int) -> Maze:
|
||||||
|
"""Маленький лабиринт с простым путём."""
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
maze.cells[y][x].is_wall = True
|
||||||
|
|
||||||
|
x, y = 0, 0
|
||||||
|
path = [(x, y)]
|
||||||
|
while x < width - 1 or y < height - 1:
|
||||||
|
if x < width - 1 and (y == height - 1 or random.random() < 0.5):
|
||||||
|
x += 1
|
||||||
|
else:
|
||||||
|
if y < height - 1:
|
||||||
|
y += 1
|
||||||
|
path.append((x, y))
|
||||||
|
|
||||||
|
for px, py in path:
|
||||||
|
maze.cells[py][px].is_wall = False
|
||||||
|
|
||||||
|
maze.set_start(0, 0)
|
||||||
|
maze.set_exit(width - 1, height - 1)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
def generate_with_dead_ends(width: int, height: int) -> Maze:
|
||||||
|
"""Средний лабиринт с гарантированным путём и множеством тупиков."""
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
maze.cells[y][x].is_wall = True
|
||||||
|
|
||||||
|
x, y = 0, 0
|
||||||
|
main_path = []
|
||||||
|
while x < width - 1 or y < height - 1:
|
||||||
|
main_path.append((x, y))
|
||||||
|
if x < width - 1 and (y == height - 1 or random.random() < 0.6):
|
||||||
|
x += 1
|
||||||
|
else:
|
||||||
|
if y < height - 1:
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
x += 1
|
||||||
|
main_path.append((width - 1, height - 1))
|
||||||
|
|
||||||
|
for px, py in main_path:
|
||||||
|
maze.cells[py][px].is_wall = False
|
||||||
|
|
||||||
|
num_dead_ends = int(width * height * 0.08)
|
||||||
|
for _ in range(num_dead_ends):
|
||||||
|
base_x, base_y = random.choice(main_path)
|
||||||
|
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
|
||||||
|
random.shuffle(directions)
|
||||||
|
for dx, dy in directions:
|
||||||
|
nx, ny = base_x + dx, base_y + dy
|
||||||
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
||||||
|
length = random.randint(2, 4)
|
||||||
|
for step in range(length):
|
||||||
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
||||||
|
maze.cells[ny][nx].is_wall = False
|
||||||
|
nx += dx
|
||||||
|
ny += dy
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
break
|
||||||
|
|
||||||
|
maze.set_start(0, 0)
|
||||||
|
maze.set_exit(width - 1, height - 1)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
def generate_complex_maze(width: int, height: int) -> Maze:
|
||||||
|
"""Большой лабиринт с гарантированным путём и высокой запутанностью."""
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
maze.cells[y][x].is_wall = True
|
||||||
|
|
||||||
|
x, y = 0, 0
|
||||||
|
main_path = []
|
||||||
|
while x < width - 1 or y < height - 1:
|
||||||
|
main_path.append((x, y))
|
||||||
|
if x < width - 1 and (y == height - 1 or random.random() < 0.7):
|
||||||
|
x += 1
|
||||||
|
else:
|
||||||
|
if y < height - 1:
|
||||||
|
y += 1
|
||||||
|
else:
|
||||||
|
x += 1
|
||||||
|
main_path.append((width - 1, height - 1))
|
||||||
|
|
||||||
|
for px, py in main_path:
|
||||||
|
maze.cells[py][px].is_wall = False
|
||||||
|
|
||||||
|
num_branches = int(width * height * 0.12)
|
||||||
|
for _ in range(num_branches):
|
||||||
|
base_x, base_y = random.choice(main_path)
|
||||||
|
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
|
||||||
|
random.shuffle(directions)
|
||||||
|
for dx, dy in directions:
|
||||||
|
nx, ny = base_x + dx, base_y + dy
|
||||||
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
||||||
|
length = random.randint(1, 5)
|
||||||
|
branch = []
|
||||||
|
for step in range(length):
|
||||||
|
if 0 <= nx < width and 0 <= ny < height and maze.cells[ny][nx].is_wall:
|
||||||
|
maze.cells[ny][nx].is_wall = False
|
||||||
|
branch.append((nx, ny))
|
||||||
|
nx += dx
|
||||||
|
ny += dy
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
if random.random() < 0.3 and len(branch) >= 2:
|
||||||
|
bx, by = branch[-1]
|
||||||
|
for ddx, ddy in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
|
||||||
|
nnx, nny = bx + ddx, by + ddy
|
||||||
|
if (0 <= nnx < width and 0 <= nny < height and
|
||||||
|
maze.cells[nny][nnx].is_wall and random.random() < 0.5):
|
||||||
|
maze.cells[nny][nnx].is_wall = False
|
||||||
|
break
|
||||||
|
|
||||||
|
maze.set_start(0, 0)
|
||||||
|
maze.set_exit(width - 1, height - 1)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
def generate_empty_maze(width: int, height: int) -> Maze:
|
||||||
|
"""Пустой лабиринт без стен."""
|
||||||
|
maze = Maze(width, height)
|
||||||
|
for y in range(height):
|
||||||
|
for x in range(width):
|
||||||
|
maze.cells[y][x].is_wall = False
|
||||||
|
maze.set_start(0, 0)
|
||||||
|
maze.set_exit(width - 1, height - 1)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
def generate_no_exit_maze(width: int, height: int) -> Maze:
|
||||||
|
"""Лабиринт без выхода (выход окружён стенами)."""
|
||||||
|
maze = generate_empty_maze(width, height)
|
||||||
|
ex, ey = width - 1, height - 1
|
||||||
|
for dx, dy in [(0, 0), (0, -1), (0, 1), (-1, 0), (1, 0), (-1, -1), (-1, 1), (1, -1), (1, 1)]:
|
||||||
|
nx, ny = ex + dx, ey + dy
|
||||||
|
if 0 <= nx < width and 0 <= ny < height:
|
||||||
|
if not (nx == 0 and ny == 0):
|
||||||
|
maze.cells[ny][nx].is_wall = True
|
||||||
|
maze.cells[ey][ex].is_wall = False
|
||||||
|
maze.set_exit(ex, ey)
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Экспериментальная часть
|
||||||
|
# ============================================================
|
||||||
|
def run_experiment(maze: Maze, strategies: List[Tuple[str, PathFindingStrategy]], runs: int = 5) -> List[dict]:
|
||||||
|
"""Запускает эксперимент на одном лабиринте и возвращает усреднённые результаты."""
|
||||||
|
results = []
|
||||||
|
for name, strategy in strategies:
|
||||||
|
times = []
|
||||||
|
visited_counts = []
|
||||||
|
path_lengths = []
|
||||||
|
for _ in range(runs):
|
||||||
|
solver = MazeSolver(maze, strategy)
|
||||||
|
stats = solver.solve()
|
||||||
|
times.append(stats.time_ms)
|
||||||
|
visited_counts.append(stats.visited_cells)
|
||||||
|
path_lengths.append(stats.path_length)
|
||||||
|
|
||||||
|
avg_time = sum(times) / runs
|
||||||
|
variance = sum((t - avg_time) ** 2 for t in times) / runs
|
||||||
|
std_time = variance ** 0.5
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
'maze_type': '',
|
||||||
|
'strategy': name,
|
||||||
|
'avg_time_ms': avg_time,
|
||||||
|
'std_time_ms': std_time,
|
||||||
|
'avg_visited': sum(visited_counts) / runs,
|
||||||
|
'avg_path_len': sum(path_lengths) / runs,
|
||||||
|
'path_found': all(l > 0 for l in path_lengths)
|
||||||
|
})
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def save_results_to_csv(results: List[dict], filename: str):
|
||||||
|
"""Сохраняет результаты в CSV с разделителем ';' для совместимости с Excel."""
|
||||||
|
if not results:
|
||||||
|
return
|
||||||
|
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
|
||||||
|
writer = csv.DictWriter(f, fieldnames=results[0].keys(), delimiter=';')
|
||||||
|
writer.writeheader()
|
||||||
|
for row in results:
|
||||||
|
row_copy = {}
|
||||||
|
for k, v in row.items():
|
||||||
|
if isinstance(v, float):
|
||||||
|
row_copy[k] = f"{v:.6f}".replace(',', '.')
|
||||||
|
else:
|
||||||
|
row_copy[k] = v
|
||||||
|
writer.writerow(row_copy)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Взвешенные лабиринты (опциональное задание)
|
||||||
|
# ============================================================
|
||||||
|
def assign_weights_random(maze: Maze, weights: List[Tuple[float, int]]) -> Maze:
|
||||||
|
"""Присваивает веса клеткам согласно вероятностям."""
|
||||||
|
for y in range(maze.height):
|
||||||
|
for x in range(maze.width):
|
||||||
|
if not maze.cells[y][x].is_wall:
|
||||||
|
r = random.random()
|
||||||
|
cum = 0
|
||||||
|
for prob, w in weights:
|
||||||
|
cum += prob
|
||||||
|
if r < cum:
|
||||||
|
maze.cells[y][x].weight = w
|
||||||
|
break
|
||||||
|
return maze
|
||||||
|
|
||||||
|
|
||||||
|
def weighted_experiment():
|
||||||
|
"""Дополнительный эксперимент со взвешенными клетками."""
|
||||||
|
print("\n=== ВЗВЕШЕННЫЕ ЛАБИРИНТЫ (опциональное задание) ===")
|
||||||
|
maze = generate_with_dead_ends(30, 30)
|
||||||
|
assign_weights_random(maze, [(0.8, 1), (0.15, 3), (0.05, 2)])
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
("A* (манхэттен)", AStarStrategy()),
|
||||||
|
("Dijkstra", DijkstraStrategy())
|
||||||
|
]
|
||||||
|
|
||||||
|
print("Лабиринт 30x30 со взвешенными клетками (болото 3, песок 2, асфальт 1)")
|
||||||
|
results = run_experiment(maze, strategies, runs=10)
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
print(f"{r['strategy']:15} | Время: {r['avg_time_ms']:.2f} мс | "
|
||||||
|
f"Посещено: {r['avg_visited']:.0f} | Длина пути: {r['avg_path_len']:.0f}")
|
||||||
|
|
||||||
|
# Сравнение с BFS
|
||||||
|
bfs = BFSStrategy()
|
||||||
|
path_bfs, _ = bfs.find_path(maze, maze.start_cell, maze.exit_cell)
|
||||||
|
if path_bfs:
|
||||||
|
cost_bfs = sum(cell.weight for cell in path_bfs)
|
||||||
|
print(f"BFS нашёл путь длиной {len(path_bfs)} клеток, стоимость = {cost_bfs}")
|
||||||
|
|
||||||
|
path_dijkstra, _ = DijkstraStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
|
||||||
|
if path_dijkstra:
|
||||||
|
cost_dijkstra = sum(cell.weight for cell in path_dijkstra)
|
||||||
|
print(f"Dijkstra нашёл путь длиной {len(path_dijkstra)} клеток, стоимость = {cost_dijkstra}")
|
||||||
|
|
||||||
|
path_astar, _ = AStarStrategy().find_path(maze, maze.start_cell, maze.exit_cell)
|
||||||
|
if path_astar:
|
||||||
|
cost_astar = sum(cell.weight for cell in path_astar)
|
||||||
|
print(f"A* нашёл путь длиной {len(path_astar)} клеток, стоимость = {cost_astar}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Демонстрация работы Observer и Command (по желанию)
|
||||||
|
# ============================================================
|
||||||
|
def demo_observer_command():
|
||||||
|
"""Демонстрирует паттерны Observer и Command."""
|
||||||
|
print("\n=== ДЕМОНСТРАЦИЯ OBSERVER И COMMAND ===")
|
||||||
|
maze = generate_simple_maze(10, 10)
|
||||||
|
|
||||||
|
controller = GameController(maze)
|
||||||
|
view = ConsoleView()
|
||||||
|
controller.attach(view)
|
||||||
|
|
||||||
|
print("Лабиринт загружен:")
|
||||||
|
controller.load_maze(maze)
|
||||||
|
|
||||||
|
print("Поиск пути с помощью BFS:")
|
||||||
|
controller.find_path(BFSStrategy())
|
||||||
|
|
||||||
|
input("Нажмите Enter для пошагового управления...")
|
||||||
|
|
||||||
|
controller.clear_path()
|
||||||
|
print("\nУправление: W/A/S/D - движение, Z - отмена, Q - выход")
|
||||||
|
while True:
|
||||||
|
cmd = input("> ").upper().strip()
|
||||||
|
if cmd == 'Q':
|
||||||
|
break
|
||||||
|
elif cmd == 'Z':
|
||||||
|
controller.undo()
|
||||||
|
elif cmd in ('W', 'A', 'S', 'D'):
|
||||||
|
move_cmd = MoveCommand(controller.player, controller.maze, cmd)
|
||||||
|
controller.execute_command(move_cmd)
|
||||||
|
else:
|
||||||
|
print("Неизвестная команда")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Основной эксперимент
|
||||||
|
# ============================================================
|
||||||
|
def main():
|
||||||
|
"""Основной эксперимент: сравнение стратегий на различных лабиринтах."""
|
||||||
|
print("=== ЗАПУСК ЭКСПЕРИМЕНТОВ ===")
|
||||||
|
|
||||||
|
strategies = [
|
||||||
|
("BFS", BFSStrategy()),
|
||||||
|
("DFS", DFSStrategy()),
|
||||||
|
("A*", AStarStrategy()),
|
||||||
|
("Dijkstra", DijkstraStrategy())
|
||||||
|
]
|
||||||
|
|
||||||
|
# Генерация тестовых лабиринтов
|
||||||
|
maze_definitions = {
|
||||||
|
"small_10x10_simple": generate_simple_maze(10, 10),
|
||||||
|
"medium_50x50_deadends": generate_with_dead_ends(50, 50),
|
||||||
|
"large_100x100_complex": generate_complex_maze(100, 100),
|
||||||
|
"empty_50x50": generate_empty_maze(50, 50),
|
||||||
|
"no_exit_50x50": generate_no_exit_maze(50, 50)
|
||||||
|
}
|
||||||
|
|
||||||
|
all_results = []
|
||||||
|
|
||||||
|
for maze_name, maze in maze_definitions.items():
|
||||||
|
print(f"\nЗапуск на лабиринте: {maze_name} ({maze.width}x{maze.height})")
|
||||||
|
results = run_experiment(maze, strategies, runs=5)
|
||||||
|
|
||||||
|
for r in results:
|
||||||
|
r['maze_type'] = maze_name
|
||||||
|
all_results.append(r)
|
||||||
|
|
||||||
|
# Вывод промежуточных результатов
|
||||||
|
for r in results:
|
||||||
|
print(f" {r['strategy']:8} | Время: {r['avg_time_ms']:7.2f}±{r['std_time_ms']:.2f} мс | "
|
||||||
|
f"Посещено: {r['avg_visited']:7.0f} | Длина пути: {r['avg_path_len']:5.0f}")
|
||||||
|
|
||||||
|
# Сохранение результатов
|
||||||
|
save_results_to_csv(all_results, "experiment_results.csv")
|
||||||
|
print("\nРезультаты сохранены в experiment_results.csv")
|
||||||
|
|
||||||
|
# Вывод сводной таблицы
|
||||||
|
print("\n" + "=" * 100)
|
||||||
|
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ")
|
||||||
|
print("=" * 100)
|
||||||
|
print(f"{'Лабиринт':<25} | {'Стратегия':<10} | {'Время (мс)':<15} | {'Посещено':<10} | {'Длина пути':<10}")
|
||||||
|
print("-" * 100)
|
||||||
|
for r in all_results:
|
||||||
|
print(f"{r['maze_type']:<25} | {r['strategy']:<10} | {r['avg_time_ms']:>8.2f} ± {r['std_time_ms']:<5.2f} | "
|
||||||
|
f"{r['avg_visited']:>8.0f} | {r['avg_path_len']:>8.0f}")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Запуск
|
||||||
|
# ============================================================
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
# Раскомментируйте для демонстрации:
|
||||||
|
# demo_observer_command()
|
||||||
|
# weighted_experiment()
|
||||||
BIN
ShapovalovKA/docs/data/2Task/time_comparison.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
ShapovalovKA/docs/data/2Task/time_empty_50x50.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ShapovalovKA/docs/data/2Task/time_large_100x100_complex.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
ShapovalovKA/docs/data/2Task/time_medium_50x50_deadends.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
ShapovalovKA/docs/data/2Task/time_no_exit_50x50.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ShapovalovKA/docs/data/2Task/time_small_10x10_simple.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_cells.png
Normal file
|
After Width: | Height: | Size: 86 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_empty_50x50.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_large_100x100_complex.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_medium_50x50_deadends.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_no_exit_50x50.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
ShapovalovKA/docs/data/2Task/visited_small_10x10_simple.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
89
ShapovalovKA/docs/data/2Task/Код диаграммы.txt
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
classDiagram
|
||||||
|
class Клетка {
|
||||||
|
+int x, y
|
||||||
|
+bool стена, старт, выход
|
||||||
|
+int вес
|
||||||
|
+проходима()
|
||||||
|
}
|
||||||
|
class Лабиринт {
|
||||||
|
+int ширина, высота
|
||||||
|
+Клетка[][] клетки
|
||||||
|
+Клетка стартоваяКлетка, выходнаяКлетка
|
||||||
|
+получитьКлетку(x,y)
|
||||||
|
+получитьСоседей(клетка)
|
||||||
|
+установитьСтарт(x,y)
|
||||||
|
+установитьВыход(x,y)
|
||||||
|
}
|
||||||
|
class СтроительЛабиринта {
|
||||||
|
<<интерфейс>>
|
||||||
|
+построитьИзФайла(имяФайла)
|
||||||
|
}
|
||||||
|
class СтроительИзТекстовогоФайла {
|
||||||
|
+построитьИзФайла(имяФайла)
|
||||||
|
}
|
||||||
|
class СтратегияПоискаПути {
|
||||||
|
<<интерфейс>>
|
||||||
|
+найтиПуть(лабиринт, старт, выход)
|
||||||
|
}
|
||||||
|
class ПоискВШирину
|
||||||
|
class ПоискВГлубину
|
||||||
|
class Астар
|
||||||
|
class Дейкстра
|
||||||
|
class РешательЛабиринта {
|
||||||
|
-Лабиринт лабиринт
|
||||||
|
-СтратегияПоискаПути стратегия
|
||||||
|
+установитьСтратегию(стратегия)
|
||||||
|
+решить() СтатистикаПоиска
|
||||||
|
}
|
||||||
|
class СтатистикаПоиска {
|
||||||
|
+int длинаПути
|
||||||
|
+int посещеноКлеток
|
||||||
|
+float времяМс
|
||||||
|
}
|
||||||
|
class Наблюдатель {
|
||||||
|
<<интерфейс>>
|
||||||
|
+обновить(событие, данные)
|
||||||
|
}
|
||||||
|
class КонсольноеПредставление {
|
||||||
|
+обновить(событие, данные)
|
||||||
|
-отобразить()
|
||||||
|
}
|
||||||
|
class Команда {
|
||||||
|
<<интерфейс>>
|
||||||
|
+выполнить()
|
||||||
|
+отменить()
|
||||||
|
}
|
||||||
|
class КомандаПеремещения {
|
||||||
|
-Игрок игрок
|
||||||
|
-Лабиринт лабиринт
|
||||||
|
-String направление
|
||||||
|
+выполнить()
|
||||||
|
+отменить()
|
||||||
|
}
|
||||||
|
class Игрок {
|
||||||
|
+Клетка позиция
|
||||||
|
+переместитьсяВ(клетка)
|
||||||
|
}
|
||||||
|
class КонтроллерИгры {
|
||||||
|
-List наблюдатели
|
||||||
|
-Stack команды
|
||||||
|
+подписать(наблюдатель)
|
||||||
|
+уведомить(событие, данные)
|
||||||
|
+выполнитьКоманду(команда)
|
||||||
|
+отменить()
|
||||||
|
}
|
||||||
|
|
||||||
|
СтроительЛабиринта <|-- СтроительИзТекстовогоФайла
|
||||||
|
СтратегияПоискаПути <|-- ПоискВШирину
|
||||||
|
СтратегияПоискаПути <|-- ПоискВГлубину
|
||||||
|
СтратегияПоискаПути <|-- Астар
|
||||||
|
СтратегияПоискаПути <|-- Дейкстра
|
||||||
|
РешательЛабиринта --> СтратегияПоискаПути
|
||||||
|
РешательЛабиринта --> Лабиринт
|
||||||
|
Лабиринт --> Клетка
|
||||||
|
КонтроллерИгры --> Игрок
|
||||||
|
КонтроллерИгры --> Команда
|
||||||
|
КонтроллерИгры --> Наблюдатель
|
||||||
|
КомандаПеремещения --> Игрок
|
||||||
|
КомандаПеремещения --> Лабиринт
|
||||||
|
КонсольноеПредставление ..|> Наблюдатель
|
||||||
1
ShapovalovKA/docs/data/2Task/Порядок использования 2.txt
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
t2.py -> res2.py
|
||||||
25
ShapovalovKA/себе.txt
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
cd 2026-rff_mp
|
||||||
|
cd shapovalovka
|
||||||
|
git add .
|
||||||
|
git commit -m "[] "
|
||||||
|
git push origin
|
||||||
|
|
||||||
|
Сделать запрос на слияние (Pull Request (PR))
|
||||||
|
|
||||||
|
|
||||||
|
git log --oneline - для логов
|
||||||
|
|
||||||
|
|
||||||
|
Логи:
|
||||||
|
|
||||||
|
e90dc47 (HEAD -> master) [10] Task 2 is complete
|
||||||
|
69a8554 [9] Task 2.7 in pogress
|
||||||
|
a644775 [8] Task 2.6 is complete
|
||||||
|
000535f [8] Task 2.6 is complete
|
||||||
|
34904a0 [7] Task 2 is started
|
||||||
|
1998da8 [6] Task 1 and analisys is complete
|
||||||
|
a88c7b8 [5] Task 1 is complete
|
||||||
|
81205c8 [4] t1.2 is complete
|
||||||
|
8dad5b8 [3] t1.1.4 in progress
|
||||||
|
9e5cee6 [2] t1.1 in progress
|
||||||
|
915990a [1] docs and data
|
||||||