Merge pull request '[12] test пуш' (#339) from shapovalovka/2026-rff_mp:ShapovalovKA into develop
Reviewed-on: #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
|
||||