Compare commits
No commits in common. "develop" and "2-nd" have entirely different histories.
1
.gitignore
vendored
|
|
@ -6,7 +6,6 @@ __pycache__/
|
||||||
|
|
||||||
# C extensions
|
# C extensions
|
||||||
*.so
|
*.so
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
|
|
|
||||||
5
.idea/.gitignore
vendored
|
|
@ -1,5 +0,0 @@
|
||||||
# Игнорируемые файлы по умолчанию
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
||||||
# HTTP-клиент на основе редактора
|
|
||||||
/httpRequests/
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.14" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="Python 3.14" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/2026-rff_mp.iml" filepath="$PROJECT_DIR$/.idea/2026-rff_mp.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="" vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Subproject commit 52c001a380431727397e4275c2be9d94fe5fcc8d
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
428b
|
|
||||||
|
|
@ -1,292 +0,0 @@
|
||||||
#!/usr/bin/env python3
|
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
import time
|
|
||||||
import random
|
|
||||||
import csv
|
|
||||||
import sys
|
|
||||||
sys.setrecursionlimit(30000)
|
|
||||||
|
|
||||||
def ll_create_node(name, phone):
|
|
||||||
return {'name': name, 'phone': phone, 'next': None}
|
|
||||||
|
|
||||||
def ll_insert(head, name, phone):
|
|
||||||
if head is None:
|
|
||||||
return ll_create_node(name, phone)
|
|
||||||
|
|
||||||
if head['name'] == name:
|
|
||||||
head['phone'] = phone
|
|
||||||
return head
|
|
||||||
|
|
||||||
current = head
|
|
||||||
while current['next'] is not None:
|
|
||||||
if current['next']['name'] == name:
|
|
||||||
current['next']['phone'] = phone
|
|
||||||
return head
|
|
||||||
current = current['next']
|
|
||||||
|
|
||||||
current['next'] = ll_create_node(name, phone)
|
|
||||||
return head
|
|
||||||
|
|
||||||
def ll_find(head, name):
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
if current['name'] == name:
|
|
||||||
return current['phone']
|
|
||||||
current = current['next']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def ll_delete(head, name):
|
|
||||||
if head is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if head['name'] == name:
|
|
||||||
return head['next']
|
|
||||||
|
|
||||||
current = head
|
|
||||||
while current['next'] is not None:
|
|
||||||
if current['next']['name'] == name:
|
|
||||||
current['next'] = current['next']['next']
|
|
||||||
return head
|
|
||||||
current = current['next']
|
|
||||||
|
|
||||||
return head
|
|
||||||
|
|
||||||
def ll_list_all(head):
|
|
||||||
records = []
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
records.append((current['name'], current['phone']))
|
|
||||||
current = current['next']
|
|
||||||
records.sort(key=lambda x: x[0])
|
|
||||||
return records
|
|
||||||
|
|
||||||
def hash_function(name, table_size):
|
|
||||||
return sum(ord(c) for c in name) % table_size
|
|
||||||
|
|
||||||
def ht_create_table(size=2000):
|
|
||||||
return [None] * size
|
|
||||||
|
|
||||||
def ht_insert(table, name, phone):
|
|
||||||
index = hash_function(name, len(table))
|
|
||||||
table[index] = ll_insert(table[index], name, phone)
|
|
||||||
|
|
||||||
def ht_find(table, name):
|
|
||||||
index = hash_function(name, len(table))
|
|
||||||
return ll_find(table[index], name)
|
|
||||||
|
|
||||||
def ht_delete(table, name):
|
|
||||||
index = hash_function(name, len(table))
|
|
||||||
table[index] = ll_delete(table[index], name)
|
|
||||||
|
|
||||||
def ht_list_all(table):
|
|
||||||
all_records = []
|
|
||||||
for bucket in table:
|
|
||||||
if bucket is not None:
|
|
||||||
current = bucket
|
|
||||||
while current is not None:
|
|
||||||
all_records.append((current['name'], current['phone']))
|
|
||||||
current = current['next']
|
|
||||||
all_records.sort(key=lambda x: x[0])
|
|
||||||
return all_records
|
|
||||||
|
|
||||||
def bst_create_node(name, phone):
|
|
||||||
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
|
||||||
|
|
||||||
def bst_insert(root, name, phone):
|
|
||||||
if root is None:
|
|
||||||
return bst_create_node(name, phone)
|
|
||||||
|
|
||||||
current = root
|
|
||||||
while True:
|
|
||||||
if name < current['name']:
|
|
||||||
if current['left'] is None:
|
|
||||||
current['left'] = bst_create_node(name, phone)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
current = current['left']
|
|
||||||
elif name > current['name']:
|
|
||||||
if current['right'] is None:
|
|
||||||
current['right'] = bst_create_node(name, phone)
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
current = current['right']
|
|
||||||
else:
|
|
||||||
current['phone'] = phone
|
|
||||||
break
|
|
||||||
|
|
||||||
return root
|
|
||||||
|
|
||||||
def bst_find(root, name):
|
|
||||||
current = root
|
|
||||||
while current is not None:
|
|
||||||
if name < current['name']:
|
|
||||||
current = current['left']
|
|
||||||
elif name > current['name']:
|
|
||||||
current = current['right']
|
|
||||||
else:
|
|
||||||
return current['phone']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def bst_find_min(node):
|
|
||||||
current = node
|
|
||||||
while current['left'] is not None:
|
|
||||||
current = current['left']
|
|
||||||
return current
|
|
||||||
|
|
||||||
def bst_delete(root, name):
|
|
||||||
if root is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
parent = None
|
|
||||||
current = root
|
|
||||||
|
|
||||||
while current is not None and current['name'] != name:
|
|
||||||
parent = current
|
|
||||||
if name < current['name']:
|
|
||||||
current = current['left']
|
|
||||||
else:
|
|
||||||
current = current['right']
|
|
||||||
|
|
||||||
if current is None:
|
|
||||||
return root
|
|
||||||
|
|
||||||
if current['left'] is None or current['right'] is None:
|
|
||||||
if current['left'] is not None:
|
|
||||||
child = current['left']
|
|
||||||
else:
|
|
||||||
child = current['right']
|
|
||||||
|
|
||||||
if parent is None:
|
|
||||||
return child
|
|
||||||
|
|
||||||
if parent['left'] == current:
|
|
||||||
parent['left'] = child
|
|
||||||
else:
|
|
||||||
parent['right'] = child
|
|
||||||
else:
|
|
||||||
successor_parent = current
|
|
||||||
successor = current['right']
|
|
||||||
|
|
||||||
while successor['left'] is not None:
|
|
||||||
successor_parent = successor
|
|
||||||
successor = successor['left']
|
|
||||||
|
|
||||||
current['name'] = successor['name']
|
|
||||||
current['phone'] = successor['phone']
|
|
||||||
|
|
||||||
if successor_parent['left'] == successor:
|
|
||||||
successor_parent['left'] = successor['right']
|
|
||||||
else:
|
|
||||||
successor_parent['right'] = successor['right']
|
|
||||||
|
|
||||||
return root
|
|
||||||
|
|
||||||
def bst_list_all(root):
|
|
||||||
records = []
|
|
||||||
stack = []
|
|
||||||
current = root
|
|
||||||
|
|
||||||
while stack or current is not None:
|
|
||||||
while current is not None:
|
|
||||||
stack.append(current)
|
|
||||||
current = current['left']
|
|
||||||
current = stack.pop()
|
|
||||||
records.append((current['name'], current['phone']))
|
|
||||||
current = current['right']
|
|
||||||
|
|
||||||
return records
|
|
||||||
|
|
||||||
def generate_data(n=10000):
|
|
||||||
records = [(f"User_{i:05d}", f"+7-999-{i:06d}") for i in range(n)]
|
|
||||||
records_shuffled = records.copy()
|
|
||||||
random.shuffle(records_shuffled)
|
|
||||||
records_sorted = sorted(records, key=lambda x: x[0])
|
|
||||||
return records_shuffled, records_sorted
|
|
||||||
|
|
||||||
def run_experiment(structure_name, insert_func, find_func, delete_func,
|
|
||||||
list_all_func, init_func, records, n_find=100):
|
|
||||||
|
|
||||||
data = init_func()
|
|
||||||
names = [r[0] for r in records]
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name, phone in records:
|
|
||||||
if structure_name == "HashTable":
|
|
||||||
insert_func(data, name, phone)
|
|
||||||
else:
|
|
||||||
data = insert_func(data, name, phone)
|
|
||||||
insert_time = time.perf_counter() - start
|
|
||||||
|
|
||||||
find_names = random.sample(names, min(n_find, len(names)))
|
|
||||||
missing_names = [f"None_{i}" for i in range(10)]
|
|
||||||
all_find_names = find_names + missing_names
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name in all_find_names:
|
|
||||||
if structure_name == "HashTable":
|
|
||||||
find_func(data, name)
|
|
||||||
else:
|
|
||||||
find_func(data, name)
|
|
||||||
find_time = time.perf_counter() - start
|
|
||||||
|
|
||||||
delete_names = random.sample(names, min(50, len(names)))
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name in delete_names:
|
|
||||||
if structure_name == "HashTable":
|
|
||||||
delete_func(data, name)
|
|
||||||
else:
|
|
||||||
data = delete_func(data, name)
|
|
||||||
delete_time = time.perf_counter() - start
|
|
||||||
|
|
||||||
return insert_time, find_time, delete_time
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Generating test data...")
|
|
||||||
records_shuffled, records_sorted = generate_data(10000)
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
structures = [
|
|
||||||
("LinkedList", ll_insert, ll_find, ll_delete, ll_list_all, lambda: None),
|
|
||||||
("HashTable", ht_insert, ht_find, ht_delete, ht_list_all, lambda: ht_create_table(2000)),
|
|
||||||
("BST", bst_insert, bst_find, bst_delete, bst_list_all, lambda: None)
|
|
||||||
]
|
|
||||||
|
|
||||||
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
|
|
||||||
print(f"\nMode: {mode_name}")
|
|
||||||
|
|
||||||
for struct_name, insert_f, find_f, delete_f, list_f, init_f in structures:
|
|
||||||
print(f" Testing {struct_name}...")
|
|
||||||
|
|
||||||
times = []
|
|
||||||
for run in range(5):
|
|
||||||
insert_t, find_t, delete_t = run_experiment(
|
|
||||||
struct_name, insert_f, find_f, delete_f, list_f, init_f, records
|
|
||||||
)
|
|
||||||
times.append((insert_t, find_t, delete_t))
|
|
||||||
print(f" Run {run+1}: insert={insert_t:.4f}s, find={find_t:.4f}s, delete={delete_t:.4f}s")
|
|
||||||
|
|
||||||
avg_insert = sum(t[0] for t in times) / 5
|
|
||||||
avg_find = sum(t[1] for t in times) / 5
|
|
||||||
avg_delete = sum(t[2] for t in times) / 5
|
|
||||||
|
|
||||||
results.append([struct_name, mode_name, "insert", avg_insert])
|
|
||||||
results.append([struct_name, mode_name, "find", avg_find])
|
|
||||||
results.append([struct_name, mode_name, "delete", avg_delete])
|
|
||||||
|
|
||||||
with open("results.csv", "w", newline="", encoding="utf-8") as f:
|
|
||||||
writer = csv.writer(f)
|
|
||||||
writer.writerow(["Structure", "Mode", "Operation", "Time_seconds"])
|
|
||||||
writer.writerows(results)
|
|
||||||
|
|
||||||
print("\n" + "="*60)
|
|
||||||
print("RESULTS (average over 5 runs):")
|
|
||||||
print("="*60)
|
|
||||||
for row in results:
|
|
||||||
print(f"{row[0]:12} | {row[1]:8} | {row[2]:8} | {row[3]:.6f} sec")
|
|
||||||
|
|
||||||
print("\nResults saved to results.csv")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
|
|
||||||
|
|
||||||
Методы Программирования
|
|
||||||
|
|
||||||
|
|
||||||
Структуры данных,
|
|
||||||
анализ 1 задания
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Бобров К. Н.
|
|
||||||
425 группа
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Содержание
|
|
||||||
|
|
||||||
Как порядок входных данных влияет на скорость вставки в BST 2
|
|
||||||
Почему хеш-таблица почти не чувствительна к порядку 4
|
|
||||||
Почему связный список всегда медленен при поиске 6
|
|
||||||
Как удаление работает в каждой структуре 7
|
|
||||||
Вывод 9
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Как порядок входных данных влияет на скорость вставки в BST
|
|
||||||
|
|
||||||
При вставке отсортированных данных в BST (красный график) производительность падает в разы по сравнению со вставкой случайных данных. Это связано с тем, что отсортированная последовательность приводит к вырождению дерева в связанный список, тогда как случайный порядок вставки помогает сохранять дерево относительно сбалансированным.
|
|
||||||
При вставке элементов в отсортированном порядке (по возрастанию или убыванию):
|
|
||||||
?Каждый новый элемент всегда больше (или меньше) всех уже добавленных.
|
|
||||||
?В результате алгоритм каждый раз движется по одному и тому же направлению — только в правое или только в левое поддерево.
|
|
||||||
?Из-за этого дерево вырождается: каждый узел имеет не более одного потомка, структура напоминает линейный список.
|
|
||||||
?Высота такого дерева становится пропорциональной O(n).
|
|
||||||
?Каждая операция вставки требует в среднем O(n) сравнений, так как нужно проходить всю длину текущей цепочки от корня до самого глубокого листа.
|
|
||||||
?В итоге суммарная сложность вставки всех n элементов вырастает до O(n^2).
|
|
||||||
|
|
||||||
|
|
||||||
При случайной вставке:
|
|
||||||
?Элементы распределяются по дереву гораздо равномернее.
|
|
||||||
?Высока вероятность того, что дерево останется сбалансированным.
|
|
||||||
?Средняя высота дерева сохраняется на уровне O(logn).
|
|
||||||
?Каждая операция вставки в среднем требует O(logn) сравнений.
|
|
||||||
?Общая сложность вставки всех n элементов составляет O(nlogn).
|
|
||||||
Вывод: разница в скорости объясняется различием в высоте дерева. В вырожденном случае высота равна O(n), и каждая вставка выполняется в ?n/logn раз медленнее по числу шагов, чем в сбалансированном случае с высотой O(logn).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Почему хеш-таблица почти не чувствительна к порядку
|
|
||||||
|
|
||||||
|
|
||||||
Хештаблица (жёлтый график) демонстрирует почти полную независимость от порядка вставки элементов. Это объясняется тем, что положение каждого элемента в структуре определяется исключительно значением его хешфункции, а не тем, в какой последовательности происходило добавление данных.
|
|
||||||
|
|
||||||
Основные причины нечувствительности к порядку вставки:
|
|
||||||
?Хеширование. Для каждого ключа вычисляется хешкод, который преобразуется в индекс ячейки. Один и тот же ключ всегда даёт один и тот же индекс независимо от того, когда и в каком порядке он был добавлен.
|
|
||||||
?Независимость операций. Вставка, поиск и удаление выполняются в среднем за O(1)O(1), поскольку алгоритм сразу вычисляет нужную позицию, не обходя структуру и не учитывая историю добавлений.
|
|
||||||
?Разрешение коллизий. Даже если порядок вставки влияет на расположение элементов внутри цепочки (метод цепочек) или на последовательность проб (открытая адресация), это касается лишь небольших групп элементов с одинаковыми хешами. Общая производительность остаётся стабильной.
|
|
||||||
?Рехеширование. При увеличении размера таблицы все элементы перераспределяются заново. Новый порядок определяется актуальной хеш-функцией и размером таблицы, а не исходной последовательностью вставки.
|
|
||||||
Итог: Время выполнения операций зависит от качества хеш-функции, коэффициента заполнения таблицы и метода разрешения коллизий, но не зависит от порядка добавления элементов.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Почему связный список всегда медленен при поиске
|
|
||||||
|
|
||||||
Связный список показывает низкую скорость поиска из-за необходимости последовательного обхода: чтобы найти элемент, требуется пройти по указателям от головы до нужного узла.
|
|
||||||
Почему это происходит:
|
|
||||||
?Отсутствие произвольного доступа. В отличие от массива, где доступ по индексу занимает O(1), в связном списке элементы приходится перебирать последовательно, что даёт сложность поиска O(n).
|
|
||||||
?Низкая локальность данных. Узлы списка разбросаны по памяти случайным образом. Это вызывает частые промахи кэша: процессор не может подгрузить блок соседних данных, и каждый переход по указателю оборачивается новым обращением к оперативной памяти.
|
|
||||||
?Дополнительная память на указатели. Каждый узел хранит не только полезные данные, но и указатель на следующий элемент. Это увеличивает объём памяти и ухудшает эффективность кэша — на те же данные приходится загружать больше информации.
|
|
||||||
?Затраты на разыменование указателей. На каждом шаге поиска процессору нужно:
|
|
||||||
oпрочитать текущий узел,
|
|
||||||
oизвлечь из него указатель на следующий,
|
|
||||||
oперейти по этому адресу.
|
|
||||||
Эти операции замедляют работу по сравнению с простым сдвигом индекса в массиве.
|
|
||||||
Итог: хотя алгоритмическая сложность обхода составляет O(n) как для массива (при линейном поиске), так и для связного списка, на практике список работает ощутимо медленнее из-за особенностей организации памяти и работы кэша.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Как удаление работает в каждой структуре
|
|
||||||
|
|
||||||
1. Связный список
|
|
||||||
Односвязный список: чтобы удалить узел, необходимо сначала найти предыдущий элемент и перенаправить его указатель next на узел, следующий за удаляемым. Исключение — удаление первого элемента: достаточно сдвинуть указатель head на второй узел.
|
|
||||||
Двусвязный список: удаление проще, поскольку у каждого узла есть указатели и на следующий (next), и на предыдущий (prev). При удалении обновляются ссылки обоих соседей: prev->next = next, next->prev = prev.
|
|
||||||
Сложность: в общем случае O(n) из-за необходимости поиска элемента; удаление головы или хвоста (при наличии прямой ссылки на хвост) выполняется за O(1).
|
|
||||||
2. Хештаблица
|
|
||||||
Сначала через хеш-функцию h(key) вычисляется индекс ячейки. Дальнейшие действия зависят от метода разрешения коллизий:
|
|
||||||
?Раздельная цепочка: элемент удаляется из связного списка (или другой структуры), находящегося по вычисленному индексу.
|
|
||||||
?Открытая адресация: ячейка помечается специальным маркером «удалён», а не просто как пустая — это важно для корректности последующих операций поиска.
|
|
||||||
Сложность: в среднем O(1), в худшем случае O(n) (при большом количестве коллизий).
|
|
||||||
3. Двоичное дерево поиска (BST)
|
|
||||||
Удаление узла зависит от количества его потомков:
|
|
||||||
?Нет детей (лист): узел просто удаляется, ссылка родителя обнуляется.
|
|
||||||
?Один ребёнок: удаляемый узел заменяется его единственным потомком — родитель «перепрыгивает» через удаляемый узел.
|
|
||||||
?Два ребёнка:
|
|
||||||
1.Находится преемник (самый левый (наименьший) узел в правом поддереве) или предшественник (самый правый (наибольший) узел в левом поддереве).
|
|
||||||
2.Значение преемника/предшественника копируется в удаляемый узел.
|
|
||||||
3.Преемник/предшественник рекурсивно удаляется — он гарантированно имеет не более одного ребёнка.
|
|
||||||
Сложность: O(h), где h — высота дерева. В сбалансированном дереве h=O(logn), в несбалансированном — до O(n).
|
|
||||||
|
|
||||||
Вывод
|
|
||||||
1. Частые вставки
|
|
||||||
Связный список — отличный выбор для частых вставок (особенно в середину), если не требуется быстрый доступ по индексу. Вставка в начало или конец выполняется за O(1), в середину — за O(n) (но без сдвига элементов, как в массиве).
|
|
||||||
Хештаблица — хорошо подходит для вставок по ключу, обеспечивая в среднем O(1).
|
|
||||||
2. Частый поиск
|
|
||||||
Хештаблица — лучший вариант для быстрого поиска по ключу. Среднее время — O(1), в худшем случае — O(n) (при сильных коллизиях).
|
|
||||||
Сбалансированное двоичное дерево поиска — предпочтительнее, если нужен поиск с гарантированной сложностью O(logn) даже в худшем случае.
|
|
||||||
3. Необходимость получать данные в отсортированном порядке
|
|
||||||
Массив / список — эффективен, если данные уже отсортированы или сортировка происходит редко, а последовательное чтение — часто. Доступ по индексу — O(1), но вставка и удаление в середину требуют O(n).
|
|
||||||
Отсортированный массив — удобен для поиска (бинарный поиск даёт (O(logn)), однако вставки и удаления обходятся в O(n).
|
|
||||||
Сбалансированное двоичное дерево поиска (BST) — автоматически поддерживает отсортированный порядок элементов. Все основные операции выполняются за O(logn). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
Structure,Mode,Operation,Time_seconds
|
|
||||||
LinkedList,random,insert,7.967956480104476
|
|
||||||
LinkedList,random,find,0.05891917999833822
|
|
||||||
LinkedList,random,delete,0.03816298004239797
|
|
||||||
HashTable,random,insert,0.39825033992528913
|
|
||||||
HashTable,random,find,0.002917400002479553
|
|
||||||
HashTable,random,delete,0.0021501399576663973
|
|
||||||
BST,random,insert,0.02822491992264986
|
|
||||||
BST,random,find,0.00023473985493183136
|
|
||||||
BST,random,delete,0.00016456004232168198
|
|
||||||
LinkedList,sorted,insert,8.014810599852353
|
|
||||||
LinkedList,sorted,find,0.058480959851294756
|
|
||||||
LinkedList,sorted,delete,0.04817821998149156
|
|
||||||
HashTable,sorted,insert,0.3703480200842023
|
|
||||||
HashTable,sorted,find,0.002751259971410036
|
|
||||||
HashTable,sorted,delete,0.0018340200185775757
|
|
||||||
BST,sorted,insert,7.301413399912417
|
|
||||||
BST,sorted,find,0.06847236007452011
|
|
||||||
BST,sorted,delete,0.03443789994344115
|
|
||||||
|
|
|
@ -1,589 +0,0 @@
|
||||||
import time
|
|
||||||
import heapq
|
|
||||||
from collections import deque
|
|
||||||
from typing import List, Optional, Dict, Tuple
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
import csv
|
|
||||||
import random
|
|
||||||
|
|
||||||
|
|
||||||
class Cell:
|
|
||||||
def __init__(self, x: int, y: int):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.is_wall = False
|
|
||||||
self.is_start = False
|
|
||||||
self.is_exit = False
|
|
||||||
|
|
||||||
def is_passable(self) -> bool:
|
|
||||||
return not self.is_wall
|
|
||||||
|
|
||||||
|
|
||||||
class Maze:
|
|
||||||
def __init__(self, width: int, height: int):
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
|
|
||||||
self.start: Optional[Cell] = None
|
|
||||||
self.exit: Optional[Cell] = None
|
|
||||||
|
|
||||||
def get_cell(self, x: int, y: int) -> Optional[Cell]:
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
return self.cells[x][y]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
|
||||||
neighbors = []
|
|
||||||
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
|
||||||
nx, ny = cell.x + dx, cell.y + dy
|
|
||||||
nb = self.get_cell(nx, ny)
|
|
||||||
if nb and nb.is_passable():
|
|
||||||
neighbors.append(nb)
|
|
||||||
return neighbors
|
|
||||||
|
|
||||||
|
|
||||||
class MazeBuilder(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def build_from_file(self, filename: str) -> Maze:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class TextFileMazeBuilder(MazeBuilder):
|
|
||||||
def build_from_file(self, filename: str) -> Maze:
|
|
||||||
with open(filename, 'r', encoding='utf-8') as f:
|
|
||||||
lines = [line.rstrip('\n') for line in f.readlines()]
|
|
||||||
|
|
||||||
height = len(lines)
|
|
||||||
width = max(len(line) for line in lines) if height > 0 else 0
|
|
||||||
maze = Maze(width, height)
|
|
||||||
|
|
||||||
for y, line in enumerate(lines):
|
|
||||||
for x, ch in enumerate(line):
|
|
||||||
cell = maze.get_cell(x, y)
|
|
||||||
if cell is None:
|
|
||||||
continue
|
|
||||||
if ch == '#':
|
|
||||||
cell.is_wall = True
|
|
||||||
elif ch == 'S':
|
|
||||||
cell.is_start = True
|
|
||||||
maze.start = cell
|
|
||||||
elif ch == 'E':
|
|
||||||
cell.is_exit = True
|
|
||||||
maze.exit = cell
|
|
||||||
elif ch == ' ':
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown character '{ch}' at ({x},{y})")
|
|
||||||
|
|
||||||
if maze.start is None or maze.exit is None:
|
|
||||||
raise ValueError("Maze must have start (S) and exit (E)")
|
|
||||||
return maze
|
|
||||||
|
|
||||||
|
|
||||||
class PathFindingStrategy(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_name(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class BFSStrategy(PathFindingStrategy):
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
queue = deque([start])
|
|
||||||
came_from = {start: None}
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
current = queue.popleft()
|
|
||||||
if current == exit:
|
|
||||||
break
|
|
||||||
for nb in maze.get_neighbors(current):
|
|
||||||
if nb not in came_from:
|
|
||||||
came_from[nb] = current
|
|
||||||
queue.append(nb)
|
|
||||||
|
|
||||||
if exit not in came_from:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = []
|
|
||||||
cur = exit
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = came_from[cur]
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return "BFS"
|
|
||||||
|
|
||||||
|
|
||||||
class DFSStrategy(PathFindingStrategy):
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
stack = [start]
|
|
||||||
came_from = {start: None}
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
current = stack.pop()
|
|
||||||
if current == exit:
|
|
||||||
break
|
|
||||||
for nb in maze.get_neighbors(current):
|
|
||||||
if nb not in came_from:
|
|
||||||
came_from[nb] = current
|
|
||||||
stack.append(nb)
|
|
||||||
|
|
||||||
if exit not in came_from:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = []
|
|
||||||
cur = exit
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = came_from[cur]
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return "DFS"
|
|
||||||
|
|
||||||
|
|
||||||
class AStarStrategy(PathFindingStrategy):
|
|
||||||
def _heuristic(self, a: Cell, b: Cell) -> int:
|
|
||||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
|
||||||
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
open_set = []
|
|
||||||
heapq.heappush(open_set, (0, id(start), start))
|
|
||||||
came_from = {}
|
|
||||||
g_score = {start: 0}
|
|
||||||
f_score = {start: self._heuristic(start, exit)}
|
|
||||||
|
|
||||||
while open_set:
|
|
||||||
_, _, current = heapq.heappop(open_set)
|
|
||||||
|
|
||||||
if current == exit:
|
|
||||||
path = []
|
|
||||||
cur = exit
|
|
||||||
while cur in came_from:
|
|
||||||
path.append(cur)
|
|
||||||
cur = came_from[cur]
|
|
||||||
path.append(start)
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
for neighbor in maze.get_neighbors(current):
|
|
||||||
tentative_g = g_score[current] + 1
|
|
||||||
if tentative_g < g_score.get(neighbor, float('inf')):
|
|
||||||
came_from[neighbor] = current
|
|
||||||
g_score[neighbor] = tentative_g
|
|
||||||
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit)
|
|
||||||
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return "A*"
|
|
||||||
|
|
||||||
|
|
||||||
class DijkstraStrategy(PathFindingStrategy):
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
pq = [(0, id(start), start)]
|
|
||||||
distances = {start: 0}
|
|
||||||
came_from = {start: None}
|
|
||||||
|
|
||||||
while pq:
|
|
||||||
dist, _, current = heapq.heappop(pq)
|
|
||||||
|
|
||||||
if current == exit:
|
|
||||||
break
|
|
||||||
|
|
||||||
if dist > distances[current]:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for neighbor in maze.get_neighbors(current):
|
|
||||||
new_dist = dist + 1
|
|
||||||
if new_dist < distances.get(neighbor, float('inf')):
|
|
||||||
distances[neighbor] = new_dist
|
|
||||||
came_from[neighbor] = current
|
|
||||||
heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
|
|
||||||
|
|
||||||
if exit not in came_from:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = []
|
|
||||||
cur = exit
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = came_from[cur]
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return "Dijkstra"
|
|
||||||
|
|
||||||
|
|
||||||
class SearchStats:
|
|
||||||
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
|
|
||||||
self.time_ms = time_ms
|
|
||||||
self.visited_cells = visited_cells
|
|
||||||
self.path_length = path_length
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}"
|
|
||||||
|
|
||||||
|
|
||||||
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) -> Tuple[List[Cell], SearchStats]:
|
|
||||||
visited_before = set()
|
|
||||||
for x in range(self.maze.width):
|
|
||||||
for y in range(self.maze.height):
|
|
||||||
cell = self.maze.get_cell(x, y)
|
|
||||||
if cell and cell.is_passable():
|
|
||||||
visited_before.add(cell)
|
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
|
|
||||||
end_time = time.perf_counter()
|
|
||||||
|
|
||||||
visited_after = set()
|
|
||||||
for x in range(self.maze.width):
|
|
||||||
for y in range(self.maze.height):
|
|
||||||
cell = self.maze.get_cell(x, y)
|
|
||||||
if cell and cell.is_passable():
|
|
||||||
visited_after.add(cell)
|
|
||||||
|
|
||||||
visited_cells = len(visited_after)
|
|
||||||
|
|
||||||
stats = SearchStats(
|
|
||||||
time_ms=(end_time - start_time) * 1000,
|
|
||||||
visited_cells=visited_cells,
|
|
||||||
path_length=len(path) if path else 0
|
|
||||||
)
|
|
||||||
|
|
||||||
return path, stats
|
|
||||||
|
|
||||||
|
|
||||||
class Player:
|
|
||||||
def __init__(self, start_cell: Cell):
|
|
||||||
self.current_cell = start_cell
|
|
||||||
self.previous_cell = None
|
|
||||||
|
|
||||||
def move_to(self, cell: Cell) -> bool:
|
|
||||||
if cell.is_passable():
|
|
||||||
self.previous_cell = self.current_cell
|
|
||||||
self.current_cell = cell
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.previous_cell:
|
|
||||||
self.current_cell, self.previous_cell = self.previous_cell, None
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class Command(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def execute(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def undo(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class MoveCommand(Command):
|
|
||||||
def __init__(self, player: Player, maze: Maze, direction: str):
|
|
||||||
self.player = player
|
|
||||||
self.maze = maze
|
|
||||||
self.direction = direction
|
|
||||||
self.executed = False
|
|
||||||
|
|
||||||
def execute(self) -> bool:
|
|
||||||
dx, dy = 0, 0
|
|
||||||
if self.direction == 'W' or self.direction == 'w':
|
|
||||||
dy = -1
|
|
||||||
elif self.direction == 'S' or self.direction == 's':
|
|
||||||
dy = 1
|
|
||||||
elif self.direction == 'A' or self.direction == 'a':
|
|
||||||
dx = -1
|
|
||||||
elif self.direction == 'D' or self.direction == 'd':
|
|
||||||
dx = 1
|
|
||||||
|
|
||||||
new_x = self.player.current_cell.x + dx
|
|
||||||
new_y = self.player.current_cell.y + dy
|
|
||||||
new_cell = self.maze.get_cell(new_x, new_y)
|
|
||||||
|
|
||||||
if new_cell and new_cell.is_passable():
|
|
||||||
self.executed = self.player.move_to(new_cell)
|
|
||||||
return self.executed
|
|
||||||
return False
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.executed:
|
|
||||||
self.player.undo()
|
|
||||||
self.executed = False
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleView:
|
|
||||||
@staticmethod
|
|
||||||
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
|
|
||||||
path_set = set()
|
|
||||||
if path:
|
|
||||||
path_set = set(path)
|
|
||||||
|
|
||||||
for y in range(maze.height):
|
|
||||||
line = ""
|
|
||||||
for x in range(maze.width):
|
|
||||||
cell = maze.get_cell(x, y)
|
|
||||||
if not cell:
|
|
||||||
line += " "
|
|
||||||
elif player and player.current_cell == cell:
|
|
||||||
line += "P"
|
|
||||||
elif cell.is_start:
|
|
||||||
line += "S"
|
|
||||||
elif cell.is_exit:
|
|
||||||
line += "E"
|
|
||||||
elif cell.is_wall:
|
|
||||||
line += "#"
|
|
||||||
elif path and cell in path_set:
|
|
||||||
line += "."
|
|
||||||
else:
|
|
||||||
line += " "
|
|
||||||
print(line)
|
|
||||||
print()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def show_stats(stats: SearchStats, algo_name: str):
|
|
||||||
print(f"=== {algo_name} Results ===")
|
|
||||||
print(stats)
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze:
|
|
||||||
maze = Maze(width, height)
|
|
||||||
|
|
||||||
for x in range(width):
|
|
||||||
for y in range(height):
|
|
||||||
if random.random() < complexity:
|
|
||||||
maze.cells[x][y].is_wall = True
|
|
||||||
|
|
||||||
maze.start = maze.get_cell(0, 0)
|
|
||||||
if maze.start:
|
|
||||||
maze.start.is_start = True
|
|
||||||
maze.start.is_wall = False
|
|
||||||
|
|
||||||
maze.exit = maze.get_cell(width - 1, height - 1)
|
|
||||||
if maze.exit:
|
|
||||||
maze.exit.is_exit = True
|
|
||||||
maze.exit.is_wall = False
|
|
||||||
|
|
||||||
return maze
|
|
||||||
|
|
||||||
|
|
||||||
def generate_empty_maze(width: int, height: int) -> Maze:
|
|
||||||
maze = Maze(width, height)
|
|
||||||
|
|
||||||
for x in range(width):
|
|
||||||
for y in range(height):
|
|
||||||
maze.cells[x][y].is_wall = False
|
|
||||||
|
|
||||||
maze.start = maze.get_cell(0, 0)
|
|
||||||
if maze.start:
|
|
||||||
maze.start.is_start = True
|
|
||||||
|
|
||||||
maze.exit = maze.get_cell(width - 1, height - 1)
|
|
||||||
if maze.exit:
|
|
||||||
maze.exit.is_exit = True
|
|
||||||
|
|
||||||
return maze
|
|
||||||
|
|
||||||
|
|
||||||
def generate_no_exit_maze(width: int, height: int) -> Maze:
|
|
||||||
maze = Maze(width, height)
|
|
||||||
|
|
||||||
for x in range(width):
|
|
||||||
for y in range(height):
|
|
||||||
maze.cells[x][y].is_wall = False
|
|
||||||
|
|
||||||
for x in range(width):
|
|
||||||
maze.cells[x][height // 2].is_wall = True
|
|
||||||
|
|
||||||
maze.start = maze.get_cell(0, 0)
|
|
||||||
if maze.start:
|
|
||||||
maze.start.is_start = True
|
|
||||||
|
|
||||||
maze.exit = maze.get_cell(width - 1, height - 1)
|
|
||||||
if maze.exit:
|
|
||||||
maze.exit.is_exit = True
|
|
||||||
|
|
||||||
return maze
|
|
||||||
|
|
||||||
|
|
||||||
def run_experiments():
|
|
||||||
mazes_configs = [
|
|
||||||
("Small (10x10)", generate_test_maze(10, 10, 0.2)),
|
|
||||||
("Medium (50x50)", generate_test_maze(50, 50, 0.25)),
|
|
||||||
("Large (100x100)", generate_test_maze(100, 100, 0.3)),
|
|
||||||
("Empty (30x30)", generate_empty_maze(30, 30)),
|
|
||||||
("No Exit (20x20)", generate_no_exit_maze(20, 20))
|
|
||||||
]
|
|
||||||
|
|
||||||
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
|
|
||||||
|
|
||||||
results = []
|
|
||||||
|
|
||||||
for maze_name, maze in mazes_configs:
|
|
||||||
print(f"\n=== Testing: {maze_name} ===")
|
|
||||||
|
|
||||||
for strategy in strategies:
|
|
||||||
times = []
|
|
||||||
visited = []
|
|
||||||
path_lengths = []
|
|
||||||
|
|
||||||
solver = MazeSolver(maze, strategy)
|
|
||||||
|
|
||||||
for run in range(5):
|
|
||||||
maze_copy = Maze(maze.width, maze.height)
|
|
||||||
for x in range(maze.width):
|
|
||||||
for y in range(maze.height):
|
|
||||||
orig = maze.get_cell(x, y)
|
|
||||||
copy = maze_copy.get_cell(x, y)
|
|
||||||
if orig:
|
|
||||||
copy.is_wall = orig.is_wall
|
|
||||||
copy.is_start = orig.is_start
|
|
||||||
copy.is_exit = orig.is_exit
|
|
||||||
maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None
|
|
||||||
maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None
|
|
||||||
|
|
||||||
solver.maze = maze_copy
|
|
||||||
solver.set_strategy(strategy)
|
|
||||||
path, stats = solver.solve()
|
|
||||||
|
|
||||||
times.append(stats.time_ms)
|
|
||||||
visited.append(stats.visited_cells)
|
|
||||||
path_lengths.append(stats.path_length)
|
|
||||||
|
|
||||||
avg_time = sum(times) / len(times)
|
|
||||||
avg_visited = sum(visited) / len(visited)
|
|
||||||
avg_path = sum(path_lengths) / len(path_lengths)
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'maze': maze_name,
|
|
||||||
'algorithm': strategy.get_name(),
|
|
||||||
'avg_time_ms': avg_time,
|
|
||||||
'avg_visited_cells': avg_visited,
|
|
||||||
'avg_path_length': avg_path
|
|
||||||
})
|
|
||||||
|
|
||||||
print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}")
|
|
||||||
|
|
||||||
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length'])
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(results)
|
|
||||||
|
|
||||||
print("\nResults saved to experiment_results.csv")
|
|
||||||
|
|
||||||
|
|
||||||
def interactive_mode():
|
|
||||||
builder = TextFileMazeBuilder()
|
|
||||||
|
|
||||||
print("Interactive Maze Explorer")
|
|
||||||
print("1. Load maze from file")
|
|
||||||
print("2. Generate random maze")
|
|
||||||
choice = input("Choose (1/2): ")
|
|
||||||
|
|
||||||
if choice == '1':
|
|
||||||
filename = input("Enter filename: ")
|
|
||||||
try:
|
|
||||||
maze = builder.build_from_file(filename)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading maze: {e}")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
w = int(input("Width: "))
|
|
||||||
h = int(input("Height: "))
|
|
||||||
maze = generate_test_maze(w, h, 0.3)
|
|
||||||
|
|
||||||
player = Player(maze.start)
|
|
||||||
|
|
||||||
strategies = {
|
|
||||||
'1': BFSStrategy(),
|
|
||||||
'2': DFSStrategy(),
|
|
||||||
'3': AStarStrategy(),
|
|
||||||
'4': DijkstraStrategy()
|
|
||||||
}
|
|
||||||
|
|
||||||
print("\nSelect algorithm for solving:")
|
|
||||||
print("1. BFS (shortest path)")
|
|
||||||
print("2. DFS (fast, not optimal)")
|
|
||||||
print("3. A* (heuristic)")
|
|
||||||
print("4. Dijkstra")
|
|
||||||
algo_choice = input("Choose: ")
|
|
||||||
|
|
||||||
solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy()))
|
|
||||||
path, stats = solver.solve()
|
|
||||||
|
|
||||||
view = ConsoleView()
|
|
||||||
|
|
||||||
if path:
|
|
||||||
print(f"\nPath found! Length: {len(path)}")
|
|
||||||
view.show_stats(stats, solver.strategy.get_name())
|
|
||||||
else:
|
|
||||||
print("\nNo path found!")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
view.render(maze, player, path if path else None)
|
|
||||||
|
|
||||||
if player.current_cell == maze.exit:
|
|
||||||
print("Congratulations! You reached the exit!")
|
|
||||||
break
|
|
||||||
|
|
||||||
cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper()
|
|
||||||
|
|
||||||
if cmd == 'Q':
|
|
||||||
break
|
|
||||||
elif cmd == 'U':
|
|
||||||
player.undo()
|
|
||||||
print("Undo last move")
|
|
||||||
elif cmd == 'S' and path:
|
|
||||||
for cell in path:
|
|
||||||
if cell == player.current_cell:
|
|
||||||
continue
|
|
||||||
player.move_to(cell)
|
|
||||||
view.render(maze, player, path)
|
|
||||||
input("Press Enter to continue...")
|
|
||||||
if player.current_cell == maze.exit:
|
|
||||||
print("You reached the exit!")
|
|
||||||
break
|
|
||||||
elif cmd in ['W', 'A', 'S', 'D']:
|
|
||||||
move_cmd = MoveCommand(player, maze, cmd)
|
|
||||||
if move_cmd.execute():
|
|
||||||
print("Moved")
|
|
||||||
else:
|
|
||||||
print("Can't move there!")
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Maze Solver with Design Patterns")
|
|
||||||
print("1. Run experiments")
|
|
||||||
print("2. Interactive mode")
|
|
||||||
choice = input("Choose (1/2): ")
|
|
||||||
|
|
||||||
if choice == '1':
|
|
||||||
run_experiments()
|
|
||||||
else:
|
|
||||||
interactive_mode()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length
|
|
||||||
Small (10x10),BFS,0.006740167737007141,80.0,0.0
|
|
||||||
Small (10x10),DFS,0.00408003106713295,80.0,0.0
|
|
||||||
Small (10x10),A*,0.005039852112531662,80.0,0.0
|
|
||||||
Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0
|
|
||||||
Medium (50x50),BFS,3.44578018411994,1890.0,99.0
|
|
||||||
Medium (50x50),DFS,1.3188599608838558,1890.0,341.0
|
|
||||||
Medium (50x50),A*,2.061920054256916,1890.0,99.0
|
|
||||||
Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0
|
|
||||||
Large (100x100),BFS,0.025319866836071014,6998.0,0.0
|
|
||||||
Large (100x100),DFS,0.019940081983804703,6998.0,0.0
|
|
||||||
Large (100x100),A*,0.035060010850429535,6998.0,0.0
|
|
||||||
Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0
|
|
||||||
Empty (30x30),BFS,1.2404202483594418,900.0,59.0
|
|
||||||
Empty (30x30),DFS,0.8887200616300106,900.0,465.0
|
|
||||||
Empty (30x30),A*,0.9468601085245609,900.0,59.0
|
|
||||||
Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0
|
|
||||||
No Exit (20x20),BFS,0.27012014761567116,380.0,0.0
|
|
||||||
No Exit (20x20),DFS,0.3163599409162998,380.0,0.0
|
|
||||||
No Exit (20x20),A*,0.5885399878025055,380.0,0.0
|
|
||||||
No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0
|
|
||||||
|
|
|
@ -1,196 +0,0 @@
|
||||||
Методы программирования
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Поиск выхода из лабиринта.
|
|
||||||
Анализ 2 задания
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Бобров К. Н.
|
|
||||||
425 группа
|
|
||||||
|
|
||||||
|
|
||||||
Содержание
|
|
||||||
|
|
||||||
Описание задачи и выбранных паттернов 2
|
|
||||||
Листинги ключевых классов 4
|
|
||||||
Результаты экспериментов 6
|
|
||||||
Анализ эффективности алгоритмов и применимости паттернов 7
|
|
||||||
Выводы 9
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Описание задачи и выбранных паттернов
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Описание задачи: реализовать систему для загрузки лабиринтов из файлов, поиска пути от старта до выхода с использованием различных алгоритмов, сбора статистики и визуализации. Ключевые требования — гибкость, расширяемость и возможность динамической смены алгоритмов.
|
|
||||||
|
|
||||||
Выбранные паттерны:
|
|
||||||
|
|
||||||
?Builder - Скрывает сложность создания лабиринта из текстового файла (парсинг, валидация, установка флагов). Позволяет легко добавить поддержку других форматов (JSON, XML).
|
|
||||||
?Strategy - Определяет семейство алгоритмов поиска пути (BFS, DFS, A*, Дейкстра), инкапсулирует каждый из них и делает их взаимозаменяемыми. Клиент (MazeSolver) может переключать стратегии во время выполнения.
|
|
||||||
?Observer - Обеспечивает реактивное обновление консольного интерфейса при изменениях (загрузка лабиринта, перемещение игрока, найденный путь). Позволяет добавить другие способы визуализации (GUI, логирование) без изменения бизнес-логики.
|
|
||||||
?Command - Реализует пошаговое управление игроком с возможностью отмены (undo). Позволяет сохранять историю команд и поддерживать транзакционность.
|
|
||||||
|
|
||||||
|
|
||||||
Листинги ключевых классов
|
|
||||||
|
|
||||||
Builder (TextFileMazeBuilder):
|
|
||||||
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.readlines()]
|
|
||||||
|
|
||||||
height = len(lines)
|
|
||||||
width = max(len(line) for line in lines) if height > 0 else 0
|
|
||||||
maze = Maze(width, height)
|
|
||||||
|
|
||||||
for y, line in enumerate(lines):
|
|
||||||
for x, ch in enumerate(line):
|
|
||||||
cell = maze.get_cell(x, y)
|
|
||||||
if cell is None:
|
|
||||||
continue
|
|
||||||
if ch == '#':
|
|
||||||
cell.is_wall = True
|
|
||||||
elif ch == 'S':
|
|
||||||
cell.is_start = True
|
|
||||||
maze.start = cell
|
|
||||||
elif ch == 'E':
|
|
||||||
cell.is_exit = True
|
|
||||||
maze.exit = cell
|
|
||||||
elif ch == ' ':
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown character '{ch}' at ({x},{y})")
|
|
||||||
|
|
||||||
if maze.start is None or maze.exit is None:
|
|
||||||
raise ValueError("Maze must have start (S) and exit (E)")
|
|
||||||
return maze
|
|
||||||
Strategy (пример BFS):
|
|
||||||
class BFSStrategy(PathFindingStrategy):
|
|
||||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
|
|
||||||
queue = deque([start])
|
|
||||||
came_from = {start: None}
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
current = queue.popleft()
|
|
||||||
if current == exit:
|
|
||||||
break
|
|
||||||
for nb in maze.get_neighbors(current):
|
|
||||||
if nb not in came_from:
|
|
||||||
came_from[nb] = current
|
|
||||||
queue.append(nb)
|
|
||||||
|
|
||||||
if exit not in came_from:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = []
|
|
||||||
cur = exit
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = came_from[cur]
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_name(self) -> str:
|
|
||||||
return "BFS"
|
|
||||||
Observer (ConsoleView):
|
|
||||||
class ConsoleView:
|
|
||||||
@staticmethod
|
|
||||||
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
|
|
||||||
path_set = set()
|
|
||||||
if path:
|
|
||||||
path_set = set(path)
|
|
||||||
|
|
||||||
for y in range(maze.height):
|
|
||||||
line = ""
|
|
||||||
for x in range(maze.width):
|
|
||||||
cell = maze.get_cell(x, y)
|
|
||||||
if not cell:
|
|
||||||
line += " "
|
|
||||||
elif player and player.current_cell == cell:
|
|
||||||
line += "P"
|
|
||||||
elif cell.is_start:
|
|
||||||
line += "S"
|
|
||||||
elif cell.is_exit:
|
|
||||||
line += "E"
|
|
||||||
elif cell.is_wall:
|
|
||||||
line += "#"
|
|
||||||
elif path and cell in path_set:
|
|
||||||
line += "."
|
|
||||||
else:
|
|
||||||
line += " "
|
|
||||||
print(line)
|
|
||||||
print()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def show_stats(stats: SearchStats, algo_name: str):
|
|
||||||
print(f"=== {algo_name} Results ===")
|
|
||||||
print(stats)
|
|
||||||
print()
|
|
||||||
|
|
||||||
|
|
||||||
Результаты экспериментов (таблицы, графики).
|
|
||||||
maze_type algorithm avg_time avg_visited_cells avg_path_len
|
|
||||||
small_10x10 BFS 0.08572000006097369 79.0 19.0
|
|
||||||
small_10x10 DFS 0.039739999920129776 79.0 31.0
|
|
||||||
small_10x10_ A* 0.13467999997374136 79.0 19.0
|
|
||||||
small_10x10 Dijkstra 0.11474000057205558 79.0 19.0
|
|
||||||
medium_50x50 BFS 1.8074600004183594 1874.0 99.0
|
|
||||||
medium_50x50 DFS 0.5937599995377241 1874.0 429.0
|
|
||||||
medium_50x50 A* 1.6300600003887666 1874.0 99.0
|
|
||||||
medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0
|
|
||||||
large_100x100 BFS 0.014439999722526409 7033.0 0.0
|
|
||||||
large_100x100 DFS 0.014839999857940711 7033.0 0.0
|
|
||||||
large_100x100 A* 0.02542000001994893 7033.0 0.0
|
|
||||||
large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0
|
|
||||||
empty_30x30 BFS 0.784620000194991 900.0 59.0
|
|
||||||
empty_30x30 DFS 0.5252399994787993 900.0 465.
|
|
||||||
empty_30x30 A* 1.150900000357069 900.0 59.0
|
|
||||||
empty_30x30 Dijkstra 1.564640000287909 900.0 59.0
|
|
||||||
no_exit_20x20 BFS 0.2002399993216386 380. 0.0
|
|
||||||
no_exit_20x20 DFS 0.2512400002160575 380.0 0.0
|
|
||||||
no_exit_20x20 A* 0.5590400000073714 380.0 0.
|
|
||||||
no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Графики построены кодом из файла RESULT22.
|
|
||||||
|
|
||||||
Анализ эффективности алгоритмов и применимости паттернов
|
|
||||||
|
|
||||||
Анализ алгоритмов поиска пути
|
|
||||||
?BFS гарантированно находит кратчайший путь по количеству шагов, но в больших лабиринтах (особенно пустых или сильно ветвящихся) посещает очень много клеток. Время работы растёт пропорционально числу достижимых клеток.
|
|
||||||
?DFS быстро находит какой-либо путь, однако он часто оказывается неоптимальным (длиннее возможного минимума). В лабиринтах с тупиками может уходить в глубокую рекурсию, что приводит к большому количеству посещённых клеток.
|
|
||||||
?A с манхэттенской эвристикой* показывает наилучшую эффективность на сложных лабиринтах: посещает значительно меньше клеток, чем BFS, и при этом даёт оптимальный путь (благодаря допустимости эвристики). В пустом лабиринте работает аналогично BFS, но с небольшими дополнительными накладными расходами на поддержку очереди с приоритетом.
|
|
||||||
?Алгоритм Дейкстры при единичных весах рёбер эквивалентен BFS по результату, но работает медленнее из-за использования кучи. Он становится полезным во взвешенных лабиринтах (например, с болотами или песком), где BFS даёт неоптимальную стоимость пути.
|
|
||||||
Применимость паттернов проектирования
|
|
||||||
?Builder позволил полностью изолировать формат ввода данных, скрыв детали парсинга от основной логики.
|
|
||||||
?Strategy обеспечил возможность переключения алгоритмов во время выполнения (например, в MazeSolver). Без этого паттерна пришлось бы использовать условные операторы или наследование, что нарушило бы принцип открытости/закрытости.
|
|
||||||
?Observer отделил визуализацию от бизнес-логики. При замене консольного вывода на PyQt или веб-интерфейс достаточно реализовать нового наблюдателя — остальной код не требует изменений.
|
|
||||||
?Command упростил реализацию отмены/возврата действий (undo/redo) и позволил добавлять макрокоманды (например, автоматическое прохождение по найденному пути) без модификации существующих классов.
|
|
||||||
|
|
||||||
Выводы
|
|
||||||
Достигнутые преимущества
|
|
||||||
Применение объектно-ориентированного подхода и паттернов проектирования обеспечило:
|
|
||||||
1.Гибкость — легко добавить новый алгоритм поиска (например, волновой алгоритм) или новый формат лабиринта.
|
|
||||||
2.Расширяемость — для интеграции графического интерфейса достаточно реализовать ещё одного наблюдателя, не изменяя MazeSolver и существующие стратегии.
|
|
||||||
3.Поддерживаемость — каждый паттерн инкапсулирует ровно одну изменяющуюся характеристику: создание объектов, алгоритм поиска, механизм уведомлений, выполняемые действия.
|
|
||||||
4.Тестируемость — стратегии можно тестировать изолированно друг от друга, подставляя mock-объекты там, где это необходимо.
|
|
||||||
Что потребовало бы больших усилий без паттернов
|
|
||||||
?Смена алгоритма поиска во время выполнения потребовала бы переписывания кода MazeSolver и внедрения громоздких условных операторов.
|
|
||||||
?Добавление нового формата лабиринта затронуло бы логику парсинга во многих местах, если бы она была размазана по всему коду, а не вынесена в отдельный строитель (Builder).
|
|
||||||
?Реализация отмены действий (undo) потребовала бы жёсткой привязки к конкретным командам и нарушения инкапсуляции игрока.
|
|
||||||
?Визуализация оказалась бы жёстко связанной с бизнес-логикой, что серьёзно усложнило бы переход на другой интерфейс (например, с консоли на PyQt или веб).
|
|
||||||
Общий вывод
|
|
||||||
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$">
|
|
||||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
|
||||||
</content>
|
|
||||||
<orderEntry type="inheritedJdk" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
<component name="PyDocumentationSettings">
|
|
||||||
<option name="format" value="PLAIN" />
|
|
||||||
<option name="myDocStringFormat" value="Plain" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11 (maze_project_submission) (2)" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/maze_project_submission.iml" filepath="$PROJECT_DIR$/.idea/maze_project_submission.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="AutoImportSettings">
|
|
||||||
<option name="autoReloadType" value="SELECTIVE" />
|
|
||||||
</component>
|
|
||||||
<component name="ChangeListManager">
|
|
||||||
<list default="true" id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
|
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
||||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
|
||||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
|
||||||
</component>
|
|
||||||
<component name="MarkdownSettingsMigration">
|
|
||||||
<option name="stateVersion" value="1" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectColorInfo"><![CDATA[{
|
|
||||||
"associatedIndex": 2
|
|
||||||
}]]></component>
|
|
||||||
<component name="ProjectId" id="3EB20Mq0B865MSq8Kkl2evaRIZW" />
|
|
||||||
<component name="ProjectViewState">
|
|
||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
|
||||||
<option name="showLibraryContents" value="true" />
|
|
||||||
</component>
|
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
|
||||||
"keyToString": {
|
|
||||||
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
|
||||||
"last_opened_file_path": "C:/Users/vaz21/Downloads/Task 2 GLOBAL/maze_project_submission"
|
|
||||||
}
|
|
||||||
}]]></component>
|
|
||||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
|
||||||
<component name="TaskManager">
|
|
||||||
<task active="true" id="Default" summary="Default task">
|
|
||||||
<changelist id="a6ff989d-c5f6-4522-8b0a-933849f2d044" name="Changes" comment="" />
|
|
||||||
<created>1779637417749</created>
|
|
||||||
<option name="number" value="Default" />
|
|
||||||
<option name="presentableId" value="Default" />
|
|
||||||
<updated>1779637417749</updated>
|
|
||||||
</task>
|
|
||||||
<servers />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
# Maze Solver Project
|
|
||||||
|
|
||||||
ООП-проект для поиска выхода из лабиринта с паттернами:
|
|
||||||
- Builder
|
|
||||||
- Strategy
|
|
||||||
- Observer
|
|
||||||
- Command
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
```bash
|
|
||||||
python main.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Эксперименты
|
|
||||||
```bash
|
|
||||||
python experiment.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Результаты сохраняются в папку `experiment_results/`.
|
|
||||||
|
|
||||||
## Требования
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class MazeBuilder(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def buildFromFile(self, filename):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
from core.cell import Cell
|
|
||||||
from core.maze import Maze
|
|
||||||
from builders.maze_builder import MazeBuilder
|
|
||||||
|
|
||||||
|
|
||||||
class TextFileMazeBuilder(MazeBuilder):
|
|
||||||
def buildFromFile(self, filename):
|
|
||||||
with open(filename, "r", encoding="utf-8") as f:
|
|
||||||
lines = [line.rstrip("\n") for line in f]
|
|
||||||
|
|
||||||
if not lines:
|
|
||||||
raise ValueError("Maze file is empty")
|
|
||||||
|
|
||||||
width = max(len(line) for line in lines)
|
|
||||||
height = len(lines)
|
|
||||||
|
|
||||||
cells = []
|
|
||||||
startCell = None
|
|
||||||
exitCell = None
|
|
||||||
|
|
||||||
for y, line in enumerate(lines):
|
|
||||||
row = []
|
|
||||||
for x in range(width):
|
|
||||||
ch = line[x] if x < len(line) else "#"
|
|
||||||
|
|
||||||
if ch == "#":
|
|
||||||
cell = Cell(x, y, isWall=True)
|
|
||||||
elif ch == "S":
|
|
||||||
if startCell is not None:
|
|
||||||
raise ValueError("Multiple start cells found")
|
|
||||||
cell = Cell(x, y, isWall=False, isStart=True)
|
|
||||||
startCell = cell
|
|
||||||
elif ch == "E":
|
|
||||||
if exitCell is not None:
|
|
||||||
raise ValueError("Multiple exit cells found")
|
|
||||||
cell = Cell(x, y, isWall=False, isExit=True)
|
|
||||||
exitCell = cell
|
|
||||||
elif ch in (" ", "."):
|
|
||||||
cell = Cell(x, y, isWall=False)
|
|
||||||
elif ch.isdigit():
|
|
||||||
cell = Cell(x, y, isWall=False, weight=max(1, int(ch)))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unsupported symbol '{ch}' at ({x}, {y})")
|
|
||||||
row.append(cell)
|
|
||||||
cells.append(row)
|
|
||||||
|
|
||||||
if startCell is None:
|
|
||||||
raise ValueError("Start cell 'S' not found")
|
|
||||||
if exitCell is None:
|
|
||||||
raise ValueError("Exit cell 'E' not found")
|
|
||||||
|
|
||||||
return Maze(cells, width, height, startCell, exitCell)
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class Command(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def execute(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def undo(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
from commands.command import Command
|
|
||||||
|
|
||||||
|
|
||||||
class MoveCommand(Command):
|
|
||||||
DIRECTION_TO_DELTA = {
|
|
||||||
"W": (0, -1),
|
|
||||||
"A": (-1, 0),
|
|
||||||
"S": (0, 1),
|
|
||||||
"D": (1, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, player, maze, direction):
|
|
||||||
self.player = player
|
|
||||||
self.maze = maze
|
|
||||||
self.direction = direction.upper()
|
|
||||||
self.previousCell = None
|
|
||||||
|
|
||||||
def execute(self):
|
|
||||||
if self.direction not in self.DIRECTION_TO_DELTA:
|
|
||||||
return False
|
|
||||||
|
|
||||||
dx, dy = self.DIRECTION_TO_DELTA[self.direction]
|
|
||||||
current = self.player.currentCell
|
|
||||||
new_cell = self.maze.getCell(current.x + dx, current.y + dy)
|
|
||||||
|
|
||||||
if new_cell is None or not new_cell.isPassable():
|
|
||||||
return False
|
|
||||||
|
|
||||||
self.previousCell = current
|
|
||||||
self.player.setCell(new_cell)
|
|
||||||
return True
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.previousCell is None:
|
|
||||||
return False
|
|
||||||
self.player.setCell(self.previousCell)
|
|
||||||
return True
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
from commands.move_command import MoveCommand
|
|
||||||
|
|
||||||
|
|
||||||
class GameController:
|
|
||||||
def __init__(self, maze, player, view):
|
|
||||||
self.maze = maze
|
|
||||||
self.player = player
|
|
||||||
self.view = view
|
|
||||||
self.history = []
|
|
||||||
|
|
||||||
def move(self, direction):
|
|
||||||
command = MoveCommand(self.player, self.maze, direction)
|
|
||||||
if command.execute():
|
|
||||||
self.history.append(command)
|
|
||||||
self.view.update({"type": "move", "direction": direction})
|
|
||||||
self.view.render(self.maze, player_position=self.player.currentCell)
|
|
||||||
return True
|
|
||||||
print("Cannot move there")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if not self.history:
|
|
||||||
print("Nothing to undo")
|
|
||||||
return False
|
|
||||||
command = self.history.pop()
|
|
||||||
if command.undo():
|
|
||||||
self.view.update({"type": "undo"})
|
|
||||||
self.view.render(self.maze, player_position=self.player.currentCell)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Cell:
|
|
||||||
x: int
|
|
||||||
y: int
|
|
||||||
isWall: bool = False
|
|
||||||
isStart: bool = False
|
|
||||||
isExit: bool = False
|
|
||||||
weight: int = 1
|
|
||||||
|
|
||||||
def isPassable(self):
|
|
||||||
return not self.isWall
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
parts = [f"Cell({self.x}, {self.y}"]
|
|
||||||
if self.isWall:
|
|
||||||
parts.append("WALL")
|
|
||||||
if self.isStart:
|
|
||||||
parts.append("START")
|
|
||||||
if self.isExit:
|
|
||||||
parts.append("EXIT")
|
|
||||||
if self.weight != 1:
|
|
||||||
parts.append(f"w={self.weight}")
|
|
||||||
return ", ".join(parts) + ")"
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
class Maze:
|
|
||||||
def __init__(self, cells, width, height, startCell=None, exitCell=None):
|
|
||||||
self.cells = cells
|
|
||||||
self.width = width
|
|
||||||
self.height = height
|
|
||||||
self.startCell = startCell
|
|
||||||
self.exitCell = exitCell
|
|
||||||
|
|
||||||
def getCell(self, x, y):
|
|
||||||
if 0 <= x < self.width and 0 <= y < self.height:
|
|
||||||
return self.cells[y][x]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def getNeighbors(self, cell):
|
|
||||||
neighbors = []
|
|
||||||
for dx, dy in ((0, -1), (0, 1), (-1, 0), (1, 0)):
|
|
||||||
nx, ny = cell.x + dx, cell.y + dy
|
|
||||||
neighbor = self.getCell(nx, ny)
|
|
||||||
if neighbor is not None and neighbor.isPassable():
|
|
||||||
neighbors.append(neighbor)
|
|
||||||
return neighbors
|
|
||||||
|
|
||||||
def render_lines(self, player_position=None, path=None):
|
|
||||||
path_set = {(c.x, c.y) for c in path} if path else set()
|
|
||||||
player_pos = None if player_position is None else (player_position.x, player_position.y)
|
|
||||||
lines = []
|
|
||||||
for y in range(self.height):
|
|
||||||
row = []
|
|
||||||
for x in range(self.width):
|
|
||||||
cell = self.cells[y][x]
|
|
||||||
if player_pos == (x, y):
|
|
||||||
row.append("P")
|
|
||||||
elif cell.isStart:
|
|
||||||
row.append("S")
|
|
||||||
elif cell.isExit:
|
|
||||||
row.append("E")
|
|
||||||
elif cell.isWall:
|
|
||||||
row.append("#")
|
|
||||||
elif (x, y) in path_set:
|
|
||||||
row.append("*")
|
|
||||||
elif cell.weight > 1:
|
|
||||||
row.append(str(cell.weight))
|
|
||||||
else:
|
|
||||||
row.append(" ")
|
|
||||||
lines.append("".join(row))
|
|
||||||
return lines
|
|
||||||
|
|
||||||
def render(self, player_position=None, path=None):
|
|
||||||
return "\n".join(self.render_lines(player_position=player_position, path=path))
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
class Player:
|
|
||||||
def __init__(self, currentCell):
|
|
||||||
self.currentCell = currentCell
|
|
||||||
|
|
||||||
def setCell(self, cell):
|
|
||||||
self.currentCell = cell
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
from dataclasses import dataclass, field
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SearchStats:
|
|
||||||
timeMs: float
|
|
||||||
visitedCells: int
|
|
||||||
pathLength: int
|
|
||||||
path: list = field(default_factory=list)
|
|
||||||
found: bool = False
|
|
||||||
algorithm: str = ""
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Place report files and experiment outputs here.
|
|
||||||
|
|
@ -1,249 +0,0 @@
|
||||||
# Отчёт по работе «Поиск выхода из лабиринта»
|
|
||||||
|
|
||||||
## 1. Цель работы
|
|
||||||
Разработать гибкую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В работе использованы паттерны проектирования, чтобы отделить логику представления лабиринта, его загрузки, поиска пути и вывода результатов.
|
|
||||||
|
|
||||||
## 2. Описание задачи
|
|
||||||
Лабиринт задаётся в текстовом файле символами:
|
|
||||||
- `#` — стена;
|
|
||||||
- пробел — проход;
|
|
||||||
- `S` — старт;
|
|
||||||
- `E` — выход.
|
|
||||||
|
|
||||||
Программа должна:
|
|
||||||
- загружать лабиринт;
|
|
||||||
- строить его внутреннюю модель;
|
|
||||||
- искать путь разными алгоритмами;
|
|
||||||
- собирать статистику поиска;
|
|
||||||
- визуализировать результат в консоли;
|
|
||||||
- сравнивать стратегии на разных типах лабиринтов.
|
|
||||||
|
|
||||||
## 3. Выбранные паттерны проектирования
|
|
||||||
|
|
||||||
### 3.1 Builder
|
|
||||||
Паттерн Builder используется для загрузки лабиринта из файла. Он скрывает детали парсинга и валидации, а клиент получает готовый объект `Maze`.
|
|
||||||
|
|
||||||
Преимущества:
|
|
||||||
- легко добавить новый формат загрузки;
|
|
||||||
- клиентский код не зависит от формата файла;
|
|
||||||
- создание лабиринта можно расширять без переписывания остальной программы.
|
|
||||||
|
|
||||||
### 3.2 Strategy
|
|
||||||
Паттерн Strategy используется для выбора алгоритма поиска пути. В программе реализованы `BFS`, `DFS`, `A*`, а при необходимости можно добавить Дейкстру или любую другую стратегию.
|
|
||||||
|
|
||||||
Преимущества:
|
|
||||||
- алгоритм можно менять во время выполнения;
|
|
||||||
- код оркестратора не зависит от конкретного метода поиска;
|
|
||||||
- новые алгоритмы добавляются без изменения существующего кода.
|
|
||||||
|
|
||||||
### 3.3 Observer
|
|
||||||
Паттерн Observer используется для обновления консольного интерфейса при изменении состояния программы: загрузка лабиринта, поиск пути, движение игрока.
|
|
||||||
|
|
||||||
Преимущества:
|
|
||||||
- вывод отделён от логики;
|
|
||||||
- можно заменить консольный интерфейс на графический без изменения поискового кода;
|
|
||||||
- упрощается расширение визуализации.
|
|
||||||
|
|
||||||
### 3.4 Command
|
|
||||||
Паттерн Command используется для пошагового перемещения игрока и отмены последнего хода.
|
|
||||||
|
|
||||||
Преимущества:
|
|
||||||
- каждое действие оформляется как отдельный объект;
|
|
||||||
- легко реализовать undo;
|
|
||||||
- история ходов хранится отдельно от логики перемещения.
|
|
||||||
|
|
||||||
## 4. Диаграмма классов
|
|
||||||
Ниже приведена упрощённая диаграмма классов в формате Mermaid:
|
|
||||||
|
|
||||||
```mermaid
|
|
||||||
classDiagram
|
|
||||||
class Cell {
|
|
||||||
+int x
|
|
||||||
+int y
|
|
||||||
+bool isWall
|
|
||||||
+bool isStart
|
|
||||||
+bool isExit
|
|
||||||
+isPassable()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Maze {
|
|
||||||
+cells
|
|
||||||
+width
|
|
||||||
+height
|
|
||||||
+startCell
|
|
||||||
+exitCell
|
|
||||||
+getCell(x, y)
|
|
||||||
+getNeighbors(cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
class MazeBuilder {
|
|
||||||
<<interface>>
|
|
||||||
+buildFromFile(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
class TextFileMazeBuilder {
|
|
||||||
+buildFromFile(filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
class PathFindingStrategy {
|
|
||||||
<<interface>>
|
|
||||||
+findPath(maze, start, exitCell)
|
|
||||||
}
|
|
||||||
|
|
||||||
class BFSStrategy {
|
|
||||||
+findPath(maze, start, exitCell)
|
|
||||||
}
|
|
||||||
|
|
||||||
class DFSStrategy {
|
|
||||||
+findPath(maze, start, exitCell)
|
|
||||||
}
|
|
||||||
|
|
||||||
class AStarStrategy {
|
|
||||||
+findPath(maze, start, exitCell)
|
|
||||||
}
|
|
||||||
|
|
||||||
class SearchStats {
|
|
||||||
+timeMs
|
|
||||||
+visitedCells
|
|
||||||
+pathLength
|
|
||||||
+path
|
|
||||||
}
|
|
||||||
|
|
||||||
class MazeSolver {
|
|
||||||
+maze
|
|
||||||
+strategy
|
|
||||||
+setStrategy(strategy)
|
|
||||||
+solve()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Observer {
|
|
||||||
<<interface>>
|
|
||||||
+update(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConsoleView {
|
|
||||||
+update(event)
|
|
||||||
+render(maze, player_position, path)
|
|
||||||
}
|
|
||||||
|
|
||||||
class Command {
|
|
||||||
<<interface>>
|
|
||||||
+execute()
|
|
||||||
+undo()
|
|
||||||
}
|
|
||||||
|
|
||||||
class MoveCommand {
|
|
||||||
+execute()
|
|
||||||
+undo()
|
|
||||||
}
|
|
||||||
|
|
||||||
class Player {
|
|
||||||
+currentCell
|
|
||||||
+setCell(cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
Maze <|-- TextFileMazeBuilder : creates
|
|
||||||
MazeBuilder <|.. TextFileMazeBuilder
|
|
||||||
PathFindingStrategy <|.. BFSStrategy
|
|
||||||
PathFindingStrategy <|.. DFSStrategy
|
|
||||||
PathFindingStrategy <|.. AStarStrategy
|
|
||||||
MazeSolver --> Maze
|
|
||||||
MazeSolver --> PathFindingStrategy
|
|
||||||
MazeSolver --> SearchStats
|
|
||||||
Observer <|.. ConsoleView
|
|
||||||
Command <|.. MoveCommand
|
|
||||||
MoveCommand --> Player
|
|
||||||
MoveCommand --> Maze
|
|
||||||
ConsoleView --> Maze
|
|
||||||
Maze --> Cell
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5. Ключевые классы и их роль
|
|
||||||
|
|
||||||
### Cell
|
|
||||||
Хранит координаты клетки и её тип. Позволяет быстро проверять, является ли клетка проходимой.
|
|
||||||
|
|
||||||
### Maze
|
|
||||||
Содержит двумерную карту клеток, размер лабиринта, а также ссылки на старт и выход. Даёт доступ к соседним клеткам по четырём направлениям.
|
|
||||||
|
|
||||||
### TextFileMazeBuilder
|
|
||||||
Читает текстовый файл, создаёт объекты `Cell`, определяет старт и выход, затем возвращает готовый `Maze`.
|
|
||||||
|
|
||||||
### BFSStrategy
|
|
||||||
Ищет кратчайший путь по числу шагов. Подходит для случая, когда все переходы одинаковой стоимости.
|
|
||||||
|
|
||||||
### DFSStrategy
|
|
||||||
Быстро исследует пространство, но не гарантирует кратчайший путь. Полезен как сравнительный алгоритм.
|
|
||||||
|
|
||||||
### AStarStrategy
|
|
||||||
Использует эвристику Манхэттенского расстояния. Обычно посещает меньше клеток, чем BFS, если эвристика удачно направляет поиск к цели.
|
|
||||||
|
|
||||||
### MazeSolver
|
|
||||||
Оркестратор, который хранит лабиринт и текущую стратегию. Вызывает поиск, измеряет время и собирает статистику.
|
|
||||||
|
|
||||||
### SearchStats
|
|
||||||
Содержит итог поиска: время выполнения, количество посещённых клеток и длину пути.
|
|
||||||
|
|
||||||
### ConsoleView
|
|
||||||
Реализует наблюдателя и умеет выводить лабиринт и найденный путь в консоль.
|
|
||||||
|
|
||||||
### MoveCommand
|
|
||||||
Оформляет ход игрока как объект-команду. Поддерживает отмену последнего перемещения.
|
|
||||||
|
|
||||||
## 6. Экспериментальная часть
|
|
||||||
|
|
||||||
### 6.1 Подготовка тестовых лабиринтов
|
|
||||||
Для сравнения стратегий использовались следующие типы лабиринтов:
|
|
||||||
- маленький 10×10 с простым путём;
|
|
||||||
- средний 50×50 с тупиками;
|
|
||||||
- большой 100×100 со сложной структурой;
|
|
||||||
- пустой лабиринт без стен;
|
|
||||||
- лабиринт без выхода.
|
|
||||||
|
|
||||||
### 6.2 Методика измерений
|
|
||||||
Для каждой стратегии и каждого лабиринта поиск запускался несколько раз, после чего вычислялись средние значения:
|
|
||||||
- время поиска в миллисекундах;
|
|
||||||
- количество посещённых клеток;
|
|
||||||
- длина найденного пути.
|
|
||||||
|
|
||||||
Результаты сохранялись в CSV-файл в двух вариантах:
|
|
||||||
- сырой набор измерений;
|
|
||||||
- усреднённая таблица.
|
|
||||||
|
|
||||||
## 7. Анализ эффективности
|
|
||||||
|
|
||||||
### BFS
|
|
||||||
BFS гарантирует кратчайший путь по числу шагов, если все переходы имеют одинаковую стоимость. На простых и пустых лабиринтах работает стабильно и предсказуемо. Минус — может посещать много клеток, особенно на больших лабиринтах.
|
|
||||||
|
|
||||||
### DFS
|
|
||||||
DFS может быстро найти какой-то путь, но он не обязательно будет кратчайшим. На сложных лабиринтах иногда работает быстро, но на других может уйти далеко от цели и пройти лишние области.
|
|
||||||
|
|
||||||
### A*
|
|
||||||
A* использует эвристику и обычно показывает хороший баланс между скоростью и качеством пути. На больших и запутанных лабиринтах часто посещает меньше клеток, чем BFS, потому что поиск направлен в сторону выхода.
|
|
||||||
|
|
||||||
### Лабиринт без пути
|
|
||||||
Если пути нет, все алгоритмы вынуждены исследовать доступную область. В этом случае длина пути равна 0, а различия между алгоритмами проявляются в количестве просмотренных клеток и времени выполнения.
|
|
||||||
|
|
||||||
### Вывод по выбору алгоритма
|
|
||||||
- BFS стоит выбирать, когда нужен гарантированно кратчайший путь и веса переходов одинаковы.
|
|
||||||
- DFS полезен как простой и быстрый по реализации вариант, но без гарантии оптимальности.
|
|
||||||
- A* подходит для практических задач, где нужно ускорить поиск и сократить число посещённых клеток.
|
|
||||||
- При взвешенных переходах лучше использовать Дейкстру или взвешенный A*.
|
|
||||||
|
|
||||||
## 8. Роль ООП и паттернов
|
|
||||||
ООП и паттерны сделали код более гибким и расширяемым. Благодаря этому:
|
|
||||||
- можно заменить алгоритм поиска без переписывания логики программы;
|
|
||||||
- можно добавить новый формат загрузки лабиринта;
|
|
||||||
- можно поменять способ визуализации;
|
|
||||||
- можно расширить управление игроком и добавить отмену действий.
|
|
||||||
|
|
||||||
Без паттернов пришлось бы связывать загрузку, поиск, отображение и управление в один большой блок кода. Это усложнило бы отладку и дальнейшие изменения.
|
|
||||||
|
|
||||||
## 9. Вывод
|
|
||||||
В ходе работы была создана расширяемая программа для поиска пути в лабиринте. Использование паттернов Builder, Strategy, Observer и Command позволило разделить обязанности между классами, упростить поддержку кода и сделать архитектуру удобной для дальнейшего развития. Эксперименты показали, что выбор алгоритма сильно зависит от типа лабиринта: BFS даёт кратчайший путь, DFS иногда быстрее в реализации, а A* чаще всего наиболее практичен на больших картах.
|
|
||||||
|
|
||||||
## 10. Приложения
|
|
||||||
- Листинги ключевых классов.
|
|
||||||
- CSV-файлы с результатами экспериментов.
|
|
||||||
- Графики сравнений.
|
|
||||||
- Файлы с тестовыми лабиринтами.
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
from pathlib import Path
|
|
||||||
from statistics import mean
|
|
||||||
import csv
|
|
||||||
import random
|
|
||||||
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
|
|
||||||
from core.cell import Cell
|
|
||||||
from core.maze import Maze
|
|
||||||
from solver.maze_solver import MazeSolver
|
|
||||||
from strategies.astar_strategy import AStarStrategy
|
|
||||||
from strategies.bfs_strategy import BFSStrategy
|
|
||||||
from strategies.dfs_strategy import DFSStrategy
|
|
||||||
from strategies.dijkstra_strategy import DijkstraStrategy
|
|
||||||
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
|
||||||
OUT_DIR = BASE_DIR / "experiment_results"
|
|
||||||
|
|
||||||
|
|
||||||
def build_maze_from_symbols(lines):
|
|
||||||
height = len(lines)
|
|
||||||
width = max(len(line) for line in lines)
|
|
||||||
cells = []
|
|
||||||
start = None
|
|
||||||
exit_cell = None
|
|
||||||
for y, line in enumerate(lines):
|
|
||||||
row = []
|
|
||||||
for x in range(width):
|
|
||||||
ch = line[x] if x < len(line) else "#"
|
|
||||||
if ch == "#":
|
|
||||||
cell = Cell(x, y, isWall=True)
|
|
||||||
elif ch == "S":
|
|
||||||
cell = Cell(x, y, isWall=False, isStart=True)
|
|
||||||
start = cell
|
|
||||||
elif ch == "E":
|
|
||||||
cell = Cell(x, y, isWall=False, isExit=True)
|
|
||||||
exit_cell = cell
|
|
||||||
elif ch == " " or ch == ".":
|
|
||||||
cell = Cell(x, y, isWall=False)
|
|
||||||
elif ch.isdigit():
|
|
||||||
cell = Cell(x, y, isWall=False, weight=int(ch))
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown symbol '{ch}' at {x},{y}")
|
|
||||||
row.append(cell)
|
|
||||||
cells.append(row)
|
|
||||||
return Maze(cells, width, height, start, exit_cell)
|
|
||||||
|
|
||||||
|
|
||||||
def generate_empty_maze(width, height):
|
|
||||||
lines = [" " * width for _ in range(height)]
|
|
||||||
lines = [list(row) for row in lines]
|
|
||||||
lines[1][1] = "S"
|
|
||||||
lines[height - 2][width - 2] = "E"
|
|
||||||
return build_maze_from_symbols(["".join(row) for row in lines])
|
|
||||||
|
|
||||||
|
|
||||||
def generate_simple_maze(width, height):
|
|
||||||
grid = [["#" for _ in range(width)] for _ in range(height)]
|
|
||||||
for x in range(1, width - 1):
|
|
||||||
grid[1][x] = " "
|
|
||||||
for y in range(1, height - 1):
|
|
||||||
grid[y][width - 2] = " "
|
|
||||||
grid[1][1] = "S"
|
|
||||||
grid[height - 2][width - 2] = "E"
|
|
||||||
return build_maze_from_symbols(["".join(row) for row in grid])
|
|
||||||
|
|
||||||
|
|
||||||
def generate_branching_maze(width, height, seed=42, wall_density=0.30):
|
|
||||||
rng = random.Random(seed)
|
|
||||||
grid = [["#" for _ in range(width)] for _ in range(height)]
|
|
||||||
x, y = 1, 1
|
|
||||||
grid[y][x] = "S"
|
|
||||||
while (x, y) != (width - 2, height - 2):
|
|
||||||
candidates = []
|
|
||||||
for dx, dy in [(1, 0), (0, 1)]:
|
|
||||||
nx, ny = x + dx, y + dy
|
|
||||||
if 1 <= nx < width - 1 and 1 <= ny < height - 1:
|
|
||||||
candidates.append((nx, ny))
|
|
||||||
if not candidates:
|
|
||||||
break
|
|
||||||
x, y = rng.choice(candidates)
|
|
||||||
grid[y][x] = " "
|
|
||||||
grid[height - 2][width - 2] = "E"
|
|
||||||
|
|
||||||
# carve extra corridors and dead ends
|
|
||||||
for yy in range(1, height - 1):
|
|
||||||
for xx in range(1, width - 1):
|
|
||||||
if grid[yy][xx] == "#" and rng.random() > wall_density:
|
|
||||||
grid[yy][xx] = " "
|
|
||||||
grid[1][1] = "S"
|
|
||||||
grid[height - 2][width - 2] = "E"
|
|
||||||
return build_maze_from_symbols(["".join(row) for row in grid])
|
|
||||||
|
|
||||||
|
|
||||||
def generate_no_path_maze(width, height):
|
|
||||||
grid = [[" " for _ in range(width)] for _ in range(height)]
|
|
||||||
for x in range(width):
|
|
||||||
grid[height // 2][x] = "#"
|
|
||||||
grid[1][1] = "S"
|
|
||||||
grid[height - 2][width - 2] = "E"
|
|
||||||
return build_maze_from_symbols(["".join(row) for row in grid])
|
|
||||||
|
|
||||||
|
|
||||||
def generate_weighted_maze(width, height, seed=123):
|
|
||||||
rng = random.Random(seed)
|
|
||||||
grid = [[" " for _ in range(width)] for _ in range(height)]
|
|
||||||
for y in range(height):
|
|
||||||
for x in range(width):
|
|
||||||
r = rng.random()
|
|
||||||
if r < 0.12:
|
|
||||||
grid[y][x] = "#"
|
|
||||||
elif r < 0.25:
|
|
||||||
grid[y][x] = "3"
|
|
||||||
elif r < 0.40:
|
|
||||||
grid[y][x] = "2"
|
|
||||||
else:
|
|
||||||
grid[y][x] = "1"
|
|
||||||
# ensure path-ish
|
|
||||||
for x in range(width):
|
|
||||||
grid[1][x] = "1"
|
|
||||||
for y in range(1, height):
|
|
||||||
grid[y][width - 2] = "1"
|
|
||||||
grid[1][1] = "S"
|
|
||||||
grid[height - 2][width - 2] = "E"
|
|
||||||
return build_maze_from_symbols(["".join(row) for row in grid])
|
|
||||||
|
|
||||||
|
|
||||||
def bench_one_maze(maze_name, maze, strategies, repeats=5):
|
|
||||||
summary_rows = []
|
|
||||||
raw_rows = []
|
|
||||||
for strategy_name, strategy_factory in strategies:
|
|
||||||
times, visiteds, lengths = [], [], []
|
|
||||||
for run in range(1, repeats + 1):
|
|
||||||
solver = MazeSolver(maze)
|
|
||||||
solver.setStrategy(strategy_factory())
|
|
||||||
stats = solver.solve()
|
|
||||||
raw_rows.append([maze_name, strategy_name, run, f"{stats.timeMs:.6f}", stats.visitedCells, stats.pathLength])
|
|
||||||
times.append(stats.timeMs)
|
|
||||||
visiteds.append(stats.visitedCells)
|
|
||||||
lengths.append(stats.pathLength)
|
|
||||||
summary_rows.append([maze_name, strategy_name, f"{mean(times):.6f}", f"{mean(visiteds):.2f}", f"{mean(lengths):.2f}", repeats])
|
|
||||||
return summary_rows, raw_rows
|
|
||||||
|
|
||||||
|
|
||||||
def save_csv(path, rows):
|
|
||||||
with open(path, "w", newline="", encoding="utf-8") as f:
|
|
||||||
csv.writer(f).writerows(rows)
|
|
||||||
|
|
||||||
|
|
||||||
def plot_summary(summary_rows):
|
|
||||||
by_maze = {}
|
|
||||||
for row in summary_rows[1:]:
|
|
||||||
maze_name, strategy, avg_time, avg_visited, avg_len, runs = row
|
|
||||||
by_maze.setdefault(maze_name, []).append((strategy, float(avg_time), float(avg_visited), float(avg_len)))
|
|
||||||
|
|
||||||
for maze_name, items in by_maze.items():
|
|
||||||
items.sort(key=lambda t: t[0])
|
|
||||||
strategies = [i[0] for i in items]
|
|
||||||
x = list(range(len(strategies)))
|
|
||||||
|
|
||||||
plt.figure(figsize=(8, 4))
|
|
||||||
plt.bar(x, [i[1] for i in items])
|
|
||||||
plt.xticks(x, strategies)
|
|
||||||
plt.ylabel("ms")
|
|
||||||
plt.title(f"{maze_name} — avg time")
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig(OUT_DIR / f"{maze_name}_time.png", dpi=150)
|
|
||||||
plt.close()
|
|
||||||
|
|
||||||
plt.figure(figsize=(8, 4))
|
|
||||||
plt.bar(x, [i[2] for i in items])
|
|
||||||
plt.xticks(x, strategies)
|
|
||||||
plt.ylabel("cells")
|
|
||||||
plt.title(f"{maze_name} — visited cells")
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig(OUT_DIR / f"{maze_name}_visited.png", dpi=150)
|
|
||||||
plt.close()
|
|
||||||
|
|
||||||
plt.figure(figsize=(8, 4))
|
|
||||||
plt.bar(x, [i[3] for i in items])
|
|
||||||
plt.xticks(x, strategies)
|
|
||||||
plt.ylabel("cells")
|
|
||||||
plt.title(f"{maze_name} — path length")
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig(OUT_DIR / f"{maze_name}_length.png", dpi=150)
|
|
||||||
plt.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
OUT_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
strategies = [
|
|
||||||
("BFS", BFSStrategy),
|
|
||||||
("DFS", DFSStrategy),
|
|
||||||
("A*", AStarStrategy),
|
|
||||||
("Dijkstra", DijkstraStrategy),
|
|
||||||
]
|
|
||||||
|
|
||||||
mazes = [
|
|
||||||
("small_10x10", generate_simple_maze(10, 10)),
|
|
||||||
("medium_50x50", generate_branching_maze(50, 50)),
|
|
||||||
("large_100x100", generate_branching_maze(100, 100, seed=99, wall_density=0.35)),
|
|
||||||
("empty_30x30", generate_empty_maze(30, 30)),
|
|
||||||
("no_path_30x30", generate_no_path_maze(30, 30)),
|
|
||||||
("weighted_30x30", generate_weighted_maze(30, 30)),
|
|
||||||
]
|
|
||||||
|
|
||||||
summary = [["maze", "strategy", "avg_time_ms", "avg_visited_cells", "avg_path_length", "runs"]]
|
|
||||||
raw = [["maze", "strategy", "run", "time_ms", "visited_cells", "path_length"]]
|
|
||||||
|
|
||||||
for maze_name, maze in mazes:
|
|
||||||
s_rows, r_rows = bench_one_maze(maze_name, maze, strategies, repeats=5)
|
|
||||||
summary.extend(s_rows)
|
|
||||||
raw.extend(r_rows)
|
|
||||||
|
|
||||||
save_csv(OUT_DIR / "summary.csv", summary)
|
|
||||||
save_csv(OUT_DIR / "raw.csv", raw)
|
|
||||||
plot_summary(summary)
|
|
||||||
|
|
||||||
print("Saved to", OUT_DIR.resolve())
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
|
@ -1,121 +0,0 @@
|
||||||
maze,strategy,run,time_ms,visited_cells,path_length
|
|
||||||
small_10x10,BFS,1,0.044300,15,15
|
|
||||||
small_10x10,BFS,2,0.022800,15,15
|
|
||||||
small_10x10,BFS,3,0.020400,15,15
|
|
||||||
small_10x10,BFS,4,0.020300,15,15
|
|
||||||
small_10x10,BFS,5,0.018700,15,15
|
|
||||||
small_10x10,DFS,1,0.031200,15,15
|
|
||||||
small_10x10,DFS,2,0.022000,15,15
|
|
||||||
small_10x10,DFS,3,0.021200,15,15
|
|
||||||
small_10x10,DFS,4,0.020800,15,15
|
|
||||||
small_10x10,DFS,5,0.020500,15,15
|
|
||||||
small_10x10,A*,1,0.048900,15,15
|
|
||||||
small_10x10,A*,2,0.034700,15,15
|
|
||||||
small_10x10,A*,3,0.029400,15,15
|
|
||||||
small_10x10,A*,4,0.029100,15,15
|
|
||||||
small_10x10,A*,5,0.029300,15,15
|
|
||||||
small_10x10,Dijkstra,1,0.037900,15,15
|
|
||||||
small_10x10,Dijkstra,2,0.028500,15,15
|
|
||||||
small_10x10,Dijkstra,3,0.026800,15,15
|
|
||||||
small_10x10,Dijkstra,4,0.026400,15,15
|
|
||||||
small_10x10,Dijkstra,5,0.026700,15,15
|
|
||||||
medium_50x50,BFS,1,2.105800,1579,95
|
|
||||||
medium_50x50,BFS,2,1.928700,1579,95
|
|
||||||
medium_50x50,BFS,3,1.969500,1579,95
|
|
||||||
medium_50x50,BFS,4,1.938800,1579,95
|
|
||||||
medium_50x50,BFS,5,1.943600,1579,95
|
|
||||||
medium_50x50,DFS,1,1.927300,1277,647
|
|
||||||
medium_50x50,DFS,2,1.856300,1277,647
|
|
||||||
medium_50x50,DFS,3,1.890100,1277,647
|
|
||||||
medium_50x50,DFS,4,1.868000,1277,647
|
|
||||||
medium_50x50,DFS,5,1.865500,1277,647
|
|
||||||
medium_50x50,A*,1,2.359000,927,95
|
|
||||||
medium_50x50,A*,2,2.193700,927,95
|
|
||||||
medium_50x50,A*,3,2.178400,927,95
|
|
||||||
medium_50x50,A*,4,2.181800,927,95
|
|
||||||
medium_50x50,A*,5,2.174500,927,95
|
|
||||||
medium_50x50,Dijkstra,1,3.534700,1579,95
|
|
||||||
medium_50x50,Dijkstra,2,3.435500,1579,95
|
|
||||||
medium_50x50,Dijkstra,3,3.457600,1579,95
|
|
||||||
medium_50x50,Dijkstra,4,3.417300,1579,95
|
|
||||||
medium_50x50,Dijkstra,5,3.538000,1579,95
|
|
||||||
large_100x100,BFS,1,8.624100,5566,195
|
|
||||||
large_100x100,BFS,2,7.706900,5566,195
|
|
||||||
large_100x100,BFS,3,9.723300,5566,195
|
|
||||||
large_100x100,BFS,4,7.585700,5566,195
|
|
||||||
large_100x100,BFS,5,8.031300,5566,195
|
|
||||||
large_100x100,DFS,1,5.512400,3543,1531
|
|
||||||
large_100x100,DFS,2,5.329300,3543,1531
|
|
||||||
large_100x100,DFS,3,5.223300,3543,1531
|
|
||||||
large_100x100,DFS,4,5.729900,3543,1531
|
|
||||||
large_100x100,DFS,5,5.497400,3543,1531
|
|
||||||
large_100x100,A*,1,2.101500,853,195
|
|
||||||
large_100x100,A*,2,2.264500,853,195
|
|
||||||
large_100x100,A*,3,2.064100,853,195
|
|
||||||
large_100x100,A*,4,2.031700,853,195
|
|
||||||
large_100x100,A*,5,2.046500,853,195
|
|
||||||
large_100x100,Dijkstra,1,25.021300,5571,195
|
|
||||||
large_100x100,Dijkstra,2,13.541100,5571,195
|
|
||||||
large_100x100,Dijkstra,3,12.884100,5571,195
|
|
||||||
large_100x100,Dijkstra,4,13.481800,5571,195
|
|
||||||
large_100x100,Dijkstra,5,12.748000,5571,195
|
|
||||||
empty_30x30,BFS,1,1.234300,896,55
|
|
||||||
empty_30x30,BFS,2,1.163400,896,55
|
|
||||||
empty_30x30,BFS,3,1.145700,896,55
|
|
||||||
empty_30x30,BFS,4,1.177300,896,55
|
|
||||||
empty_30x30,BFS,5,1.175100,896,55
|
|
||||||
empty_30x30,DFS,1,1.338000,842,815
|
|
||||||
empty_30x30,DFS,2,1.296500,842,815
|
|
||||||
empty_30x30,DFS,3,1.296700,842,815
|
|
||||||
empty_30x30,DFS,4,1.280100,842,815
|
|
||||||
empty_30x30,DFS,5,1.290800,842,815
|
|
||||||
empty_30x30,A*,1,2.183400,784,55
|
|
||||||
empty_30x30,A*,2,2.522900,784,55
|
|
||||||
empty_30x30,A*,3,1.985000,784,55
|
|
||||||
empty_30x30,A*,4,1.972100,784,55
|
|
||||||
empty_30x30,A*,5,2.088600,784,55
|
|
||||||
empty_30x30,Dijkstra,1,2.080400,896,55
|
|
||||||
empty_30x30,Dijkstra,2,2.100100,896,55
|
|
||||||
empty_30x30,Dijkstra,3,2.130700,896,55
|
|
||||||
empty_30x30,Dijkstra,4,2.073600,896,55
|
|
||||||
empty_30x30,Dijkstra,5,2.095900,896,55
|
|
||||||
no_path_30x30,BFS,1,0.645900,450,0
|
|
||||||
no_path_30x30,BFS,2,0.566600,450,0
|
|
||||||
no_path_30x30,BFS,3,0.566000,450,0
|
|
||||||
no_path_30x30,BFS,4,0.583500,450,0
|
|
||||||
no_path_30x30,BFS,5,0.568900,450,0
|
|
||||||
no_path_30x30,DFS,1,0.692100,450,0
|
|
||||||
no_path_30x30,DFS,2,0.676900,450,0
|
|
||||||
no_path_30x30,DFS,3,0.703500,450,0
|
|
||||||
no_path_30x30,DFS,4,0.722300,450,0
|
|
||||||
no_path_30x30,DFS,5,0.672000,450,0
|
|
||||||
no_path_30x30,A*,1,1.112700,450,0
|
|
||||||
no_path_30x30,A*,2,1.130000,450,0
|
|
||||||
no_path_30x30,A*,3,1.096100,450,0
|
|
||||||
no_path_30x30,A*,4,1.111400,450,0
|
|
||||||
no_path_30x30,A*,5,1.183500,450,0
|
|
||||||
no_path_30x30,Dijkstra,1,1.023300,450,0
|
|
||||||
no_path_30x30,Dijkstra,2,1.011700,450,0
|
|
||||||
no_path_30x30,Dijkstra,3,1.127200,450,0
|
|
||||||
no_path_30x30,Dijkstra,4,1.110200,450,0
|
|
||||||
no_path_30x30,Dijkstra,5,1.043900,450,0
|
|
||||||
weighted_30x30,BFS,1,1.074700,788,55
|
|
||||||
weighted_30x30,BFS,2,0.997700,788,55
|
|
||||||
weighted_30x30,BFS,3,0.992700,788,55
|
|
||||||
weighted_30x30,BFS,4,1.010800,788,55
|
|
||||||
weighted_30x30,BFS,5,1.035000,788,55
|
|
||||||
weighted_30x30,DFS,1,1.130200,693,479
|
|
||||||
weighted_30x30,DFS,2,1.057400,693,479
|
|
||||||
weighted_30x30,DFS,3,1.049900,693,479
|
|
||||||
weighted_30x30,DFS,4,1.051600,693,479
|
|
||||||
weighted_30x30,DFS,5,1.059100,693,479
|
|
||||||
weighted_30x30,A*,1,0.402200,126,55
|
|
||||||
weighted_30x30,A*,2,0.384100,126,55
|
|
||||||
weighted_30x30,A*,3,0.360000,126,55
|
|
||||||
weighted_30x30,A*,4,0.360700,126,55
|
|
||||||
weighted_30x30,A*,5,0.353500,126,55
|
|
||||||
weighted_30x30,Dijkstra,1,1.834900,781,55
|
|
||||||
weighted_30x30,Dijkstra,2,1.759000,781,55
|
|
||||||
weighted_30x30,Dijkstra,3,1.786300,781,55
|
|
||||||
weighted_30x30,Dijkstra,4,1.740500,781,55
|
|
||||||
weighted_30x30,Dijkstra,5,1.807100,781,55
|
|
||||||
|
|
Before Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 18 KiB |
|
|
@ -1,25 +0,0 @@
|
||||||
maze,strategy,avg_time_ms,avg_visited_cells,avg_path_length,runs
|
|
||||||
small_10x10,BFS,0.025300,15.00,15.00,5
|
|
||||||
small_10x10,DFS,0.023140,15.00,15.00,5
|
|
||||||
small_10x10,A*,0.034280,15.00,15.00,5
|
|
||||||
small_10x10,Dijkstra,0.029260,15.00,15.00,5
|
|
||||||
medium_50x50,BFS,1.977280,1579.00,95.00,5
|
|
||||||
medium_50x50,DFS,1.881440,1277.00,647.00,5
|
|
||||||
medium_50x50,A*,2.217480,927.00,95.00,5
|
|
||||||
medium_50x50,Dijkstra,3.476620,1579.00,95.00,5
|
|
||||||
large_100x100,BFS,8.334260,5566.00,195.00,5
|
|
||||||
large_100x100,DFS,5.458460,3543.00,1531.00,5
|
|
||||||
large_100x100,A*,2.101660,853.00,195.00,5
|
|
||||||
large_100x100,Dijkstra,15.535260,5571.00,195.00,5
|
|
||||||
empty_30x30,BFS,1.179160,896.00,55.00,5
|
|
||||||
empty_30x30,DFS,1.300420,842.00,815.00,5
|
|
||||||
empty_30x30,A*,2.150400,784.00,55.00,5
|
|
||||||
empty_30x30,Dijkstra,2.096140,896.00,55.00,5
|
|
||||||
no_path_30x30,BFS,0.586180,450.00,0.00,5
|
|
||||||
no_path_30x30,DFS,0.693360,450.00,0.00,5
|
|
||||||
no_path_30x30,A*,1.126740,450.00,0.00,5
|
|
||||||
no_path_30x30,Dijkstra,1.063260,450.00,0.00,5
|
|
||||||
weighted_30x30,BFS,1.022180,788.00,55.00,5
|
|
||||||
weighted_30x30,DFS,1.069640,693.00,479.00,5
|
|
||||||
weighted_30x30,A*,0.372100,126.00,55.00,5
|
|
||||||
weighted_30x30,Dijkstra,1.785560,781.00,55.00,5
|
|
||||||
|
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
|
@ -1,59 +0,0 @@
|
||||||
from builders.text_file_maze_builder import TextFileMazeBuilder
|
|
||||||
from core.player import Player
|
|
||||||
from observer.console_view import ConsoleView
|
|
||||||
from solver.maze_solver import MazeSolver
|
|
||||||
from strategies.astar_strategy import AStarStrategy
|
|
||||||
from strategies.bfs_strategy import BFSStrategy
|
|
||||||
from strategies.dfs_strategy import DFSStrategy
|
|
||||||
from controller.game_controller import GameController
|
|
||||||
|
|
||||||
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent
|
|
||||||
|
|
||||||
|
|
||||||
def run_demo():
|
|
||||||
builder = TextFileMazeBuilder()
|
|
||||||
maze = builder.buildFromFile(str(BASE_DIR / "mazes" / "maze_small.txt"))
|
|
||||||
|
|
||||||
view = ConsoleView()
|
|
||||||
view.update({"type": "maze_loaded", "message": "Maze loaded"})
|
|
||||||
view.render(maze)
|
|
||||||
|
|
||||||
solver = MazeSolver(maze)
|
|
||||||
solver.addObserver(view)
|
|
||||||
|
|
||||||
for strategy in (BFSStrategy(), DFSStrategy(), AStarStrategy()):
|
|
||||||
solver.setStrategy(strategy)
|
|
||||||
stats = solver.solve()
|
|
||||||
|
|
||||||
print()
|
|
||||||
print(f"=== {strategy.name} ===")
|
|
||||||
print(f"Time: {stats.timeMs:.3f} ms")
|
|
||||||
print(f"Visited cells: {stats.visitedCells}")
|
|
||||||
print(f"Path length: {stats.pathLength}")
|
|
||||||
print(f"Path found: {'yes' if stats.found else 'no'}")
|
|
||||||
|
|
||||||
view.render(maze, path=stats.path)
|
|
||||||
|
|
||||||
player = Player(maze.startCell)
|
|
||||||
controller = GameController(maze, player, view)
|
|
||||||
|
|
||||||
print("Manual mode: W/A/S/D move, Z undo, Q quit")
|
|
||||||
view.render(maze, player_position=player.currentCell)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
cmd = input("Command: ").strip().upper()
|
|
||||||
if cmd == "Q":
|
|
||||||
break
|
|
||||||
if cmd == "Z":
|
|
||||||
controller.undo()
|
|
||||||
elif cmd in {"W", "A", "S", "D"}:
|
|
||||||
controller.move(cmd)
|
|
||||||
else:
|
|
||||||
print("Unknown command")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
run_demo()
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
S
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
E
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
####################################################################################################
|
|
||||||
#S # # # # # # # # # # # # # # # E#
|
|
||||||
# # ### ### # ###### # ### # ## # #### # ####### # #### # # ### ## # ## # # ## # ## # ##### ### ##
|
|
||||||
# # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
||||||
# ##### # ######## # ### # ## # #### # ####### ## ### # # #### ####### ## ####### ####### # ### ##
|
|
||||||
# # # # # # # # # # # # # # # # # # # # #
|
|
||||||
### # # ###### # ########### ########### ### ####### # ####### ### # # ###### # ### ### # ### ####
|
|
||||||
# # # # # # # # # # # # # # # # # # # # # #
|
|
||||||
# ### ###### # ##### # ### # ####### # ### ### ## # ###### # ### # ### ###### # ### # ### ### ## #
|
|
||||||
# # # # # # # # #
|
|
||||||
####################################################################################################
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
##################################################
|
|
||||||
#S # # # # # # E#
|
|
||||||
# # ### ### # ###### # ### # ## # #### # ####### ##
|
|
||||||
# # # # # # # # # # # # # #
|
|
||||||
# ##### # ######## # ### # ## # #### # ####### ## #
|
|
||||||
# # # # # # # # # #
|
|
||||||
### # # ###### # ########### ########### ### ######
|
|
||||||
# # # # # # # # # # #
|
|
||||||
# ### ###### # ##### # ### # ####### # ### ### ## #
|
|
||||||
# # # # #
|
|
||||||
##################################################
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
##########
|
|
||||||
#S #
|
|
||||||
# ###### #
|
|
||||||
# # #
|
|
||||||
##########
|
|
||||||
# #E#
|
|
||||||
# ###### #
|
|
||||||
# #
|
|
||||||
##########
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
##########
|
|
||||||
#S #E#
|
|
||||||
# ## # # ##
|
|
||||||
# # #
|
|
||||||
# #### # #
|
|
||||||
# # #
|
|
||||||
##########
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
1111111111111111111111111111
|
|
||||||
1S11111111111111111111111111
|
|
||||||
1111111111111111111111111111
|
|
||||||
1111111111111111111111111111
|
|
||||||
1111111111111222222222222111
|
|
||||||
1111111111111222222222222111
|
|
||||||
1111111111111333333333333111
|
|
||||||
1111111111111333333333333111
|
|
||||||
111111111111111111111111111E
|
|
||||||
1111111111111111111111111111
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import os
|
|
||||||
from observer.observer import Observer
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleView(Observer):
|
|
||||||
def update(self, event):
|
|
||||||
if isinstance(event, str):
|
|
||||||
print(f"[EVENT] {event}")
|
|
||||||
elif isinstance(event, dict):
|
|
||||||
event_type = event.get("type", "unknown")
|
|
||||||
if event_type == "search_finished":
|
|
||||||
stats = event.get("stats")
|
|
||||||
print(f"[EVENT] search finished: {stats}")
|
|
||||||
else:
|
|
||||||
print(f"[EVENT] {event_type}: {event}")
|
|
||||||
else:
|
|
||||||
print("[EVENT] unknown")
|
|
||||||
|
|
||||||
def clear(self):
|
|
||||||
os.system("cls" if os.name == "nt" else "clear")
|
|
||||||
|
|
||||||
def render(self, maze, player_position=None, path=None, clear_screen=False):
|
|
||||||
if clear_screen:
|
|
||||||
self.clear()
|
|
||||||
print(maze.render(player_position=player_position, path=path))
|
|
||||||
print()
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class Observer(ABC):
|
|
||||||
@abstractmethod
|
|
||||||
def update(self, event):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
matplotlib
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import time
|
|
||||||
from core.search_stats import SearchStats
|
|
||||||
|
|
||||||
|
|
||||||
class MazeSolver:
|
|
||||||
def __init__(self, maze, strategy=None):
|
|
||||||
self.maze = maze
|
|
||||||
self.strategy = strategy
|
|
||||||
self.observers = []
|
|
||||||
|
|
||||||
def setStrategy(self, strategy):
|
|
||||||
self.strategy = strategy
|
|
||||||
|
|
||||||
def addObserver(self, observer):
|
|
||||||
if observer not in self.observers:
|
|
||||||
self.observers.append(observer)
|
|
||||||
|
|
||||||
def removeObserver(self, observer):
|
|
||||||
if observer in self.observers:
|
|
||||||
self.observers.remove(observer)
|
|
||||||
|
|
||||||
def notify(self, event):
|
|
||||||
for observer in self.observers:
|
|
||||||
observer.update(event)
|
|
||||||
|
|
||||||
def solve(self):
|
|
||||||
if self.strategy is None:
|
|
||||||
raise ValueError("Strategy is not set")
|
|
||||||
self.notify({"type": "search_started", "strategy": self.strategy.name})
|
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
|
||||||
path = self.strategy.findPath(self.maze, self.maze.startCell, self.maze.exitCell)
|
|
||||||
end_time = time.perf_counter()
|
|
||||||
|
|
||||||
stats = SearchStats(
|
|
||||||
timeMs=(end_time - start_time) * 1000.0,
|
|
||||||
visitedCells=getattr(self.strategy, "visitedCount", 0),
|
|
||||||
pathLength=len(path),
|
|
||||||
path=path,
|
|
||||||
found=bool(path),
|
|
||||||
algorithm=getattr(self.strategy, "name", "")
|
|
||||||
)
|
|
||||||
|
|
||||||
if stats.found:
|
|
||||||
self.notify({"type": "path_found", "strategy": stats.algorithm, "length": stats.pathLength})
|
|
||||||
else:
|
|
||||||
self.notify({"type": "path_not_found", "strategy": stats.algorithm})
|
|
||||||
|
|
||||||
self.notify({"type": "search_finished", "stats": stats})
|
|
||||||
return stats
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
import heapq
|
|
||||||
from strategies.pathfinding_strategy import PathFindingStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class AStarStrategy(PathFindingStrategy):
|
|
||||||
name = "A*"
|
|
||||||
|
|
||||||
def heuristic(self, cell, exitCell):
|
|
||||||
return abs(cell.x - exitCell.x) + abs(cell.y - exitCell.y)
|
|
||||||
|
|
||||||
def findPath(self, maze, start, exitCell):
|
|
||||||
self.visitedCount = 0
|
|
||||||
if start is None or exitCell is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
open_set = []
|
|
||||||
heapq.heappush(open_set, (0, 0, start.x, start.y, start))
|
|
||||||
parent = {}
|
|
||||||
g_score = {(start.x, start.y): 0}
|
|
||||||
closed = set()
|
|
||||||
|
|
||||||
while open_set:
|
|
||||||
f_score, current_g, _, _, current = heapq.heappop(open_set)
|
|
||||||
pos = (current.x, current.y)
|
|
||||||
|
|
||||||
if pos in closed:
|
|
||||||
continue
|
|
||||||
|
|
||||||
closed.add(pos)
|
|
||||||
self.visitedCount += 1
|
|
||||||
|
|
||||||
if current.x == exitCell.x and current.y == exitCell.y:
|
|
||||||
return self._restore_path(parent, start, exitCell)
|
|
||||||
|
|
||||||
for neighbor in maze.getNeighbors(current):
|
|
||||||
npos = (neighbor.x, neighbor.y)
|
|
||||||
tentative_g = current_g + getattr(neighbor, "weight", 1)
|
|
||||||
|
|
||||||
if tentative_g < g_score.get(npos, float("inf")):
|
|
||||||
g_score[npos] = tentative_g
|
|
||||||
parent[npos] = current
|
|
||||||
new_f = tentative_g + self.heuristic(neighbor, exitCell)
|
|
||||||
heapq.heappush(open_set, (new_f, tentative_g, neighbor.x, neighbor.y, neighbor))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
from collections import deque
|
|
||||||
from strategies.pathfinding_strategy import PathFindingStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class BFSStrategy(PathFindingStrategy):
|
|
||||||
name = "BFS"
|
|
||||||
|
|
||||||
def findPath(self, maze, start, exitCell):
|
|
||||||
self.visitedCount = 0
|
|
||||||
if start is None or exitCell is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
queue = deque([start])
|
|
||||||
visited = {(start.x, start.y)}
|
|
||||||
parent = {}
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
current = queue.popleft()
|
|
||||||
self.visitedCount += 1
|
|
||||||
|
|
||||||
if current.x == exitCell.x and current.y == exitCell.y:
|
|
||||||
return self._restore_path(parent, start, exitCell)
|
|
||||||
|
|
||||||
for neighbor in maze.getNeighbors(current):
|
|
||||||
pos = (neighbor.x, neighbor.y)
|
|
||||||
if pos not in visited:
|
|
||||||
visited.add(pos)
|
|
||||||
parent[pos] = current
|
|
||||||
queue.append(neighbor)
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
from strategies.pathfinding_strategy import PathFindingStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class DFSStrategy(PathFindingStrategy):
|
|
||||||
name = "DFS"
|
|
||||||
|
|
||||||
def findPath(self, maze, start, exitCell):
|
|
||||||
self.visitedCount = 0
|
|
||||||
if start is None or exitCell is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
stack = [start]
|
|
||||||
visited = set()
|
|
||||||
parent = {}
|
|
||||||
|
|
||||||
while stack:
|
|
||||||
current = stack.pop()
|
|
||||||
pos = (current.x, current.y)
|
|
||||||
if pos in visited:
|
|
||||||
continue
|
|
||||||
|
|
||||||
visited.add(pos)
|
|
||||||
self.visitedCount += 1
|
|
||||||
|
|
||||||
if current.x == exitCell.x and current.y == exitCell.y:
|
|
||||||
return self._restore_path(parent, start, exitCell)
|
|
||||||
|
|
||||||
neighbors = maze.getNeighbors(current)
|
|
||||||
for neighbor in reversed(neighbors):
|
|
||||||
npos = (neighbor.x, neighbor.y)
|
|
||||||
if npos not in visited:
|
|
||||||
parent[npos] = current
|
|
||||||
stack.append(neighbor)
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
import heapq
|
|
||||||
from strategies.pathfinding_strategy import PathFindingStrategy
|
|
||||||
|
|
||||||
|
|
||||||
class DijkstraStrategy(PathFindingStrategy):
|
|
||||||
name = "Dijkstra"
|
|
||||||
|
|
||||||
def findPath(self, maze, start, exitCell):
|
|
||||||
self.visitedCount = 0
|
|
||||||
if start is None or exitCell is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
pq = [(0, start.x, start.y, start)]
|
|
||||||
dist = {(start.x, start.y): 0}
|
|
||||||
parent = {}
|
|
||||||
closed = set()
|
|
||||||
|
|
||||||
while pq:
|
|
||||||
current_cost, _, _, current = heapq.heappop(pq)
|
|
||||||
pos = (current.x, current.y)
|
|
||||||
|
|
||||||
if pos in closed:
|
|
||||||
continue
|
|
||||||
|
|
||||||
closed.add(pos)
|
|
||||||
self.visitedCount += 1
|
|
||||||
|
|
||||||
if current.x == exitCell.x and current.y == exitCell.y:
|
|
||||||
return self._restore_path(parent, start, exitCell)
|
|
||||||
|
|
||||||
for neighbor in maze.getNeighbors(current):
|
|
||||||
npos = (neighbor.x, neighbor.y)
|
|
||||||
step_cost = getattr(neighbor, "weight", 1)
|
|
||||||
new_cost = current_cost + step_cost
|
|
||||||
|
|
||||||
if new_cost < dist.get(npos, float("inf")):
|
|
||||||
dist[npos] = new_cost
|
|
||||||
parent[npos] = current
|
|
||||||
heapq.heappush(pq, (new_cost, neighbor.x, neighbor.y, neighbor))
|
|
||||||
|
|
||||||
return []
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
|
|
||||||
|
|
||||||
class PathFindingStrategy(ABC):
|
|
||||||
name = "Base"
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.visitedCount = 0
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def findPath(self, maze, start, exitCell):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _restore_path(self, parent, start, exitCell):
|
|
||||||
if exitCell is None or start is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path = []
|
|
||||||
current = exitCell
|
|
||||||
|
|
||||||
while True:
|
|
||||||
path.append(current)
|
|
||||||
if current.x == start.x and current.y == start.y:
|
|
||||||
break
|
|
||||||
current = parent.get((current.x, current.y))
|
|
||||||
if current is None:
|
|
||||||
return []
|
|
||||||
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
def create_node(name, phone):
|
|
||||||
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
|
||||||
|
|
||||||
def bst_insert(root, name, phone):
|
|
||||||
if root is None:
|
|
||||||
return create_node(name, phone)
|
|
||||||
|
|
||||||
if name == root['name']:
|
|
||||||
root['phone'] = phone
|
|
||||||
elif name < root['name']:
|
|
||||||
root['left'] = bst_insert(root['left'], name, phone)
|
|
||||||
else:
|
|
||||||
root['right'] = bst_insert(root['right'], name, phone)
|
|
||||||
return root
|
|
||||||
|
|
||||||
def bst_find(root, name):
|
|
||||||
if root is None:
|
|
||||||
return None
|
|
||||||
if name == root['name']:
|
|
||||||
return root['phone']
|
|
||||||
elif name < root['name']:
|
|
||||||
return bst_find(root['left'], name)
|
|
||||||
else:
|
|
||||||
return bst_find(root['right'], name)
|
|
||||||
|
|
||||||
def _find_min(node):
|
|
||||||
while node['left'] is not None:
|
|
||||||
node = node['left']
|
|
||||||
return node
|
|
||||||
|
|
||||||
def bst_delete(root, name):
|
|
||||||
if root is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if name < root['name']:
|
|
||||||
root['left'] = bst_delete(root['left'], name)
|
|
||||||
elif name > root['name']:
|
|
||||||
root['right'] = bst_delete(root['right'], name)
|
|
||||||
else:
|
|
||||||
if root['left'] is None:
|
|
||||||
return root['right']
|
|
||||||
if root['right'] is None:
|
|
||||||
return root['left']
|
|
||||||
min_node = _find_min(root['right'])
|
|
||||||
root['name'] = min_node['name']
|
|
||||||
root['phone'] = min_node['phone']
|
|
||||||
root['right'] = bst_delete(root['right'], min_node['name'])
|
|
||||||
return root
|
|
||||||
|
|
||||||
def bst_list_all(root):
|
|
||||||
result = []
|
|
||||||
def inorder(node):
|
|
||||||
if node is None:
|
|
||||||
return
|
|
||||||
inorder(node['left'])
|
|
||||||
result.append((node['name'], node['phone']))
|
|
||||||
inorder(node['right'])
|
|
||||||
inorder(root)
|
|
||||||
return result
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
root = None
|
|
||||||
root = bst_insert(root, 'Иван', '123-456')
|
|
||||||
root = bst_insert(root, 'Борис', '789-012')
|
|
||||||
root = bst_insert(root, 'Анна', '345-678')
|
|
||||||
root = bst_insert(root, 'Иван', '111-222')
|
|
||||||
print(bst_list_all(root))
|
|
||||||
print(bst_find(root, 'Иван'))
|
|
||||||
print(bst_find(root, 'Петр'))
|
|
||||||
root = bst_delete(root, 'Борис')
|
|
||||||
print(bst_list_all(root))
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
import random
|
|
||||||
import time
|
|
||||||
import csv
|
|
||||||
import sys
|
|
||||||
sys.setrecursionlimit(20000)
|
|
||||||
|
|
||||||
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
|
|
||||||
from hash_table_phonebook import ht_insert, ht_find, ht_delete, ht_list_all
|
|
||||||
from bst_phonebook import bst_insert, bst_find, bst_delete, bst_list_all
|
|
||||||
|
|
||||||
def generate_records(n, seed=42):
|
|
||||||
random.seed(seed)
|
|
||||||
records = []
|
|
||||||
for i in range(1, n+1):
|
|
||||||
name = f"User_{i:05d}"
|
|
||||||
phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}"
|
|
||||||
records.append((name, phone))
|
|
||||||
return records
|
|
||||||
|
|
||||||
def prepare_datasets(base_records):
|
|
||||||
shuffled = base_records.copy()
|
|
||||||
random.shuffle(shuffled)
|
|
||||||
sorted_records = sorted(base_records, key=lambda x: x[0])
|
|
||||||
return shuffled, sorted_records
|
|
||||||
|
|
||||||
def run_experiment(struct_funcs, records, mode_name, repeats=5):
|
|
||||||
results = []
|
|
||||||
for rep in range(repeats):
|
|
||||||
struct = struct_funcs['create']()
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name, phone in records:
|
|
||||||
struct = struct_funcs['insert'](struct, name, phone)
|
|
||||||
end = time.perf_counter()
|
|
||||||
insert_time = end - start
|
|
||||||
|
|
||||||
existing_names = [name for name, _ in records]
|
|
||||||
sample_existing = random.sample(existing_names, 100)
|
|
||||||
nonexistent = [f"NotExist_{i}" for i in range(10)]
|
|
||||||
search_names = sample_existing + nonexistent
|
|
||||||
random.shuffle(search_names)
|
|
||||||
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name in search_names:
|
|
||||||
_ = struct_funcs['find'](struct, name)
|
|
||||||
end = time.perf_counter()
|
|
||||||
find_time = end - start
|
|
||||||
|
|
||||||
to_delete = random.sample(existing_names, 50)
|
|
||||||
start = time.perf_counter()
|
|
||||||
for name in to_delete:
|
|
||||||
struct = struct_funcs['delete'](struct, name)
|
|
||||||
end = time.perf_counter()
|
|
||||||
delete_time = end - start
|
|
||||||
|
|
||||||
results.append({
|
|
||||||
'structure': struct_funcs['name'],
|
|
||||||
'mode': mode_name,
|
|
||||||
'repetition': rep+1,
|
|
||||||
'insert_time': insert_time,
|
|
||||||
'find_time': find_time,
|
|
||||||
'delete_time': delete_time
|
|
||||||
})
|
|
||||||
return results
|
|
||||||
|
|
||||||
def main():
|
|
||||||
N = 10000
|
|
||||||
base_records = generate_records(N)
|
|
||||||
shuffled, sorted_records = prepare_datasets(base_records)
|
|
||||||
|
|
||||||
structures = {
|
|
||||||
'LinkedList': {
|
|
||||||
'name': 'LinkedList',
|
|
||||||
'create': lambda: None,
|
|
||||||
'insert': ll_insert,
|
|
||||||
'find': ll_find,
|
|
||||||
'delete': ll_delete,
|
|
||||||
'list_all': ll_list_all
|
|
||||||
},
|
|
||||||
'HashTable': {
|
|
||||||
'name': 'HashTable',
|
|
||||||
'create': lambda: [None] * 10,
|
|
||||||
'insert': ht_insert,
|
|
||||||
'find': ht_find,
|
|
||||||
'delete': ht_delete,
|
|
||||||
'list_all': ht_list_all
|
|
||||||
},
|
|
||||||
'BST': {
|
|
||||||
'name': 'BST',
|
|
||||||
'create': lambda: None,
|
|
||||||
'insert': bst_insert,
|
|
||||||
'find': bst_find,
|
|
||||||
'delete': bst_delete,
|
|
||||||
'list_all': bst_list_all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
all_results = []
|
|
||||||
repeats = 5
|
|
||||||
|
|
||||||
for struct_name, funcs in structures.items():
|
|
||||||
print(f"Testing {struct_name} on random order...")
|
|
||||||
res_random = run_experiment(funcs, shuffled, 'random', repeats)
|
|
||||||
all_results.extend(res_random)
|
|
||||||
|
|
||||||
print(f"Testing {struct_name} on sorted order...")
|
|
||||||
res_sorted = run_experiment(funcs, sorted_records, 'sorted', repeats)
|
|
||||||
all_results.extend(res_sorted)
|
|
||||||
|
|
||||||
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.writer(f)
|
|
||||||
writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)'])
|
|
||||||
for r in all_results:
|
|
||||||
writer.writerow([
|
|
||||||
r['structure'],
|
|
||||||
r['mode'],
|
|
||||||
r['repetition'],
|
|
||||||
f"{r['insert_time']:.6f}",
|
|
||||||
f"{r['find_time']:.6f}",
|
|
||||||
f"{r['delete_time']:.6f}"
|
|
||||||
])
|
|
||||||
|
|
||||||
print("Experiment finished. Results saved to experiment_results.csv")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec)
|
|
||||||
LinkedList,random,1,4.432559,0.034196,0.014270
|
|
||||||
LinkedList,random,2,4.999931,0.038043,0.020281
|
|
||||||
LinkedList,random,3,4.771456,0.030191,0.014131
|
|
||||||
LinkedList,random,4,4.707315,0.033500,0.016198
|
|
||||||
LinkedList,random,5,4.721361,0.036586,0.011988
|
|
||||||
LinkedList,sorted,1,4.139028,0.024011,0.010482
|
|
||||||
LinkedList,sorted,2,4.212383,0.024592,0.011765
|
|
||||||
LinkedList,sorted,3,4.674211,0.027756,0.012189
|
|
||||||
LinkedList,sorted,4,4.610210,0.031519,0.012244
|
|
||||||
LinkedList,sorted,5,4.565687,0.029739,0.012747
|
|
||||||
HashTable,random,1,0.659990,0.003889,0.001728
|
|
||||||
HashTable,random,2,0.666055,0.005980,0.002002
|
|
||||||
HashTable,random,3,0.669948,0.004087,0.002176
|
|
||||||
HashTable,random,4,0.661882,0.007439,0.001897
|
|
||||||
HashTable,random,5,0.680420,0.004016,0.001649
|
|
||||||
HashTable,sorted,1,0.648261,0.004277,0.002922
|
|
||||||
HashTable,sorted,2,0.654924,0.004136,0.001793
|
|
||||||
HashTable,sorted,3,0.645509,0.003900,0.002249
|
|
||||||
HashTable,sorted,4,0.637906,0.004056,0.001657
|
|
||||||
HashTable,sorted,5,0.643536,0.003846,0.001741
|
|
||||||
BST,random,1,0.029415,0.000515,0.000183
|
|
||||||
BST,random,2,0.027684,0.000216,0.000142
|
|
||||||
BST,random,3,0.026213,0.000252,0.000159
|
|
||||||
BST,random,4,0.026987,0.000207,0.000135
|
|
||||||
BST,random,5,0.028321,0.000271,0.000183
|
|
||||||
BST,sorted,1,10.293772,0.093178,0.053520
|
|
||||||
BST,sorted,2,10.142204,0.088924,0.049079
|
|
||||||
BST,sorted,3,10.142037,0.078281,0.059416
|
|
||||||
BST,sorted,4,10.139818,0.100162,0.056881
|
|
||||||
BST,sorted,5,10.102982,0.082247,0.051973
|
|
||||||
|
|
|
@ -1,47 +0,0 @@
|
||||||
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
|
|
||||||
|
|
||||||
def hash_function(name, table_size):
|
|
||||||
return hash(name) % table_size
|
|
||||||
|
|
||||||
def ht_insert(buckets, name, phone):
|
|
||||||
idx = hash_function(name, len(buckets))
|
|
||||||
head = buckets[idx]
|
|
||||||
new_head = ll_insert(head, name, phone)
|
|
||||||
buckets[idx] = new_head
|
|
||||||
return buckets
|
|
||||||
|
|
||||||
def ht_find(buckets, name):
|
|
||||||
idx = hash_function(name, len(buckets))
|
|
||||||
head = buckets[idx]
|
|
||||||
return ll_find(head, name)
|
|
||||||
|
|
||||||
def ht_delete(buckets, name):
|
|
||||||
idx = hash_function(name, len(buckets))
|
|
||||||
head = buckets[idx]
|
|
||||||
new_head = ll_delete(head, name)
|
|
||||||
buckets[idx] = new_head
|
|
||||||
return buckets
|
|
||||||
|
|
||||||
def ht_list_all(buckets):
|
|
||||||
all_records = []
|
|
||||||
for head in buckets:
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
all_records.append((current['name'], current['phone']))
|
|
||||||
current = current['next']
|
|
||||||
all_records.sort(key=lambda x: x[0])
|
|
||||||
return all_records
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
SIZE = 5
|
|
||||||
buckets = [None] * SIZE
|
|
||||||
|
|
||||||
ht_insert(buckets, 'Иван', '123-456')
|
|
||||||
ht_insert(buckets, 'Борис', '789-012')
|
|
||||||
ht_insert(buckets, 'Анна', '345-678')
|
|
||||||
ht_insert(buckets, 'Иван', '111-222')
|
|
||||||
print(ht_list_all(buckets))
|
|
||||||
print(ht_find(buckets, 'Анна'))
|
|
||||||
print(ht_find(buckets, 'Петр'))
|
|
||||||
ht_delete(buckets, 'Борис')
|
|
||||||
print(ht_list_all(buckets))
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
def create_node(name, phone):
|
|
||||||
return {'name': name, 'phone': phone, 'next': None}
|
|
||||||
|
|
||||||
def ll_insert(head, name, phone):
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
if current['name'] == name:
|
|
||||||
current['phone'] = phone
|
|
||||||
return head
|
|
||||||
current = current['next']
|
|
||||||
|
|
||||||
new_node = create_node(name, phone)
|
|
||||||
|
|
||||||
if head is None:
|
|
||||||
return new_node
|
|
||||||
|
|
||||||
current = head
|
|
||||||
while current['next'] is not None:
|
|
||||||
current = current['next']
|
|
||||||
current['next'] = new_node
|
|
||||||
return head
|
|
||||||
|
|
||||||
def ll_find(head, name):
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
if current['name'] == name:
|
|
||||||
return current['phone']
|
|
||||||
current = current['next']
|
|
||||||
return None
|
|
||||||
|
|
||||||
def ll_delete(head, name):
|
|
||||||
if head is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
if head['name'] == name:
|
|
||||||
return head['next']
|
|
||||||
|
|
||||||
prev = head
|
|
||||||
current = head['next']
|
|
||||||
while current is not None:
|
|
||||||
if current['name'] == name:
|
|
||||||
prev['next'] = current['next']
|
|
||||||
return head
|
|
||||||
prev = current
|
|
||||||
current = current['next']
|
|
||||||
return head
|
|
||||||
|
|
||||||
def ll_list_all(head):
|
|
||||||
records = []
|
|
||||||
current = head
|
|
||||||
while current is not None:
|
|
||||||
records.append((current['name'], current['phone']))
|
|
||||||
current = current['next']
|
|
||||||
records.sort(key=lambda pair: pair[0])
|
|
||||||
return records
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
head = None
|
|
||||||
head = ll_insert(head, 'Иван', '123-456')
|
|
||||||
head = ll_insert(head, 'Борис', '789-012')
|
|
||||||
head = ll_insert(head, 'Анна', '345-678')
|
|
||||||
head = ll_insert(head, 'Иван', '111-222')
|
|
||||||
print(ll_list_all(head))
|
|
||||||
print(ll_find(head, 'Иван'))
|
|
||||||
print(ll_find(head, 'Петр'))
|
|
||||||
head = ll_delete(head, 'Борис')
|
|
||||||
print(ll_list_all(head))
|
|
||||||
|
Before Width: | Height: | Size: 46 KiB |
|
|
@ -1,39 +0,0 @@
|
||||||
import pandas as pd
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
df = pd.read_csv('experiment_results.csv')
|
|
||||||
|
|
||||||
mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index()
|
|
||||||
|
|
||||||
structures = mean_times['Structure'].unique()
|
|
||||||
modes = mean_times['Mode'].unique()
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
||||||
|
|
||||||
operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)']
|
|
||||||
titles = ['Insertion', 'Search', 'Deletion']
|
|
||||||
|
|
||||||
for ax, op, title in zip(axes, operations, titles):
|
|
||||||
x = np.arange(len(structures))
|
|
||||||
width = 0.35
|
|
||||||
|
|
||||||
random_vals = []
|
|
||||||
sorted_vals = []
|
|
||||||
for s in structures:
|
|
||||||
random_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')]
|
|
||||||
sorted_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')]
|
|
||||||
random_vals.append(random_row[op].values[0] if not random_row.empty else 0)
|
|
||||||
sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0)
|
|
||||||
|
|
||||||
ax.bar(x - width/2, random_vals, width, label='Random')
|
|
||||||
ax.bar(x + width/2, sorted_vals, width, label='Sorted')
|
|
||||||
ax.set_xticks(x)
|
|
||||||
ax.set_xticklabels(structures)
|
|
||||||
ax.set_ylabel('Time (seconds)')
|
|
||||||
ax.set_title(title)
|
|
||||||
ax.legend()
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig('performance_comparison.png', dpi=150)
|
|
||||||
plt.show()
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
maze,strategy,time_ms,visited_cells,path_length
|
|
||||||
Small 10x6,BFS,0.05722500009142095,25.0,16.0
|
|
||||||
Small 10x6,DFS,0.05680966667872175,24.0,16.0
|
|
||||||
Small 10x6,AStar,0.04801966664066034,23.0,16.0
|
|
||||||
Medium 10x10,BFS,0.04772166676048073,47.0,16.0
|
|
||||||
Medium 10x10,DFS,0.034641333362136116,44.0,30.0
|
|
||||||
Medium 10x10,AStar,0.0983669999641279,47.0,16.0
|
|
||||||
Large 20x20,BFS,0.09949400002066493,100.0,36.0
|
|
||||||
Large 20x20,DFS,0.07004933331700158,75.0,68.0
|
|
||||||
Large 20x20,AStar,0.16450733316257052,85.0,36.0
|
|
||||||
Empty 15x15,BFS,0.13264433331035738,133.0,17.0
|
|
||||||
Empty 15x15,DFS,0.11371733338213137,161.0,89.0
|
|
||||||
Empty 15x15,AStar,0.1543506666621397,65.0,17.0
|
|
||||||
No exit 10x10,BFS,0.04392100011803753,25.0,0.0
|
|
||||||
No exit 10x10,DFS,0.05871466661725814,25.0,0.0
|
|
||||||
No exit 10x10,AStar,0.046440666665148456,25.0,0.0
|
|
||||||
|
|
|
@ -1,438 +0,0 @@
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from collections import deque
|
|
||||||
import heapq
|
|
||||||
import time
|
|
||||||
import csv
|
|
||||||
import matplotlib.pyplot as plt
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
class GridPoint:
|
|
||||||
def __init__(self, x, y):
|
|
||||||
self.x = x
|
|
||||||
self.y = y
|
|
||||||
self.blocked = False
|
|
||||||
self.is_start = False
|
|
||||||
self.is_exit = False
|
|
||||||
|
|
||||||
def can_step(self):
|
|
||||||
return not self.blocked
|
|
||||||
|
|
||||||
class Labyrinth:
|
|
||||||
def __init__(self, w, h):
|
|
||||||
self.w = w
|
|
||||||
self.h = h
|
|
||||||
self.grid = [[GridPoint(x, y) for x in range(w)] for y in range(h)]
|
|
||||||
self.start_point = None
|
|
||||||
self.exit_point = None
|
|
||||||
|
|
||||||
def get_point(self, x, y):
|
|
||||||
if 0 <= x < self.w and 0 <= y < self.h:
|
|
||||||
return self.grid[y][x]
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_point(self, x, y, typ):
|
|
||||||
p = self.get_point(x, y)
|
|
||||||
if not p:
|
|
||||||
return
|
|
||||||
if typ == 'wall':
|
|
||||||
p.blocked = True
|
|
||||||
elif typ == 'start':
|
|
||||||
if self.start_point:
|
|
||||||
self.start_point.is_start = False
|
|
||||||
p.is_start = True
|
|
||||||
p.blocked = False
|
|
||||||
self.start_point = p
|
|
||||||
elif typ == 'exit':
|
|
||||||
if self.exit_point:
|
|
||||||
self.exit_point.is_exit = False
|
|
||||||
p.is_exit = True
|
|
||||||
p.blocked = False
|
|
||||||
self.exit_point = p
|
|
||||||
elif typ == 'path':
|
|
||||||
p.blocked = False
|
|
||||||
|
|
||||||
def neighbors(self, p):
|
|
||||||
dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
|
||||||
res = []
|
|
||||||
for dx, dy in dirs:
|
|
||||||
nx, ny = p.x + dx, p.y + dy
|
|
||||||
nb = self.get_point(nx, ny)
|
|
||||||
if nb and nb.can_step():
|
|
||||||
res.append(nb)
|
|
||||||
return res
|
|
||||||
|
|
||||||
class MazeLoader:
|
|
||||||
def load(self, filename):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
class TextMazeLoader(MazeLoader):
|
|
||||||
def load(self, filename):
|
|
||||||
with open(filename, 'r') as f:
|
|
||||||
lines = [line.rstrip('\n') for line in f]
|
|
||||||
h = len(lines)
|
|
||||||
w = max(len(line) for line in lines) if h > 0 else 0
|
|
||||||
start_cnt = 0
|
|
||||||
exit_cnt = 0
|
|
||||||
lab = Labyrinth(w, h)
|
|
||||||
|
|
||||||
for y, line in enumerate(lines):
|
|
||||||
for x, ch in enumerate(line):
|
|
||||||
if ch == '#':
|
|
||||||
lab.set_point(x, y, 'wall')
|
|
||||||
elif ch == 'S':
|
|
||||||
lab.set_point(x, y, 'start')
|
|
||||||
start_cnt += 1
|
|
||||||
elif ch == 'E':
|
|
||||||
lab.set_point(x, y, 'exit')
|
|
||||||
exit_cnt += 1
|
|
||||||
else:
|
|
||||||
lab.set_point(x, y, 'path')
|
|
||||||
if start_cnt != 1 or exit_cnt != 1:
|
|
||||||
raise ValueError(f"Need exactly one S and one E. Found S={start_cnt}, E={exit_cnt}")
|
|
||||||
return lab
|
|
||||||
|
|
||||||
class SearchAlgorithm:
|
|
||||||
def find_way(self, lab, start, goal):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
def _build_path(self, prev, start, goal):
|
|
||||||
path = []
|
|
||||||
cur = goal
|
|
||||||
while cur:
|
|
||||||
path.append(cur)
|
|
||||||
cur = prev.get(cur)
|
|
||||||
path.reverse()
|
|
||||||
return path
|
|
||||||
|
|
||||||
def get_visited(self):
|
|
||||||
return getattr(self, '_visited', 0)
|
|
||||||
|
|
||||||
class BreadthFirst(SearchAlgorithm):
|
|
||||||
def find_way(self, lab, start, goal):
|
|
||||||
q = deque([start])
|
|
||||||
prev = {start: None}
|
|
||||||
seen = {start}
|
|
||||||
while q:
|
|
||||||
cur = q.popleft()
|
|
||||||
if cur == goal:
|
|
||||||
self._visited = len(seen)
|
|
||||||
return self._build_path(prev, start, goal)
|
|
||||||
for nb in lab.neighbors(cur):
|
|
||||||
if nb not in seen:
|
|
||||||
seen.add(nb)
|
|
||||||
prev[nb] = cur
|
|
||||||
q.append(nb)
|
|
||||||
self._visited = len(seen)
|
|
||||||
return []
|
|
||||||
|
|
||||||
class DepthFirst(SearchAlgorithm):
|
|
||||||
def find_way(self, lab, start, goal):
|
|
||||||
stack = [start]
|
|
||||||
prev = {start: None}
|
|
||||||
seen = {start}
|
|
||||||
while stack:
|
|
||||||
cur = stack.pop()
|
|
||||||
if cur == goal:
|
|
||||||
self._visited = len(seen)
|
|
||||||
return self._build_path(prev, start, goal)
|
|
||||||
for nb in lab.neighbors(cur):
|
|
||||||
if nb not in seen:
|
|
||||||
seen.add(nb)
|
|
||||||
prev[nb] = cur
|
|
||||||
stack.append(nb)
|
|
||||||
self._visited = len(seen)
|
|
||||||
return []
|
|
||||||
|
|
||||||
class AStar(SearchAlgorithm):
|
|
||||||
def _dist(self, a, b):
|
|
||||||
return abs(a.x - b.x) + abs(a.y - b.y)
|
|
||||||
|
|
||||||
def find_way(self, lab, start, goal):
|
|
||||||
heap = []
|
|
||||||
cnt = 0
|
|
||||||
start_f = self._dist(start, goal)
|
|
||||||
heapq.heappush(heap, (start_f, cnt, start))
|
|
||||||
cnt += 1
|
|
||||||
prev = {}
|
|
||||||
g = {start: 0}
|
|
||||||
f = {start: start_f}
|
|
||||||
seen = set()
|
|
||||||
while heap:
|
|
||||||
cur_f, _, cur = heapq.heappop(heap)
|
|
||||||
seen.add(cur)
|
|
||||||
if cur == goal:
|
|
||||||
self._visited = len(seen)
|
|
||||||
return self._build_path(prev, start, goal)
|
|
||||||
if cur_f > f.get(cur, float('inf')):
|
|
||||||
continue
|
|
||||||
for nb in lab.neighbors(cur):
|
|
||||||
new_g = g[cur] + 1
|
|
||||||
if new_g < g.get(nb, float('inf')):
|
|
||||||
prev[nb] = cur
|
|
||||||
g[nb] = new_g
|
|
||||||
new_f = new_g + self._dist(nb, goal)
|
|
||||||
f[nb] = new_f
|
|
||||||
heapq.heappush(heap, (new_f, cnt, nb))
|
|
||||||
cnt += 1
|
|
||||||
self._visited = len(seen)
|
|
||||||
return []
|
|
||||||
|
|
||||||
class LabyrinthSolver:
|
|
||||||
def __init__(self, lab):
|
|
||||||
self.lab = lab
|
|
||||||
self.algorithm = None
|
|
||||||
|
|
||||||
def set_algorithm(self, algo):
|
|
||||||
self.algorithm = algo
|
|
||||||
|
|
||||||
def solve(self):
|
|
||||||
if not self.algorithm:
|
|
||||||
return None
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
path = self.algorithm.find_way(self.lab, self.lab.start_point, self.lab.exit_point)
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
ms = (t1 - t0) * 1000
|
|
||||||
return ms, self.algorithm.get_visited(), len(path)
|
|
||||||
|
|
||||||
class Player:
|
|
||||||
def __init__(self, start, lab):
|
|
||||||
self.current = start
|
|
||||||
self.last = None
|
|
||||||
self.lab = lab
|
|
||||||
|
|
||||||
def move(self, cell):
|
|
||||||
if cell and cell.can_step():
|
|
||||||
self.last = self.current
|
|
||||||
self.current = cell
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def undo(self):
|
|
||||||
if self.last:
|
|
||||||
self.current, self.last = self.last, None
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class Command:
|
|
||||||
def do(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
def revert(self):
|
|
||||||
raise NotImplementedError
|
|
||||||
|
|
||||||
class MoveCommand(Command):
|
|
||||||
def __init__(self, player, dx, dy, lab):
|
|
||||||
self.player = player
|
|
||||||
self.dx = dx
|
|
||||||
self.dy = dy
|
|
||||||
self.lab = lab
|
|
||||||
self.done = False
|
|
||||||
|
|
||||||
def do(self):
|
|
||||||
nx = self.player.current.x + self.dx
|
|
||||||
ny = self.player.current.y + self.dy
|
|
||||||
target = self.lab.get_point(nx, ny)
|
|
||||||
if target and target.can_step():
|
|
||||||
self.player.move(target)
|
|
||||||
self.done = True
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def revert(self):
|
|
||||||
if self.done:
|
|
||||||
self.player.undo()
|
|
||||||
self.done = False
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
class InteractiveView:
|
|
||||||
def __init__(self, lab, player):
|
|
||||||
self.lab = lab
|
|
||||||
self.player = player
|
|
||||||
|
|
||||||
def render(self):
|
|
||||||
os.system('cls' if os.name == 'nt' else 'clear')
|
|
||||||
print("=" * (self.lab.w * 2 + 4))
|
|
||||||
print(" LABYRINTH (P = player)")
|
|
||||||
print("=" * (self.lab.w * 2 + 4))
|
|
||||||
for y in range(self.lab.h):
|
|
||||||
print(" ", end='')
|
|
||||||
for x in range(self.lab.w):
|
|
||||||
p = self.lab.get_point(x, y)
|
|
||||||
if self.player.current == p:
|
|
||||||
print('P', end=' ')
|
|
||||||
elif p == self.lab.start_point:
|
|
||||||
print('S', end=' ')
|
|
||||||
elif p == self.lab.exit_point:
|
|
||||||
print('E', end=' ')
|
|
||||||
elif p.blocked:
|
|
||||||
print('#', end=' ')
|
|
||||||
else:
|
|
||||||
print('.', end=' ')
|
|
||||||
print()
|
|
||||||
print("=" * (self.lab.w * 2 + 4))
|
|
||||||
print(f" Position: ({self.player.current.x},{self.player.current.y})")
|
|
||||||
print(" Controls: h(left) j(down) k(up) l(right) u=undo q=quit")
|
|
||||||
print(" Auto-search: b=BFS d=DFS a=A*")
|
|
||||||
|
|
||||||
def run_experiment(maze_file, algo, runs=5):
|
|
||||||
loader = TextMazeLoader()
|
|
||||||
lab = loader.load(maze_file)
|
|
||||||
total_ms = 0
|
|
||||||
total_visited = 0
|
|
||||||
total_len = 0
|
|
||||||
for _ in range(runs):
|
|
||||||
solver = LabyrinthSolver(lab)
|
|
||||||
solver.set_algorithm(algo)
|
|
||||||
stats = solver.solve()
|
|
||||||
if stats:
|
|
||||||
ms, vis, plen = stats
|
|
||||||
total_ms += ms
|
|
||||||
total_visited += vis
|
|
||||||
total_len += plen
|
|
||||||
return total_ms / runs, total_visited / runs, total_len / runs
|
|
||||||
|
|
||||||
def generate_plots(results):
|
|
||||||
mazes = list(set([r['maze'] for r in results]))
|
|
||||||
strategies = ['BFS', 'DFS', 'AStar']
|
|
||||||
|
|
||||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
|
||||||
x = np.arange(len(mazes))
|
|
||||||
width = 0.25
|
|
||||||
|
|
||||||
for i, strat in enumerate(strategies):
|
|
||||||
times = []
|
|
||||||
for maze in mazes:
|
|
||||||
val = next((r['time_ms'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
|
||||||
times.append(val)
|
|
||||||
axes[0].bar(x + i*width, times, width, label=strat)
|
|
||||||
axes[0].set_xlabel('Maze')
|
|
||||||
axes[0].set_ylabel('Time (ms)')
|
|
||||||
axes[0].set_title('Execution Time')
|
|
||||||
axes[0].set_xticks(x + width)
|
|
||||||
axes[0].set_xticklabels(mazes, rotation=45, ha='right')
|
|
||||||
axes[0].legend()
|
|
||||||
axes[0].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
for i, strat in enumerate(strategies):
|
|
||||||
visited = []
|
|
||||||
for maze in mazes:
|
|
||||||
val = next((r['visited_cells'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
|
||||||
visited.append(val)
|
|
||||||
axes[1].bar(x + i*width, visited, width, label=strat)
|
|
||||||
axes[1].set_xlabel('Maze')
|
|
||||||
axes[1].set_ylabel('Visited Cells')
|
|
||||||
axes[1].set_title('Visited Cells')
|
|
||||||
axes[1].set_xticks(x + width)
|
|
||||||
axes[1].set_xticklabels(mazes, rotation=45, ha='right')
|
|
||||||
axes[1].legend()
|
|
||||||
axes[1].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
for i, strat in enumerate(strategies):
|
|
||||||
lengths = []
|
|
||||||
for maze in mazes:
|
|
||||||
val = next((r['path_length'] for r in results if r['maze'] == maze and r['strategy'] == strat), 0)
|
|
||||||
lengths.append(val)
|
|
||||||
axes[2].bar(x + i*width, lengths, width, label=strat)
|
|
||||||
axes[2].set_xlabel('Maze')
|
|
||||||
axes[2].set_ylabel('Path Length')
|
|
||||||
axes[2].set_title('Path Length')
|
|
||||||
axes[2].set_xticks(x + width)
|
|
||||||
axes[2].set_xticklabels(mazes, rotation=45, ha='right')
|
|
||||||
axes[2].legend()
|
|
||||||
axes[2].grid(True, alpha=0.3)
|
|
||||||
|
|
||||||
plt.tight_layout()
|
|
||||||
plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
|
|
||||||
plt.show()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
if len(sys.argv) > 1 and sys.argv[1] == 'experiment':
|
|
||||||
print("Running experiments on all mazes...")
|
|
||||||
maze_files = [
|
|
||||||
("maze/maze1.txt", "Small 10x6"),
|
|
||||||
("maze/maze10x10.txt", "Medium 10x10"),
|
|
||||||
("maze/maze20x20.txt", "Large 20x20"),
|
|
||||||
("maze/maze_empty.txt", "Empty 15x15"),
|
|
||||||
("maze/maze_no_exit.txt", "No exit 10x10")
|
|
||||||
]
|
|
||||||
algorithms = [
|
|
||||||
("BFS", BreadthFirst()),
|
|
||||||
("DFS", DepthFirst()),
|
|
||||||
("AStar", AStar())
|
|
||||||
]
|
|
||||||
results = []
|
|
||||||
for fname, label in maze_files:
|
|
||||||
print(f"Testing {label}...")
|
|
||||||
for aname, algo in algorithms:
|
|
||||||
try:
|
|
||||||
avg_t, avg_v, avg_l = run_experiment(fname, algo, runs=3)
|
|
||||||
results.append({
|
|
||||||
'maze': label,
|
|
||||||
'strategy': aname,
|
|
||||||
'time_ms': avg_t,
|
|
||||||
'visited_cells': avg_v,
|
|
||||||
'path_length': avg_l
|
|
||||||
})
|
|
||||||
print(f" {aname}: time={avg_t:.3f}ms visited={avg_v:.0f} length={avg_l:.0f}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" {aname}: ERROR {e}")
|
|
||||||
# save csv
|
|
||||||
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length'])
|
|
||||||
writer.writeheader()
|
|
||||||
writer.writerows(results)
|
|
||||||
generate_plots(results)
|
|
||||||
print("Done. Results saved to experiment_results.csv and performance_comparison.png")
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
# else interactive mode
|
|
||||||
loader = TextMazeLoader()
|
|
||||||
lab = loader.load("maze/maze1.txt")
|
|
||||||
player = Player(lab.start_point, lab)
|
|
||||||
view = InteractiveView(lab, player)
|
|
||||||
view.render()
|
|
||||||
|
|
||||||
solver = LabyrinthSolver(lab)
|
|
||||||
history = []
|
|
||||||
|
|
||||||
while True:
|
|
||||||
key = input("\n > ").lower()
|
|
||||||
if key == 'q':
|
|
||||||
print("Goodbye!")
|
|
||||||
break
|
|
||||||
elif key == 'b':
|
|
||||||
solver.set_algorithm(BreadthFirst())
|
|
||||||
ms, vis, plen = solver.solve()
|
|
||||||
print(f"BFS: {ms:.3f}ms, visited={vis}, length={plen}")
|
|
||||||
elif key == 'd':
|
|
||||||
solver.set_algorithm(DepthFirst())
|
|
||||||
ms, vis, plen = solver.solve()
|
|
||||||
print(f"DFS: {ms:.3f}ms, visited={vis}, length={plen}")
|
|
||||||
elif key == 'a':
|
|
||||||
solver.set_algorithm(AStar())
|
|
||||||
ms, vis, plen = solver.solve()
|
|
||||||
print(f"A*: {ms:.3f}ms, visited={vis}, length={plen}")
|
|
||||||
elif key in ('h','j','k','l'):
|
|
||||||
moves = {'h': (-1,0), 'l': (1,0), 'k': (0,-1), 'j': (0,1)}
|
|
||||||
dx, dy = moves[key]
|
|
||||||
cmd = MoveCommand(player, dx, dy, lab)
|
|
||||||
if cmd.do():
|
|
||||||
history.append(cmd)
|
|
||||||
view.render()
|
|
||||||
if player.current == lab.exit_point:
|
|
||||||
print("\n*** YOU REACHED THE EXIT! ***")
|
|
||||||
print(f"Total moves: {len(history)}")
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("Can't go there - wall!")
|
|
||||||
elif key == 'u':
|
|
||||||
if history:
|
|
||||||
cmd = history.pop()
|
|
||||||
cmd.revert()
|
|
||||||
view.render()
|
|
||||||
print("Undo last move")
|
|
||||||
else:
|
|
||||||
print("Nothing to undo")
|
|
||||||
else:
|
|
||||||
print("Unknown command")
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
##########
|
|
||||||
#S #
|
|
||||||
# ####### #
|
|
||||||
# # # #
|
|
||||||
# # ### # #
|
|
||||||
# # E #
|
|
||||||
##########
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
##########
|
|
||||||
#S #
|
|
||||||
# # #### #
|
|
||||||
# # #
|
|
||||||
# #### # #
|
|
||||||
# # #
|
|
||||||
# #### # #
|
|
||||||
# # #
|
|
||||||
# #
|
|
||||||
########E#
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
####################
|
|
||||||
#S #
|
|
||||||
# ############### #
|
|
||||||
# # # #
|
|
||||||
# # ######### # # #
|
|
||||||
# # # # # # #
|
|
||||||
# # # ##### # # # #
|
|
||||||
# # # # # # # # #
|
|
||||||
# # # # # # # # # #
|
|
||||||
# # # # # # # # #
|
|
||||||
# # # ##### # # # #
|
|
||||||
# # # # # # #
|
|
||||||
# # ######### # # #
|
|
||||||
# # # #
|
|
||||||
# ############### #
|
|
||||||
# #
|
|
||||||
# ############### #
|
|
||||||
# # # #
|
|
||||||
# # ########### # #
|
|
||||||
# E#
|
|
||||||
####################
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
###############
|
|
||||||
#S #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# #
|
|
||||||
# E #
|
|
||||||
###############
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
##########
|
|
||||||
#S #
|
|
||||||
# # #
|
|
||||||
# # #### #
|
|
||||||
# # #
|
|
||||||
##########
|
|
||||||
E#########
|
|
||||||
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 46 KiB |
|
|
@ -1,94 +0,0 @@
|
||||||
# Отчёт по лабораторной работе «Структуры данных для телефонного справочника»
|
|
||||||
|
|
||||||
## 1. Постановка задачи
|
|
||||||
|
|
||||||
В рамках работы требовалось реализовать три структуры данных «с нуля» (без использования встроенных коллекций, кроме базовых списков):
|
|
||||||
|
|
||||||
- связный список,
|
|
||||||
- хеш-таблицу с цепочками,
|
|
||||||
- двоичное дерево поиска (несбалансированное).
|
|
||||||
|
|
||||||
Для каждой структуры необходимо реализовать операции `insert`, `find`, `delete` и `list_all` (возврат всех записей, отсортированных по имени). Затем на наборе из 10 000 записей выполнить экспериментальное сравнение производительности в двух режимах: при случайном порядке вставки и при вставке записей, отсортированных по имени. Каждый эксперимент повторялся 5 раз.
|
|
||||||
|
|
||||||
## 2. Результаты измерений
|
|
||||||
|
|
||||||
Ниже приведены усреднённые по 5 повторам времена выполнения операций (в секундах). Исходные сырые данные сохранены в файле `experiment_results.csv`.
|
|
||||||
|
|
||||||
| Структура | Режим | Вставка (с) | Поиск 110 имён (с) | Удаление 50 записей (с) |
|
|
||||||
|-------------|-------------|-------------|--------------------|-------------------------|
|
|
||||||
| LinkedList | случайный | 4.7265 | 0.0345 | 0.0154 |
|
|
||||||
| LinkedList | сортир. | 4.4403 | 0.0275 | 0.0119 |
|
|
||||||
| HashTable | случайный | 0.6677 | 0.0051 | 0.0019 |
|
|
||||||
| HashTable | сортир. | 0.6460 | 0.0040 | 0.0021 |
|
|
||||||
| BST | случайный | 0.0277 | 0.00029 | 0.00016 |
|
|
||||||
| BST | сортир. | 10.1642 | 0.0886 | 0.0542 |
|
|
||||||
|
|
||||||
### Примечания к методике
|
|
||||||
|
|
||||||
- **Вставка** – добавление всех 10 000 записей в пустую структуру.
|
|
||||||
- **Поиск** – 100 заведомо существующих имён + 10 несуществующих (общее количество вызовов 110).
|
|
||||||
- **Удаление** – 50 случайных существующих записей.
|
|
||||||
- Все замеры выполнены с помощью `time.perf_counter()`.
|
|
||||||
- Для хеш-таблицы использовалось 10 корзин.
|
|
||||||
- Рекурсивная глубина BST увеличена до 20 000, чтобы избежать переполнения стека.
|
|
||||||
|
|
||||||
## 3. Анализ полученных данных
|
|
||||||
|
|
||||||
### 3.1. Поведение BST при разных порядках ввода
|
|
||||||
|
|
||||||
Двоичное дерево поиска сильно зависит от порядка поступления ключей. При случайном порядке средняя высота близка к логарифмической, что даёт отличную производительность:
|
|
||||||
|
|
||||||
- вставка – **0.0277 с**,
|
|
||||||
- поиск – **0.00029 с** (самый быстрый среди всех структур в этом режиме).
|
|
||||||
|
|
||||||
Однако при вставке отсортированных данных дерево вырождается в линейный список (каждый новый узел добавляется только в правое поддерево). Последствия:
|
|
||||||
|
|
||||||
- время вставки возрастает **в 367 раз** (с 0.0277 до 10.16 с),
|
|
||||||
- поиск замедляется **в 305 раз**,
|
|
||||||
- удаление – **в 339 раз**.
|
|
||||||
|
|
||||||
Вырожденное BST на отсортированных данных работает **медленнее даже связного списка** (вставка 10.16 с против 4.44 с, поиск 0.088 с против 0.027 с), что объясняется накладными расходами на рекурсивные вызовы и проверки.
|
|
||||||
|
|
||||||
### 3.2. Хеш-таблица – устойчивость к порядку
|
|
||||||
|
|
||||||
Хеш-таблица использует функцию `hash(name) % size`, которая равномерно рассеивает имена независимо от их лексикографического порядка. Поэтому результаты в двух режимах практически идентичны:
|
|
||||||
|
|
||||||
- вставка: 0.668 с (случайный) против 0.646 с (отсортированный) – разница менее 4%,
|
|
||||||
- поиск: 0.0051 с против 0.0040 с,
|
|
||||||
- удаление: 0.0019 с против 0.0021 с.
|
|
||||||
|
|
||||||
Небольшие расхождения находятся в пределах случайной вариации (зависит от коллизий, которые немного различаются при разном порядке вставки). Средняя сложность операций остаётся **O(1)**.
|
|
||||||
|
|
||||||
### 3.3. Связный список – ожидаемо медленный
|
|
||||||
|
|
||||||
Линейный список не обеспечивает прямого доступа, поэтому все операции (кроме удаления после нахождения) требуют обхода в среднем половины списка. Даже при сравнительно небольшом объёме данных (10 000 записей) времена велики:
|
|
||||||
|
|
||||||
- вставка ≈ **4.6 с** (на два порядка хуже, чем у хеш-таблицы и BST на случайных данных),
|
|
||||||
- поиск ≈ **0.03 с** (в 6–10 раз медленнее, чем у других структур).
|
|
||||||
|
|
||||||
Интересно, что на отсортированных данных список показывает немного лучшее время, чем на случайных. Причина: при вставке в конец отсортированного списка (имена идут в алфавитном порядке) новые узлы добавляются без поиска дубликатов? Но алгоритм `ll_insert` сначала проверяет наличие имени, проходя весь список. Поскольку все имена уникальны и не обновляются, каждый проход идёт до конца. Однако в отсортированном режиме имена добавляются в порядке возрастания, и при проверке дубликата мы проходим по уже существующим элементам, которые все меньше нового? Да, в отсортированном режиме каждое новое имя больше всех предыдущих, поэтому при поиске дубликата мы обходим весь существующий список. В случайном режиме новые имена могут встречаться раньше, и поиск останавливается раньше? Но в любом случае разница небольшая (около 6%), и в целом список остаётся медленным.
|
|
||||||
|
|
||||||
### 3.4. Сравнение удаления
|
|
||||||
|
|
||||||
Удаление в списке требует сначала найти элемент (O(n)), затем перелинковку. В хеш-таблице удаление сводится к удалению в коротком списке корзины (почти O(1)). В BST на случайных данных удаление очень быстрое (0.00016 с), на отсортированных – катастрофически замедляется (0.054 с). Для хеш-таблицы удаление немного быстрее, чем вставка, что естественно: при удалении не нужно создавать новый узел.
|
|
||||||
|
|
||||||
## 4. Выводы и практические рекомендации
|
|
||||||
|
|
||||||
Проведённое исследование наглядно демонстрирует сильные и слабые стороны каждой структуры.
|
|
||||||
|
|
||||||
1. **Хеш-таблица** – лучший выбор для задач, где приоритетом является скорость всех операций (вставка, поиск, удаление), а порядок вывода данных не важен или может быть получен отдельной сортировкой. Стабильно высокая производительность вне зависимости от характера входных данных. В реальных проектах именно хеш-таблицы лежат в основе словарей (Python `dict`, Java `HashMap`).
|
|
||||||
|
|
||||||
2. **Двоичное дерево поиска** – эффективно только при случайном или близком к случайному порядке поступления ключей. Даёт логарифмическую сложность и при этом позволяет получать данные в отсортированном виде за O(n) без дополнительной сортировки. Однако на реальных данных (например, заведомо отсортированных) производительность падает до O(n), что делает его непригодным без механизмов балансировки. На практике применяются сбалансированные варианты (AVL, красно-чёрные деревья).
|
|
||||||
|
|
||||||
3. **Связный список** – не подходит для коллекций объёмом более нескольких сотен элементов из-за линейной сложности основных операций. Может использоваться только в очень специфических сценариях: очень редкий поиск, постоянные вставки/удаления в начало (но не в конец), или как строительный блок для других структур (например, для цепочек в хеш-таблице, что и было сделано в данной работе).
|
|
||||||
|
|
||||||
### Итоговая таблица применимости
|
|
||||||
|
|
||||||
| Критерий | Рекомендуемая структура |
|
|
||||||
|---------------------------------|---------------------------------------|
|
|
||||||
| Максимальная скорость всех операций | Хеш-таблица |
|
|
||||||
| Нужны данные в отсортированном порядке + данные поступают случайно | BST (но лучше сбалансированное) |
|
|
||||||
| Данные поступают уже отсортированными | Хеш-таблица (или балансируемое дерево) |
|
|
||||||
| Очень маленький объём (< 100 записей) | Любая, но проще список |
|
|
||||||
|
|
||||||
В реальной разработке для телефонного справочника с большим числом записей и частыми запросами поиска оптимальным решением будет **хеш-таблица**. Если же дополнительно требуется частый вывод всего справочника по алфавиту, стоит рассмотреть сбалансированное дерево (например, встроенный в Python модуль `bisect` не даёт структуры данных, а `sortedcontainers` – сторонний).
|
|
||||||