forked from UNN/2026-rff_mp
Merge pull request '[1] lab1, lab2' (#251) from pogodinda/2026-rff_mp:pogodinda into develop
Reviewed-on: UNN/2026-rff_mp#251
This commit is contained in:
commit
bcdaf65dbc
144
pogodinda/lab1/benchmark.py
Normal file
144
pogodinda/lab1/benchmark.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import time
|
||||
import random
|
||||
import csv
|
||||
import sys
|
||||
from linked_list_phonebook import *
|
||||
from hash_table_phonebook import *
|
||||
from bst_phonebook import *
|
||||
|
||||
sys.setrecursionlimit(100000)
|
||||
|
||||
def generate_test_data(n=10000):
|
||||
"""Генерация тестовых данных"""
|
||||
uniform_records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)]
|
||||
|
||||
shuffled_records = uniform_records.copy()
|
||||
random.shuffle(shuffled_records)
|
||||
|
||||
sorted_records = sorted(uniform_records, key=lambda x: x[0])
|
||||
|
||||
existing_names = [f"User_{i:05d}" for i in random.sample(range(n), 100)]
|
||||
non_existing_names = [f"None_{i:05d}" for i in range(10)]
|
||||
search_names = existing_names + non_existing_names
|
||||
|
||||
delete_names = [f"User_{i:05d}" for i in random.sample(range(n), 50)]
|
||||
|
||||
return {
|
||||
'shuffled': shuffled_records,
|
||||
'sorted': sorted_records,
|
||||
'search_names': search_names,
|
||||
'delete_names': delete_names
|
||||
}
|
||||
|
||||
def run_benchmarks():
|
||||
print("Генерация тестовых данных...")
|
||||
N = 10000
|
||||
test_data = generate_test_data(N)
|
||||
|
||||
results = []
|
||||
|
||||
structures = [
|
||||
('LinkedList', 'll'),
|
||||
('HashTable', 'ht'),
|
||||
('BST', 'bst')
|
||||
]
|
||||
|
||||
modes = [
|
||||
('случайный', test_data['shuffled']),
|
||||
('отсортированный', test_data['sorted'])
|
||||
]
|
||||
|
||||
REPEATS = 5 # Количество повторов
|
||||
|
||||
for struct_name, struct_type in structures:
|
||||
print(f"\n=== Тестирование {struct_name} ===")
|
||||
|
||||
for mode_name, records in modes:
|
||||
print(f" Режим: {mode_name}")
|
||||
|
||||
# Запускаем 5 повторов для каждой комбинации
|
||||
for rep in range(1, REPEATS + 1):
|
||||
print(f" Повтор {rep}/{REPEATS}...")
|
||||
|
||||
# Создаем структуру и меряем вставку
|
||||
if struct_type == 'll':
|
||||
structure = None
|
||||
start = time.perf_counter()
|
||||
for name, phone in records:
|
||||
structure = ll_insert(structure, name, phone)
|
||||
end = time.perf_counter()
|
||||
insert_time = end - start
|
||||
|
||||
elif struct_type == 'ht':
|
||||
structure = create_hash_table(5000)
|
||||
start = time.perf_counter()
|
||||
for name, phone in records:
|
||||
ht_insert(structure, name, phone)
|
||||
end = time.perf_counter()
|
||||
insert_time = end - start
|
||||
|
||||
elif struct_type == 'bst':
|
||||
structure = None
|
||||
start = time.perf_counter()
|
||||
for name, phone in records:
|
||||
structure = bst_insert(structure, name, phone)
|
||||
end = time.perf_counter()
|
||||
insert_time = end - start
|
||||
|
||||
results.append([struct_name, mode_name, "вставка", insert_time, rep])
|
||||
|
||||
# Поиск
|
||||
start = time.perf_counter()
|
||||
for name in test_data['search_names']:
|
||||
if struct_type == 'll':
|
||||
ll_find(structure, name)
|
||||
elif struct_type == 'ht':
|
||||
ht_find(structure, name)
|
||||
elif struct_type == 'bst':
|
||||
bst_find(structure, name)
|
||||
end = time.perf_counter()
|
||||
find_time = end - start
|
||||
results.append([struct_name, mode_name, "поиск", find_time, rep])
|
||||
|
||||
# Удаление
|
||||
start = time.perf_counter()
|
||||
for name in test_data['delete_names']:
|
||||
if struct_type == 'll':
|
||||
structure = ll_delete(structure, name)
|
||||
elif struct_type == 'ht':
|
||||
ht_delete(structure, name)
|
||||
elif struct_type == 'bst':
|
||||
structure = bst_delete(structure, name)
|
||||
end = time.perf_counter()
|
||||
delete_time = end - start
|
||||
results.append([struct_name, mode_name, "удаление", delete_time, rep])
|
||||
|
||||
# Сохраняем в CSV с колонкой "Повтор"
|
||||
with open('docs/data/results.csv', 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Структура', 'Режим', 'Операция', 'Время (сек)', 'Повтор'])
|
||||
writer.writerows(results)
|
||||
|
||||
# Подсчет средних значений
|
||||
print("\n" + "="*70)
|
||||
print("СРЕДНИЕ ЗНАЧЕНИЯ (по 5 повторам)")
|
||||
print("="*70)
|
||||
|
||||
# Собираем данные для средних
|
||||
from collections import defaultdict
|
||||
avg_data = defaultdict(list)
|
||||
for row in results:
|
||||
key = (row[0], row[1], row[2])
|
||||
avg_data[key].append(row[3])
|
||||
|
||||
print(f"{'Структура':15} {'Режим':13} {'Операция':10} {'Среднее время':>12}")
|
||||
print("-"*55)
|
||||
for (struct, mode, op), times in avg_data.items():
|
||||
avg_time = sum(times) / len(times)
|
||||
print(f"{struct:15} {mode:13} {op:10} {avg_time:12.6f}")
|
||||
|
||||
print(f"\nВсе замеры (5 повторов) сохранены в docs/data/results.csv")
|
||||
|
||||
if __name__ == "__main__":
|
||||
random.seed(42)
|
||||
run_benchmarks()
|
||||
66
pogodinda/lab1/bst_phonebook.py
Normal file
66
pogodinda/lab1/bst_phonebook.py
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
def create_bst_node(name, phone):
|
||||
return {'name': name, 'phone': phone, 'left': None, 'right': None}
|
||||
|
||||
def bst_insert(root, name, phone):
|
||||
if root is None:
|
||||
return create_bst_node(name, phone)
|
||||
|
||||
if name < root['name']:
|
||||
root['left'] = bst_insert(root['left'], name, phone)
|
||||
elif name > root['name']:
|
||||
root['right'] = bst_insert(root['right'], name, phone)
|
||||
else:
|
||||
root['phone'] = phone
|
||||
|
||||
return root
|
||||
|
||||
def bst_find(root, name):
|
||||
if root is None:
|
||||
return None
|
||||
|
||||
if name < root['name']:
|
||||
return bst_find(root['left'], name)
|
||||
elif name > root['name']:
|
||||
return bst_find(root['right'], name)
|
||||
else:
|
||||
return root['phone']
|
||||
|
||||
def bst_find_min(root):
|
||||
current = root
|
||||
while current and current['left'] is not None:
|
||||
current = current['left']
|
||||
return current
|
||||
|
||||
def bst_delete(root, name):
|
||||
if root is None:
|
||||
return None
|
||||
|
||||
if name < root['name']:
|
||||
root['left'] = bst_delete(root['left'], name)
|
||||
elif name > root['name']:
|
||||
root['right'] = bst_delete(root['right'], name)
|
||||
else:
|
||||
if root['left'] is None:
|
||||
return root['right']
|
||||
elif root['right'] is None:
|
||||
return root['left']
|
||||
|
||||
min_node = bst_find_min(root['right'])
|
||||
root['name'] = min_node['name']
|
||||
root['phone'] = min_node['phone']
|
||||
root['right'] = bst_delete(root['right'], min_node['name'])
|
||||
|
||||
return root
|
||||
|
||||
def bst_list_all(root):
|
||||
records = []
|
||||
|
||||
def inorder_traversal(node):
|
||||
if node is None:
|
||||
return
|
||||
inorder_traversal(node['left'])
|
||||
records.append((node['name'], node['phone']))
|
||||
inorder_traversal(node['right'])
|
||||
|
||||
inorder_traversal(root)
|
||||
return records
|
||||
BIN
pogodinda/lab1/docs/data/graph.png
Normal file
BIN
pogodinda/lab1/docs/data/graph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
91
pogodinda/lab1/docs/data/results.csv
Normal file
91
pogodinda/lab1/docs/data/results.csv
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
Структура,Режим,Операция,Время (сек),Повтор
|
||||
LinkedList,случайный,вставка,4.9375476999994135,1
|
||||
LinkedList,случайный,поиск,0.04131099999358412,1
|
||||
LinkedList,случайный,удаление,0.02149870000721421,1
|
||||
LinkedList,случайный,вставка,4.644251200006693,2
|
||||
LinkedList,случайный,поиск,0.042833100000279956,2
|
||||
LinkedList,случайный,удаление,0.020811700000194833,2
|
||||
LinkedList,случайный,вставка,4.78751110000303,3
|
||||
LinkedList,случайный,поиск,0.041008800006238744,3
|
||||
LinkedList,случайный,удаление,0.02328480000142008,3
|
||||
LinkedList,случайный,вставка,5.261260200000834,4
|
||||
LinkedList,случайный,поиск,0.043706600001314655,4
|
||||
LinkedList,случайный,удаление,0.022936199995456263,4
|
||||
LinkedList,случайный,вставка,4.584412900003372,5
|
||||
LinkedList,случайный,поиск,0.10296139999991283,5
|
||||
LinkedList,случайный,удаление,0.06556309999723453,5
|
||||
LinkedList,отсортированный,вставка,4.5104472000093665,1
|
||||
LinkedList,отсортированный,поиск,0.03982529998756945,1
|
||||
LinkedList,отсортированный,удаление,0.016976200000499375,1
|
||||
LinkedList,отсортированный,вставка,4.366683700005524,2
|
||||
LinkedList,отсортированный,поиск,0.06564230000367388,2
|
||||
LinkedList,отсортированный,удаление,0.028787899995222688,2
|
||||
LinkedList,отсортированный,вставка,4.719926499994472,3
|
||||
LinkedList,отсортированный,поиск,0.04211149999173358,3
|
||||
LinkedList,отсортированный,удаление,0.01897859999735374,3
|
||||
LinkedList,отсортированный,вставка,4.7542686000088,4
|
||||
LinkedList,отсортированный,поиск,0.036636300006648526,4
|
||||
LinkedList,отсортированный,удаление,0.018097999985911883,4
|
||||
LinkedList,отсортированный,вставка,4.634292700007791,5
|
||||
LinkedList,отсортированный,поиск,0.038695100010954775,5
|
||||
LinkedList,отсортированный,удаление,0.0167280000023311,5
|
||||
HashTable,случайный,вставка,0.02125880001403857,1
|
||||
HashTable,случайный,поиск,0.0002066000015474856,1
|
||||
HashTable,случайный,удаление,0.0001053000014508143,1
|
||||
HashTable,случайный,вставка,0.02124099999491591,2
|
||||
HashTable,случайный,поиск,0.00018730000010691583,2
|
||||
HashTable,случайный,удаление,9.57000011112541e-05,2
|
||||
HashTable,случайный,вставка,0.022729699994670227,3
|
||||
HashTable,случайный,поиск,0.00018990000535268337,3
|
||||
HashTable,случайный,удаление,9.289999434258789e-05,3
|
||||
HashTable,случайный,вставка,0.02114750001055654,4
|
||||
HashTable,случайный,поиск,0.00018650000856723636,4
|
||||
HashTable,случайный,удаление,8.990000060293823e-05,4
|
||||
HashTable,случайный,вставка,0.022626199992373586,5
|
||||
HashTable,случайный,поиск,0.0002082999999402091,5
|
||||
HashTable,случайный,удаление,0.00010770000517368317,5
|
||||
HashTable,отсортированный,вставка,0.020416200000909157,1
|
||||
HashTable,отсортированный,поиск,0.0001990000018849969,1
|
||||
HashTable,отсортированный,удаление,9.100000897888094e-05,1
|
||||
HashTable,отсортированный,вставка,0.0198104000010062,2
|
||||
HashTable,отсортированный,поиск,0.00022190000163391232,2
|
||||
HashTable,отсортированный,удаление,0.00010359998850617558,2
|
||||
HashTable,отсортированный,вставка,0.020307500002672896,3
|
||||
HashTable,отсортированный,поиск,0.00020939999376423657,3
|
||||
HashTable,отсортированный,удаление,9.639999188948423e-05,3
|
||||
HashTable,отсортированный,вставка,0.020547599997371435,4
|
||||
HashTable,отсортированный,поиск,0.00019010000687558204,4
|
||||
HashTable,отсортированный,удаление,8.830000297166407e-05,4
|
||||
HashTable,отсортированный,вставка,0.021012699988204986,5
|
||||
HashTable,отсортированный,поиск,0.00023970000620465726,5
|
||||
HashTable,отсортированный,удаление,0.00011470000026747584,5
|
||||
BST,случайный,вставка,0.0366175000090152,1
|
||||
BST,случайный,поиск,0.00028440001187846065,1
|
||||
BST,случайный,удаление,0.0001773999974830076,1
|
||||
BST,случайный,вставка,0.03504180000163615,2
|
||||
BST,случайный,поиск,0.00026760000037029386,2
|
||||
BST,случайный,удаление,0.00017100000695791095,2
|
||||
BST,случайный,вставка,0.10903169999073725,3
|
||||
BST,случайный,поиск,0.00026849999267142266,3
|
||||
BST,случайный,удаление,0.00016820000018924475,3
|
||||
BST,случайный,вставка,0.03673420000995975,4
|
||||
BST,случайный,поиск,0.00029830000130459666,4
|
||||
BST,случайный,удаление,0.00018350000027567148,4
|
||||
BST,случайный,вставка,0.03608160000294447,5
|
||||
BST,случайный,поиск,0.00028360000578686595,5
|
||||
BST,случайный,удаление,0.00017559999832883477,5
|
||||
BST,отсортированный,вставка,19.357352699997136,1
|
||||
BST,отсортированный,поиск,0.17716789999394678,1
|
||||
BST,отсортированный,удаление,0.0909034999931464,1
|
||||
BST,отсортированный,вставка,17.69543930000509,2
|
||||
BST,отсортированный,поиск,0.14151260000653565,2
|
||||
BST,отсортированный,удаление,0.0668835999967996,2
|
||||
BST,отсортированный,вставка,18.86925250000786,3
|
||||
BST,отсортированный,поиск,0.16006389999529347,3
|
||||
BST,отсортированный,удаление,0.06768140000349376,3
|
||||
BST,отсортированный,вставка,17.811097199999494,4
|
||||
BST,отсортированный,поиск,0.16981530000339262,4
|
||||
BST,отсортированный,удаление,0.0726349000033224,4
|
||||
BST,отсортированный,вставка,16.240639600000577,5
|
||||
BST,отсортированный,поиск,0.1427488000044832,5
|
||||
BST,отсортированный,удаление,0.062093499989714473,5
|
||||
|
51
pogodinda/lab1/docs/report.md
Normal file
51
pogodinda/lab1/docs/report.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Отчёт по лабораторной работе "Структуры данных"
|
||||
|
||||
## 1. Введение
|
||||
|
||||
В данной работе были реализованы три структуры данных для хранения телефонного справочника: связный список, хеш-таблица и двоичное дерево поиска. Проведено экспериментальное сравнение производительности операций вставки, поиска и удаления на наборе из **10 000 записей**.
|
||||
|
||||
Для каждой структуры тестирование выполнялось на двух вариантах входных данных: случайный порядок и отсортированный по имени. Каждый эксперимент повторялся **5 раз**, в таблице приведены средние значения.
|
||||
|
||||
## 2. Результаты измерений
|
||||
|
||||
| Структура | Режим | Вставка, с | Поиск, с | Удаление, с |
|
||||
|-----------|-------|------------|----------|-------------|
|
||||
| Связный список | случайный | 4.84 | 0.0544 | 0.0308 |
|
||||
| Связный список | отсортированный | 4.60 | 0.0446 | 0.0199 |
|
||||
| Хеш-таблица | случайный | 0.0218 | 0.000196 | 0.000096 |
|
||||
| Хеш-таблица | отсортированный | 0.0204 | 0.000212 | 0.000098 |
|
||||
| Двоичное дерево | случайный | 0.0507 | 0.000280 | 0.000175 |
|
||||
| Двоичное дерево | отсортированный | 17.99 | 0.1583 | 0.0720 |
|
||||
|
||||

|
||||
|
||||
## 3. Анализ результатов
|
||||
|
||||
### 3.1. Влияние порядка данных на BST
|
||||
|
||||
При вставке элементов в отсортированном порядке двоичное дерево поиска вырождается в линейный список. Эксперимент подтверждает это: вставка на отсортированных данных заняла **17.99 секунды**, что в **355 раз** медленнее, чем на случайных данных (0.0507 секунды). Поиск и удаление также замедлились примерно в 500 раз.
|
||||
|
||||
### 3.2. Устойчивость хеш-таблицы к порядку
|
||||
|
||||
Хеш-таблица использует хеш-функцию, которая равномерно распределяет ключи по корзинам независимо от порядка поступления. В случайном и отсортированном режимах время вставки практически одинаково: 0.0218 и 0.0204 секунды. Разница находится в пределах погрешности измерений.
|
||||
|
||||
### 3.3. Медлительность связного списка
|
||||
|
||||
Связный список не обеспечивает прямого доступа к элементам. Вставка 10000 записей заняла **4.84 секунды** в случайном режиме. Время поиска в списке (0.0544 сек) в **278 раз** больше, чем в хеш-таблице (0.000196 сек). Удаление также значительно медленнее.
|
||||
|
||||
### 3.4. Сравнение удаления
|
||||
|
||||
- **Связный список**: 0.0308 сек (случайный) — требуется линейный поиск
|
||||
- **Хеш-таблица**: 0.000096 сек — практически мгновенно
|
||||
- **BST на случайных данных**: 0.000175 сек — очень быстро
|
||||
- **BST на отсортированных данных**: 0.0720 сек — в 400 раз медленнее, чем на случайных
|
||||
|
||||
## 4. Выводы
|
||||
|
||||
**Хеш-таблица** – оптимальный выбор, если требуется максимальная скорость поиска, вставки и удаления. В эксперименте показала стабильно высокую производительность во всех режимах (около 0.02 секунды на вставку 10000 записей).
|
||||
|
||||
**Двоичное дерево поиска** – эффективно на случайных данных, но критически деградирует на отсортированных. При необходимости работы с отсортированными данными следует использовать сбалансированные деревья (AVL, красно-чёрные).
|
||||
|
||||
**Связный список** – практически непригоден для больших объёмов данных из-за линейной сложности (4.84 секунды на вставку 10000 записей).
|
||||
|
||||
**Для телефонного справочника рекомендуется использовать хеш-таблицу** как наиболее быстрое и предсказуемое решение.
|
||||
31
pogodinda/lab1/hash_table_phonebook.py
Normal file
31
pogodinda/lab1/hash_table_phonebook.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from linked_list_phonebook import ll_insert, ll_find, ll_delete, ll_list_all
|
||||
|
||||
def hash_function(name, table_size):
|
||||
hash_value = 0
|
||||
for char in name:
|
||||
hash_value = (hash_value * 31 + ord(char)) % table_size
|
||||
return hash_value
|
||||
|
||||
def create_hash_table(size=1000):
|
||||
return [None] * size
|
||||
|
||||
def ht_insert(buckets, name, phone):
|
||||
index = hash_function(name, len(buckets))
|
||||
buckets[index] = ll_insert(buckets[index], name, phone)
|
||||
|
||||
def ht_find(buckets, name):
|
||||
index = hash_function(name, len(buckets))
|
||||
return ll_find(buckets[index], name)
|
||||
|
||||
def ht_delete(buckets, name):
|
||||
index = hash_function(name, len(buckets))
|
||||
buckets[index] = ll_delete(buckets[index], name)
|
||||
|
||||
def ht_list_all(buckets):
|
||||
all_records = []
|
||||
for head in buckets:
|
||||
if head is not None:
|
||||
all_records.extend(ll_list_all(head))
|
||||
|
||||
all_records.sort(key=lambda x: x[0])
|
||||
return all_records
|
||||
54
pogodinda/lab1/linked_list_phonebook.py
Normal file
54
pogodinda/lab1/linked_list_phonebook.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
def create_node(name, phone):
|
||||
return {'name': name, 'phone': phone, 'next': None}
|
||||
|
||||
def ll_insert(head, name, phone):
|
||||
if head is None:
|
||||
return create_node(name, phone)
|
||||
|
||||
if head['name'] == name:
|
||||
head['phone'] = phone
|
||||
return head
|
||||
|
||||
current = head
|
||||
while current['next'] is not None:
|
||||
if current['next']['name'] == name:
|
||||
current['next']['phone'] = phone
|
||||
return head
|
||||
current = current['next']
|
||||
|
||||
current['next'] = create_node(name, phone)
|
||||
return head
|
||||
|
||||
def ll_find(head, name):
|
||||
current = head
|
||||
while current is not None:
|
||||
if current['name'] == name:
|
||||
return current['phone']
|
||||
current = current['next']
|
||||
return None
|
||||
|
||||
def ll_delete(head, name):
|
||||
if head is None:
|
||||
return None
|
||||
|
||||
if head['name'] == name:
|
||||
return head['next']
|
||||
|
||||
current = head
|
||||
while current['next'] is not None:
|
||||
if current['next']['name'] == name:
|
||||
current['next'] = current['next']['next']
|
||||
return head
|
||||
current = current['next']
|
||||
|
||||
return head
|
||||
|
||||
def ll_list_all(head):
|
||||
records = []
|
||||
current = head
|
||||
while current is not None:
|
||||
records.append((current['name'], current['phone']))
|
||||
current = current['next']
|
||||
|
||||
records.sort(key=lambda x: x[0])
|
||||
return records
|
||||
96
pogodinda/lab1/plot_results.py
Normal file
96
pogodinda/lab1/plot_results.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
import matplotlib.pyplot as plt
|
||||
import csv
|
||||
import numpy as np
|
||||
from collections import defaultdict
|
||||
|
||||
# Читаем результаты из CSV и усредняем по 5 повторам
|
||||
data = defaultdict(lambda: defaultdict(lambda: defaultdict(list)))
|
||||
|
||||
with open('docs/data/results.csv', 'r', encoding='utf-8') as f:
|
||||
reader = csv.reader(f)
|
||||
header = next(reader) # пропускаем заголовок
|
||||
|
||||
# Проверяем, есть ли колонка "Повтор"
|
||||
has_repeat = 'Повтор' in header
|
||||
|
||||
for row in reader:
|
||||
struct = row[0]
|
||||
mode = row[1]
|
||||
op = row[2]
|
||||
time_val = float(row[3])
|
||||
|
||||
# Сохраняем все замеры
|
||||
data[op][struct][mode].append(time_val)
|
||||
|
||||
# Усредняем
|
||||
avg_data = {}
|
||||
for op in data:
|
||||
avg_data[op] = {}
|
||||
for struct in data[op]:
|
||||
avg_data[op][struct] = {}
|
||||
for mode in data[op][struct]:
|
||||
times = data[op][struct][mode]
|
||||
avg_data[op][struct][mode] = sum(times) / len(times)
|
||||
|
||||
# Создаём графики
|
||||
operations = ['вставка', 'поиск', 'удаление']
|
||||
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
|
||||
|
||||
colors = {'случайный': '#1f77b4', 'отсортированный': '#d62728'}
|
||||
|
||||
for idx, op in enumerate(operations):
|
||||
ax = axes[idx]
|
||||
|
||||
structures = ['Связный список', 'Хеш-таблица', 'Двоичное дерево']
|
||||
data_keys = ['LinkedList', 'HashTable', 'BST']
|
||||
|
||||
x = np.arange(len(structures))
|
||||
width = 0.35
|
||||
|
||||
random_times = [avg_data[op][key].get('случайный', 0) for key in data_keys]
|
||||
sorted_times = [avg_data[op][key].get('отсортированный', 0) for key in data_keys]
|
||||
|
||||
bars1 = ax.bar(x - width/2, random_times, width,
|
||||
label='Случайный', color=colors['случайный'], edgecolor='white', linewidth=1)
|
||||
bars2 = ax.bar(x + width/2, sorted_times, width,
|
||||
label='Отсортированный', color=colors['отсортированный'], edgecolor='white', linewidth=1)
|
||||
|
||||
ax.set_ylabel('Время (секунды)', fontsize=11)
|
||||
ax.set_title(f'{op.upper()}', fontsize=13, fontweight='bold')
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(structures, fontsize=10)
|
||||
|
||||
ax.grid(True, axis='y', alpha=0.3, linestyle='--')
|
||||
ax.set_axisbelow(True)
|
||||
|
||||
ax.spines['top'].set_visible(False)
|
||||
ax.spines['right'].set_visible(False)
|
||||
|
||||
for bar in bars1:
|
||||
height = bar.get_height()
|
||||
if height > 0:
|
||||
ax.annotate(f'{height:.4f}',
|
||||
xy=(bar.get_x() + bar.get_width()/2, height),
|
||||
xytext=(0, 3), textcoords="offset points",
|
||||
ha='center', va='bottom', fontsize=8)
|
||||
|
||||
for bar in bars2:
|
||||
height = bar.get_height()
|
||||
if height > 0:
|
||||
ax.annotate(f'{height:.4f}',
|
||||
xy=(bar.get_x() + bar.get_width()/2, height),
|
||||
xytext=(0, 3), textcoords="offset points",
|
||||
ha='center', va='bottom', fontsize=8)
|
||||
|
||||
fig.legend(labels=['Случайный', 'Отсортированный'],
|
||||
loc='lower center', bbox_to_anchor=(0.5, -0.05),
|
||||
ncol=2, fontsize=11, frameon=True, fancybox=True, shadow=True)
|
||||
|
||||
plt.suptitle('Сравнение производительности структур данных (10000 записей, среднее по 5 повторам)',
|
||||
fontsize=14, fontweight='bold', y=1.02)
|
||||
plt.tight_layout()
|
||||
plt.subplots_adjust(bottom=0.12)
|
||||
plt.savefig('docs/data/graph.png', dpi=150, bbox_inches='tight', facecolor='white')
|
||||
plt.show()
|
||||
|
||||
print("График сохранён в docs/data/graph.png (использованы средние значения по 5 повторам)")
|
||||
10
pogodinda/lab2/data/demo_maze.txt
Normal file
10
pogodinda/lab2/data/demo_maze.txt
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
###############
|
||||
#S ##
|
||||
# ####### # # #
|
||||
# # # # # #
|
||||
# # ### # # #
|
||||
# # ##### #
|
||||
##### # #
|
||||
# # ##### #
|
||||
# ##### E
|
||||
###############
|
||||
56
pogodinda/lab2/main.py
Normal file
56
pogodinda/lab2/main.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src'))
|
||||
|
||||
from src.maze_builder import TextFileMazeBuilder
|
||||
from src.maze_solver import MazeSolver
|
||||
from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
from src.observer import ConsoleView
|
||||
from src.commands import Player, MoveCommand
|
||||
|
||||
|
||||
def demo():
|
||||
print("=" * 70)
|
||||
print("ПОИСК ВЫХОДА ИЗ ЛАБИРИНТА")
|
||||
print("=" * 70)
|
||||
|
||||
# 1. Загрузка лабиринта
|
||||
print("\n[1] Загрузка лабиринта из файла")
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("data/demo_maze.txt")
|
||||
print(maze)
|
||||
print(f"Старт: ({maze.start.x}, {maze.start.y})")
|
||||
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
|
||||
|
||||
# 2. Сравнение алгоритмов (Strategy)
|
||||
print("\n[2] Сравнение алгоритмов")
|
||||
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
|
||||
|
||||
for strategy in strategies:
|
||||
solver = MazeSolver(maze, strategy)
|
||||
stats = solver.solve("demo")
|
||||
print(f"{strategy.get_name():10} | {stats.time_ms:8.4f} мс | "
|
||||
f"посещено: {stats.visited_cells:3} | длина: {stats.path_length}")
|
||||
|
||||
# 3. Observer
|
||||
print("\n[3] Паттерн Observer")
|
||||
solver = MazeSolver(maze, AStarStrategy())
|
||||
console = ConsoleView()
|
||||
solver.add_observer(console)
|
||||
solver.solve("demo")
|
||||
print(f"События: {console.events}")
|
||||
|
||||
# 4. Command
|
||||
print("\n[4] Паттерн Command")
|
||||
player = Player(maze.start)
|
||||
console.render(maze, player.current_cell)
|
||||
|
||||
cmd = MoveCommand(player, maze, 'S')
|
||||
cmd.execute()
|
||||
print(f"После S: {player}")
|
||||
|
||||
cmd.undo()
|
||||
print(f"После undo: {player}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
demo()
|
||||
68
pogodinda/lab2/report.md
Normal file
68
pogodinda/lab2/report.md
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# Лабораторная работа 2: Поиск выхода из лабиринта
|
||||
|
||||
## 1. Цель
|
||||
Разработать гибкую, расширяемую программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма, визуализации процесса и экспериментального сравнения алгоритмов. В ходе работы необходимо применить минимум 3 паттерна проектирования из списка GoF, обосновать их выбор и продемонстрировать преимущества такой архитектуры.
|
||||
|
||||
## 2. Паттерны
|
||||
|
||||
| Паттерн | Где | Зачем |
|
||||
|---------|-----|-------|
|
||||
| Builder | `maze_builder.py` | Загрузка лабиринта из файла |
|
||||
| Strategy | `pathfinding.py` | Смена алгоритмов (BFS, DFS, A*, Дейкстра) |
|
||||
| Observer | `observer.py` | Уведомления о событиях поиска |
|
||||
| Command | `commands.py` | Перемещение игрока с undo |
|
||||
|
||||
## 3. Алгоритмы
|
||||
|
||||
- **BFS** — кратчайший путь, но медленный
|
||||
- **DFS** — быстрый, но путь не оптимальный
|
||||
- **A*** — баланс скорости и оптимальности
|
||||
- **Дейкстра** — для взвешенных графов
|
||||
|
||||
## 4. Результаты
|
||||
|
||||
### Таблица: посещённые клетки
|
||||
|
||||
| Лабиринт | BFS | DFS | A* | Дейкстра |
|
||||
|:--------:|:---:|:---:|:--:|:--------:|
|
||||
| 10×10 | 52 | 49 | 47 | 52 |
|
||||
| 50×50 | 514 | 326 | 491 | 511 |
|
||||
| 100×100 | 1989 | 1509 | 1909 | 1987 |
|
||||
| Пустой | 398 | 399 | 324 | 396 |
|
||||
| Без выхода | 0 | 0 | 0 | 0 |
|
||||
| Взвешенный | 145 | 111 | 139 | 143 |
|
||||
|
||||
### Время (мс)
|
||||
|
||||
| Лабиринт | BFS | DFS | A* | Дейкстра |
|
||||
|:--------:|:---:|:---:|:--:|:--------:|
|
||||
| 10×10 | 0.107 | 0.068 | 0.142 | 0.146 |
|
||||
| 50×50 | 1.141 | 0.690 | 1.509 | 1.482 |
|
||||
| 100×100 | 7.667 | 5.207 | 8.254 | 8.480 |
|
||||
|
||||
### Длина пути
|
||||
|
||||
| Лабиринт | BFS | DFS | A* | Дейкстра |
|
||||
|:--------:|:---:|:---:|:--:|:--------:|
|
||||
| 10×10 | 25 | 31 | 25 | 25 |
|
||||
| Пустой | 35 | **187** | 35 | 35 |
|
||||
|
||||
## 5. Графики
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## 6. Анализ
|
||||
|
||||
- **DFS** быстрее всех, но в пустом лабиринте путь в 5.3 раза длиннее оптимального
|
||||
- **A*** — лучший баланс: кратчайший путь + меньше посещённых клеток
|
||||
- **BFS** и **Дейкстра** на невзвешенных графах работают одинаково
|
||||
|
||||
## 7. Вывод
|
||||
|
||||
- Паттерны сделали код **гибким** и **расширяемым**
|
||||
- **A*** — оптимальный выбор для большинства задач
|
||||
- **Дейкстра** нужен только для взвешенных графов
|
||||
25
pogodinda/lab2/results/experiment_results.csv
Normal file
25
pogodinda/lab2/results/experiment_results.csv
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
Лабиринт,Алгоритм,Время_мс,Посещено_клеток,Длина_пути
|
||||
small_10x10,BFS,0.185460,52,25
|
||||
small_10x10,DFS,0.115700,49,31
|
||||
small_10x10,A*,0.231720,47,25
|
||||
small_10x10,Dijkstra,0.243700,52,25
|
||||
medium_50x50,BFS,2.559340,603,425
|
||||
medium_50x50,DFS,1.772520,454,425
|
||||
medium_50x50,A*,3.118380,591,425
|
||||
medium_50x50,Dijkstra,2.985780,601,425
|
||||
large_100x100,BFS,9.025880,1761,877
|
||||
large_100x100,DFS,4.254880,946,877
|
||||
large_100x100,A*,11.625360,1689,877
|
||||
large_100x100,Dijkstra,11.725580,1759,877
|
||||
empty_20x20,BFS,1.531960,398,35
|
||||
empty_20x20,DFS,1.376720,399,187
|
||||
empty_20x20,A*,1.968720,324,35
|
||||
empty_20x20,Dijkstra,2.215060,396,35
|
||||
no_exit_10x10,BFS,0.000700,0,0
|
||||
no_exit_10x10,DFS,0.000560,0,0
|
||||
no_exit_10x10,A*,0.000420,0,0
|
||||
no_exit_10x10,Dijkstra,0.000540,0,0
|
||||
weighted_15x15,BFS,0.528120,145,33
|
||||
weighted_15x15,DFS,0.270900,111,53
|
||||
weighted_15x15,A*,0.815480,131,33
|
||||
weighted_15x15,Dijkstra,0.762560,138,33
|
||||
|
BIN
pogodinda/lab2/results/maze_comparison.png
Normal file
BIN
pogodinda/lab2/results/maze_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 127 KiB |
BIN
pogodinda/lab2/results/time_comparison.png
Normal file
BIN
pogodinda/lab2/results/time_comparison.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 185 KiB |
BIN
pogodinda/lab2/results/visited_cells.png
Normal file
BIN
pogodinda/lab2/results/visited_cells.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 170 KiB |
0
pogodinda/lab2/src/__init__.py
Normal file
0
pogodinda/lab2/src/__init__.py
Normal file
79
pogodinda/lab2/src/commands.py
Normal file
79
pogodinda/lab2/src/commands.py
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List
|
||||
|
||||
from maze import Maze, Cell
|
||||
|
||||
|
||||
class Command(ABC):
|
||||
"""Интерфейс команды (Command pattern)."""
|
||||
|
||||
@abstractmethod
|
||||
def execute(self) -> bool:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def undo(self):
|
||||
pass
|
||||
|
||||
|
||||
class Player:
|
||||
"""Игрок, перемещающийся по лабиринту."""
|
||||
|
||||
def __init__(self, cell: Cell):
|
||||
self.current_cell = cell
|
||||
self._history: List[Cell] = []
|
||||
|
||||
def move_to(self, cell: Cell):
|
||||
self._history.append(self.current_cell)
|
||||
self.current_cell = cell
|
||||
|
||||
def move_back(self):
|
||||
if self._history:
|
||||
self.current_cell = self._history.pop()
|
||||
return self.current_cell
|
||||
return None
|
||||
|
||||
def __repr__(self):
|
||||
return f"Player({self.current_cell.x}, {self.current_cell.y})"
|
||||
|
||||
|
||||
class MoveCommand(Command):
|
||||
"""Команда перемещения игрока."""
|
||||
|
||||
DIRECTIONS = {
|
||||
'W': (0, -1),
|
||||
'S': (0, 1),
|
||||
'A': (-1, 0),
|
||||
'D': (1, 0),
|
||||
}
|
||||
|
||||
def __init__(self, player: Player, maze: Maze, direction: str):
|
||||
self.player = player
|
||||
self.maze = maze
|
||||
self.direction = direction.upper()
|
||||
self._previous_cell = None
|
||||
self._executed = False
|
||||
|
||||
def execute(self) -> bool:
|
||||
if self.direction not in self.DIRECTIONS:
|
||||
return False
|
||||
|
||||
dx, dy = self.DIRECTIONS[self.direction]
|
||||
new_x = self.player.current_cell.x + dx
|
||||
new_y = self.player.current_cell.y + dy
|
||||
|
||||
new_cell = self.maze.get_cell(new_x, new_y)
|
||||
|
||||
if new_cell and new_cell.is_passable():
|
||||
self._previous_cell = self.player.current_cell
|
||||
self.player.move_to(new_cell)
|
||||
self._executed = True
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def undo(self):
|
||||
if self._executed and self._previous_cell:
|
||||
self.player.current_cell = self._previous_cell
|
||||
self._executed = False
|
||||
self._previous_cell = None
|
||||
240
pogodinda/lab2/src/experiments.py
Normal file
240
pogodinda/lab2/src/experiments.py
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
import os
|
||||
import random
|
||||
import time
|
||||
import csv
|
||||
from typing import Dict, List
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from maze import Maze, Cell
|
||||
from maze_builder import RandomMazeBuilder
|
||||
from pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
from maze_solver import MazeSolver, SearchStats
|
||||
|
||||
|
||||
def create_test_mazes() -> Dict[str, Maze]:
|
||||
"""Создаёт тестовые лабиринты разной сложности."""
|
||||
mazes = {}
|
||||
|
||||
# 1. Маленький 10×10 с простым путём
|
||||
small = Maze(10, 10)
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or
|
||||
(x == 3 and y < 7) or (x == 6 and y > 2))
|
||||
is_start = (x == 1 and y == 1)
|
||||
is_exit = (x == 8 and y == 8)
|
||||
small.set_cell(x, y, Cell(x, y, is_wall=is_wall,
|
||||
is_start=is_start, is_exit=is_exit))
|
||||
mazes['small_10x10'] = small
|
||||
|
||||
# 2. Средний 50×50 — случайный
|
||||
mazes['medium_50x50'] = RandomMazeBuilder(51, 51).build_from_file()
|
||||
|
||||
# 3. Большой 100×100 — случайный
|
||||
mazes['large_100x100'] = RandomMazeBuilder(101, 101).build_from_file()
|
||||
|
||||
# 4. Пустой 20×20 — без стен
|
||||
empty = Maze(20, 20)
|
||||
for y in range(20):
|
||||
for x in range(20):
|
||||
empty.set_cell(x, y, Cell(x, y, is_wall=False,
|
||||
is_start=(x==1 and y==1),
|
||||
is_exit=(x==18 and y==18)))
|
||||
mazes['empty_20x20'] = empty
|
||||
|
||||
# 5. Без выхода — проверка обработки
|
||||
no_exit = Maze(10, 10)
|
||||
for y in range(10):
|
||||
for x in range(10):
|
||||
is_wall = (x == 0 or x == 9 or y == 0 or y == 9 or x == 5)
|
||||
no_exit.set_cell(x, y, Cell(x, y, is_wall=is_wall,
|
||||
is_start=(x==1 and y==1)))
|
||||
mazes['no_exit_10x10'] = no_exit
|
||||
|
||||
# 6. Взвешенный 15×15
|
||||
weighted = Maze(15, 15)
|
||||
for y in range(15):
|
||||
for x in range(15):
|
||||
is_wall = (x == 0 or x == 14 or y == 0 or y == 14 or
|
||||
(x == 5 and y != 7) or (x == 10 and y != 3))
|
||||
weight = 1
|
||||
if 3 <= x <= 7 and 3 <= y <= 7:
|
||||
weight = 2 # песок
|
||||
elif 8 <= x <= 12 and 8 <= y <= 12:
|
||||
weight = 3 # болото
|
||||
|
||||
weighted.set_cell(x, y, Cell(x, y,
|
||||
is_wall=is_wall,
|
||||
is_start=(x==1 and y==1),
|
||||
is_exit=(x==13 and y==13),
|
||||
weight=weight))
|
||||
mazes['weighted_15x15'] = weighted
|
||||
|
||||
return mazes
|
||||
|
||||
|
||||
def run_experiments(mazes: Dict[str, Maze],
|
||||
strategies: List,
|
||||
runs_per_test: int = 5) -> List[SearchStats]:
|
||||
"""Запускает эксперименты, возвращает список статистик."""
|
||||
results = []
|
||||
|
||||
for maze_name, maze in mazes.items():
|
||||
print(f"\n{'='*60}")
|
||||
print(f"Лабиринт: {maze_name} ({maze.width}x{maze.height})")
|
||||
print(f"{'='*60}")
|
||||
|
||||
for strategy in strategies:
|
||||
print(f"\nАлгоритм: {strategy.get_name()}")
|
||||
|
||||
times = []
|
||||
visited_list = []
|
||||
path_lens = []
|
||||
|
||||
solver = MazeSolver(maze, strategy)
|
||||
|
||||
for run in range(runs_per_test):
|
||||
stats = solver.solve(maze_name)
|
||||
times.append(stats.time_ms)
|
||||
visited_list.append(stats.visited_cells)
|
||||
path_lens.append(stats.path_length)
|
||||
|
||||
print(f" Запуск {run+1}: {stats.time_ms:.4f} мс, "
|
||||
f"посещено: {stats.visited_cells}, "
|
||||
f"длина: {stats.path_length}")
|
||||
|
||||
# Средние значения
|
||||
avg = SearchStats(
|
||||
time_ms=np.mean(times),
|
||||
visited_cells=int(np.mean(visited_list)),
|
||||
path_length=int(np.mean(path_lens)),
|
||||
algorithm_name=strategy.get_name(),
|
||||
maze_name=maze_name
|
||||
)
|
||||
results.append(avg)
|
||||
|
||||
print(f" СРЕДНЕЕ: {avg.time_ms:.4f} мс, "
|
||||
f"посещено: {avg.visited_cells}, "
|
||||
f"длина: {avg.path_length}")
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def save_csv(results: List[SearchStats], filename: str):
|
||||
"""Сохраняет результаты в CSV."""
|
||||
with open(filename, 'w', newline='', encoding='utf-8') as f:
|
||||
writer = csv.writer(f)
|
||||
writer.writerow(['Лабиринт', 'Алгоритм', 'Время_мс',
|
||||
'Посещено_клеток', 'Длина_пути'])
|
||||
for r in results:
|
||||
writer.writerow([
|
||||
r.maze_name, r.algorithm_name,
|
||||
f"{r.time_ms:.6f}", r.visited_cells, r.path_length
|
||||
])
|
||||
print(f"\nCSV сохранён: {filename}")
|
||||
|
||||
|
||||
def create_plots(results: List[SearchStats], output_dir: str = "."):
|
||||
"""Создаёт графики сравнения."""
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
maze_names = sorted(set(r.maze_name for r in results))
|
||||
algorithms = sorted(set(r.algorithm_name for r in results))
|
||||
|
||||
# 1. Общее сравнение по лабиринтам
|
||||
fig, axes = plt.subplots(2, 3, figsize=(18, 12))
|
||||
fig.suptitle('Сравнение алгоритмов поиска пути', fontsize=16)
|
||||
|
||||
for idx, maze_name in enumerate(maze_names):
|
||||
ax = axes[idx // 3, idx % 3]
|
||||
maze_res = [r for r in results if r.maze_name == maze_name]
|
||||
|
||||
names = [r.algorithm_name for r in maze_res]
|
||||
times = [r.time_ms for r in maze_res]
|
||||
visited = [r.visited_cells for r in maze_res]
|
||||
paths = [r.path_length for r in maze_res]
|
||||
|
||||
x = np.arange(len(names))
|
||||
w = 0.25
|
||||
|
||||
ax.bar(x - w, times, w, label='Время (мс)', color='skyblue')
|
||||
ax.bar(x, [v/10 for v in visited], w,
|
||||
label='Посещено (÷10)', color='lightcoral')
|
||||
ax.bar(x + w, paths, w, label='Длина пути', color='lightgreen')
|
||||
|
||||
ax.set_title(maze_name)
|
||||
ax.set_xticks(x)
|
||||
ax.set_xticklabels(names, rotation=45)
|
||||
ax.legend(fontsize=8)
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'{output_dir}/maze_comparison.png', dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 2. Посещённые клетки
|
||||
fig, ax = plt.subplots(figsize=(14, 8))
|
||||
for algo in algorithms:
|
||||
algo_res = [r for r in results if r.algorithm_name == algo]
|
||||
names = [r.maze_name for r in algo_res]
|
||||
vals = [r.visited_cells for r in algo_res]
|
||||
ax.plot(names, vals, marker='o', label=algo, linewidth=2)
|
||||
|
||||
ax.set_title('Посещённые клетки по лабиринтам', fontsize=14)
|
||||
ax.set_xlabel('Лабиринт')
|
||||
ax.set_ylabel('Клетки')
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3)
|
||||
plt.xticks(rotation=45)
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'{output_dir}/visited_cells.png', dpi=150)
|
||||
plt.close()
|
||||
|
||||
# 3. Время выполнения (логарифмическая шкала)
|
||||
fig, ax = plt.subplots(figsize=(14, 8))
|
||||
for algo in algorithms:
|
||||
algo_res = [r for r in results if r.algorithm_name == algo]
|
||||
names = [r.maze_name for r in algo_res]
|
||||
vals = [max(r.time_ms, 0.001) for r in algo_res]
|
||||
ax.plot(names, vals, marker='s', label=algo, linewidth=2)
|
||||
|
||||
ax.set_title('Время выполнения (лог. шкала)', fontsize=14)
|
||||
ax.set_xlabel('Лабиринт')
|
||||
ax.set_ylabel('Время (мс)')
|
||||
ax.set_yscale('log')
|
||||
ax.legend()
|
||||
ax.grid(True, alpha=0.3, which='both')
|
||||
plt.xticks(rotation=45)
|
||||
plt.tight_layout()
|
||||
plt.savefig(f'{output_dir}/time_comparison.png', dpi=150)
|
||||
plt.close()
|
||||
|
||||
print(f"Графики сохранены в {output_dir}/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print("ЭКСПЕРИМЕНТАЛЬНАЯ ЧАСТЬ")
|
||||
print("=" * 70)
|
||||
|
||||
# Создаём лабиринты
|
||||
mazes = create_test_mazes()
|
||||
|
||||
# Алгоритмы для сравнения
|
||||
strategies = [BFSStrategy(), DFSStrategy(),
|
||||
AStarStrategy(), DijkstraStrategy()]
|
||||
|
||||
# Запускаем эксперименты
|
||||
results = run_experiments(mazes, strategies, runs_per_test=5)
|
||||
|
||||
# Сохраняем CSV
|
||||
save_csv(results, "experiment_results.csv")
|
||||
|
||||
# Строим графики
|
||||
create_plots(results)
|
||||
|
||||
print("\n" + "=" * 70)
|
||||
print("ГОТОВО!")
|
||||
print("=" * 70)
|
||||
84
pogodinda/lab2/src/maze.py
Normal file
84
pogodinda/lab2/src/maze.py
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
from typing import List, Optional
|
||||
|
||||
|
||||
class Cell:
|
||||
"""Клетка лабиринта."""
|
||||
|
||||
def __init__(self, x: int, y: int, is_wall: bool = False,
|
||||
is_start: bool = False, is_exit: bool = False, weight: int = 1):
|
||||
self.x = x
|
||||
self.y = y
|
||||
self.is_wall = is_wall
|
||||
self.is_start = is_start
|
||||
self.is_exit = is_exit
|
||||
self.weight = weight # для взвешенных лабиринтов
|
||||
|
||||
def is_passable(self) -> bool:
|
||||
return not self.is_wall
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_start:
|
||||
return 'S'
|
||||
elif self.is_exit:
|
||||
return 'E'
|
||||
elif self.is_wall:
|
||||
return '#'
|
||||
else:
|
||||
return ' '
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, Cell):
|
||||
return self.x == other.x and self.y == other.y
|
||||
return False
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.x, self.y))
|
||||
|
||||
|
||||
class Maze:
|
||||
"""Лабиринт — сетка клеток."""
|
||||
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
self.cells: List[List[Cell]] = []
|
||||
self.start: Optional[Cell] = None
|
||||
self.exit: Optional[Cell] = None
|
||||
|
||||
for y in range(height):
|
||||
row = []
|
||||
for x in range(width):
|
||||
row.append(Cell(x, y))
|
||||
self.cells.append(row)
|
||||
|
||||
def get_cell(self, x: int, y: int) -> Optional[Cell]:
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
return self.cells[y][x]
|
||||
return None
|
||||
|
||||
def set_cell(self, x: int, y: int, cell: Cell):
|
||||
if 0 <= x < self.width and 0 <= y < self.height:
|
||||
self.cells[y][x] = cell
|
||||
if cell.is_start:
|
||||
self.start = cell
|
||||
if cell.is_exit:
|
||||
self.exit = cell
|
||||
|
||||
def get_neighbors(self, cell: Cell) -> List[Cell]:
|
||||
"""Соседи сверху/снизу/слева/справа, если проходимы."""
|
||||
neighbors = []
|
||||
directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]
|
||||
|
||||
for dx, dy in directions:
|
||||
nx, ny = cell.x + dx, cell.y + dy
|
||||
neighbor = self.get_cell(nx, ny)
|
||||
if neighbor and neighbor.is_passable():
|
||||
neighbors.append(neighbor)
|
||||
|
||||
return neighbors
|
||||
|
||||
def __repr__(self):
|
||||
lines = []
|
||||
for row in self.cells:
|
||||
lines.append(''.join(str(cell) for cell in row))
|
||||
return '\n'.join(lines)
|
||||
121
pogodinda/lab2/src/maze_builder.py
Normal file
121
pogodinda/lab2/src/maze_builder.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
# maze_builder.py
|
||||
from abc import ABC, abstractmethod
|
||||
from maze import Maze, Cell
|
||||
|
||||
|
||||
class MazeBuilder(ABC):
|
||||
"""Интерфейс строителя лабиринта (Builder pattern)."""
|
||||
|
||||
@abstractmethod
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
"""Принимает путь к файлу, возвращает готовый Maze."""
|
||||
pass
|
||||
|
||||
|
||||
class TextFileMazeBuilder(MazeBuilder):
|
||||
"""
|
||||
Строитель лабиринта из текстового файла.
|
||||
|
||||
Формат файла:
|
||||
# — стена
|
||||
. или пробел — проход
|
||||
S — старт
|
||||
E — выход
|
||||
"""
|
||||
|
||||
def build_from_file(self, filename: str) -> Maze:
|
||||
with open(filename, 'r', encoding='utf-8') as f:
|
||||
lines = [line.rstrip('\n') for line in f.readlines()]
|
||||
|
||||
if not lines:
|
||||
raise ValueError("Файл лабиринта пуст")
|
||||
|
||||
height = len(lines)
|
||||
width = max(len(line) for line in lines)
|
||||
|
||||
maze = Maze(width, height)
|
||||
start_found = False
|
||||
exit_found = False
|
||||
|
||||
for y, line in enumerate(lines):
|
||||
for x, char in enumerate(line):
|
||||
is_wall = (char == '#')
|
||||
is_start = (char == 'S')
|
||||
is_exit = (char == 'E')
|
||||
|
||||
if is_start:
|
||||
start_found = True
|
||||
if is_exit:
|
||||
exit_found = True
|
||||
|
||||
cell = Cell(x, y, is_wall=is_wall, is_start=is_start, is_exit=is_exit)
|
||||
maze.set_cell(x, y, cell)
|
||||
|
||||
if not start_found or not exit_found:
|
||||
raise ValueError("Лабиринт должен содержать старт (S) и выход (E)")
|
||||
|
||||
return maze
|
||||
|
||||
|
||||
class RandomMazeBuilder(MazeBuilder):
|
||||
"""
|
||||
Строитель случайного лабиринта.
|
||||
Алгоритм: рекурсивный бэктрекинг.
|
||||
"""
|
||||
|
||||
def __init__(self, width: int, height: int):
|
||||
self.width = width
|
||||
self.height = height
|
||||
|
||||
def build_from_file(self, filename: str = None) -> Maze:
|
||||
"""
|
||||
filename игнорируется — лабиринт генерируется случайно.
|
||||
Название метода общее для всех Builder'ов.
|
||||
"""
|
||||
import random
|
||||
|
||||
maze = Maze(self.width, self.height)
|
||||
|
||||
# Шаг 1: Заполняем всё стенами
|
||||
for y in range(self.height):
|
||||
for x in range(self.width):
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=True))
|
||||
|
||||
# Шаг 2: Рекурсивно прокладываем пути
|
||||
visited = set()
|
||||
|
||||
def carve(x, y):
|
||||
"""Прокладывает проход из точки (x, y)."""
|
||||
visited.add((x, y))
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=False))
|
||||
|
||||
# 4 направления, перемешанные случайно
|
||||
directions = [(0, -2), (0, 2), (-2, 0), (2, 0)]
|
||||
random.shuffle(directions)
|
||||
|
||||
for dx, dy in directions:
|
||||
nx, ny = x + dx, y + dy
|
||||
|
||||
# Проверяем границы и что ещё не посещали
|
||||
if (0 <= nx < self.width and 0 <= ny < self.height
|
||||
and (nx, ny) not in visited):
|
||||
|
||||
# Убираем стену между текущей и новой клеткой
|
||||
wall_x = x + dx // 2
|
||||
wall_y = y + dy // 2
|
||||
maze.set_cell(wall_x, wall_y, Cell(wall_x, wall_y, is_wall=False))
|
||||
|
||||
# Рекурсия в новую клетку
|
||||
carve(nx, ny)
|
||||
|
||||
# Начинаем с (1, 1) — нечётные координаты для коридоров
|
||||
carve(1, 1)
|
||||
|
||||
# Шаг 3: Устанавливаем старт и выход в углах лабиринта
|
||||
maze.set_cell(1, 1, Cell(1, 1, is_wall=False, is_start=True))
|
||||
maze.set_cell(
|
||||
self.width - 2, self.height - 2,
|
||||
Cell(self.width - 2, self.height - 2, is_wall=False, is_exit=True)
|
||||
)
|
||||
|
||||
return maze
|
||||
86
pogodinda/lab2/src/maze_solver.py
Normal file
86
pogodinda/lab2/src/maze_solver.py
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from maze import Maze, Cell
|
||||
from pathfinding import PathFindingStrategy
|
||||
|
||||
|
||||
@dataclass
|
||||
class SearchStats:
|
||||
"""Статистика поиска пути."""
|
||||
time_ms: float # время выполнения в миллисекундах
|
||||
visited_cells: int # сколько клеток посетил алгоритм
|
||||
path_length: int # длина найденного пути
|
||||
algorithm_name: str # какой алгоритм использовался
|
||||
maze_name: str # название лабиринта
|
||||
|
||||
|
||||
class MazeSolver:
|
||||
"""
|
||||
Оркестратор поиска пути.
|
||||
Использует паттерн Strategy для переключения алгоритмов.
|
||||
"""
|
||||
|
||||
def __init__(self, maze: Maze, strategy: PathFindingStrategy = None):
|
||||
self.maze = maze
|
||||
self.strategy = strategy
|
||||
self._observers = [] # для паттерна Observer (Этап 5)
|
||||
|
||||
def set_strategy(self, strategy: PathFindingStrategy):
|
||||
"""
|
||||
Динамическая смена алгоритма.
|
||||
Без паттерна Strategy пришлось бы переписывать этот метод
|
||||
под каждый новый алгоритм.
|
||||
"""
|
||||
self.strategy = strategy
|
||||
|
||||
def add_observer(self, observer):
|
||||
"""Добавление наблюдателя (подготовка к Этапу 5)."""
|
||||
self._observers.append(observer)
|
||||
|
||||
def _notify_observers(self, event: str):
|
||||
"""Уведомляет всех наблюдателей о событии."""
|
||||
for observer in self._observers:
|
||||
observer.update(event)
|
||||
|
||||
def solve(self, maze_name: str = "unnamed") -> SearchStats:
|
||||
"""
|
||||
Выполняет поиск пути и возвращает статистику.
|
||||
|
||||
Args:
|
||||
maze_name: название лабиринта для отчёта
|
||||
|
||||
Returns:
|
||||
SearchStats с результатами поиска
|
||||
"""
|
||||
if not self.strategy:
|
||||
raise ValueError("Стратегия не установлена! Вызовите set_strategy()")
|
||||
|
||||
# Уведомляем наблюдателей
|
||||
self._notify_observers("search_started")
|
||||
|
||||
# Замер времени
|
||||
start_time = time.perf_counter()
|
||||
|
||||
# Запускаем алгоритм (Strategy делает всю работу)
|
||||
path, visited_count = self.strategy.find_path(
|
||||
self.maze, self.maze.start, self.maze.exit
|
||||
)
|
||||
|
||||
# Останавливаем замер
|
||||
end_time = time.perf_counter()
|
||||
time_ms = (end_time - start_time) * 1000
|
||||
|
||||
# Уведомляем о результате
|
||||
event = "path_found" if path else "no_path"
|
||||
self._notify_observers(event)
|
||||
|
||||
# Формируем статистику
|
||||
return SearchStats(
|
||||
time_ms=time_ms,
|
||||
visited_cells=visited_count,
|
||||
path_length=len(path),
|
||||
algorithm_name=self.strategy.get_name(),
|
||||
maze_name=maze_name
|
||||
)
|
||||
54
pogodinda/lab2/src/observer.py
Normal file
54
pogodinda/lab2/src/observer.py
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List, Optional
|
||||
|
||||
from maze import Maze, Cell
|
||||
|
||||
|
||||
class Observer(ABC):
|
||||
"""Интерфейс наблюдателя (Observer pattern)."""
|
||||
|
||||
@abstractmethod
|
||||
def update(self, event: str):
|
||||
pass
|
||||
|
||||
|
||||
class ConsoleView(Observer):
|
||||
"""
|
||||
Консольное представление лабиринта.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.events: List[str] = []
|
||||
|
||||
def update(self, event: str):
|
||||
"""Получаем уведомление о событии."""
|
||||
self.events.append(event)
|
||||
print(f"[Observer] Событие: {event}")
|
||||
|
||||
def render(self, maze: Maze, player_position: Cell = None, path: List[Cell] = None):
|
||||
"""
|
||||
Отрисовка лабиринта в консоли.
|
||||
"""
|
||||
path_set = set(path) if path else set()
|
||||
|
||||
for y in range(maze.height):
|
||||
row = []
|
||||
for x in range(maze.width):
|
||||
cell = maze.get_cell(x, y)
|
||||
|
||||
if player_position and cell == player_position:
|
||||
row.append('P')
|
||||
elif cell in path_set:
|
||||
row.append('*')
|
||||
else:
|
||||
row.append(str(cell))
|
||||
|
||||
print(''.join(row))
|
||||
print()
|
||||
|
||||
def render_stats(self, stats):
|
||||
"""Отрисовка статистики поиска."""
|
||||
print(f"Алгоритм: {stats.algorithm_name}")
|
||||
print(f"Время: {stats.time_ms:.4f} мс")
|
||||
print(f"Посещено клеток: {stats.visited_cells}")
|
||||
print(f"Длина пути: {stats.path_length}")
|
||||
183
pogodinda/lab2/src/pathfinding.py
Normal file
183
pogodinda/lab2/src/pathfinding.py
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import List, Tuple
|
||||
from collections import deque
|
||||
import heapq
|
||||
from maze import Maze, Cell
|
||||
|
||||
|
||||
class PathFindingStrategy(ABC):
|
||||
"""Интерфейс стратегии поиска пути (Strategy pattern)."""
|
||||
|
||||
@abstractmethod
|
||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
|
||||
"""
|
||||
Возвращает:
|
||||
- путь (список клеток от старта до выхода)
|
||||
- количество посещённых клеток
|
||||
Если пути нет — возвращает ([], 0).
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_name(self) -> str:
|
||||
"""Название алгоритма для отчёта."""
|
||||
pass
|
||||
|
||||
|
||||
class BFSStrategy(PathFindingStrategy):
|
||||
"""BFS — поиск в ширину. Гарантирует кратчайший путь по числу шагов."""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "BFS"
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
|
||||
if not start or not exit:
|
||||
return [], 0
|
||||
|
||||
# Очередь: (текущая_клетка, путь_до_неё)
|
||||
queue = deque([(start, [start])])
|
||||
visited = {start}
|
||||
visited_count = 1
|
||||
|
||||
while queue:
|
||||
current, path = queue.popleft()
|
||||
|
||||
if current == exit:
|
||||
return path, visited_count
|
||||
|
||||
# Проверяем всех соседей
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
visited_count += 1
|
||||
queue.append((neighbor, path + [neighbor]))
|
||||
|
||||
return [], visited_count
|
||||
|
||||
|
||||
class DFSStrategy(PathFindingStrategy):
|
||||
"""DFS — поиск в глубину. Быстрый, но путь не обязательно кратчайший."""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "DFS"
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
|
||||
if not start or not exit:
|
||||
return [], 0
|
||||
|
||||
# Стек: (текущая_клетка, путь_до_неё)
|
||||
stack = [(start, [start])]
|
||||
visited = {start}
|
||||
visited_count = 1
|
||||
|
||||
while stack:
|
||||
current, path = stack.pop()
|
||||
|
||||
if current == exit:
|
||||
return path, visited_count
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
visited_count += 1
|
||||
stack.append((neighbor, path + [neighbor]))
|
||||
|
||||
return [], visited_count
|
||||
|
||||
|
||||
class AStarStrategy(PathFindingStrategy):
|
||||
"""
|
||||
A* — поиск с эвристикой.
|
||||
Использует приоритетную очередь (кучу) и манхэттенское расстояние.
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "A*"
|
||||
|
||||
def _heuristic(self, cell: Cell, exit: Cell) -> int:
|
||||
"""Манхэттенское расстояние: |x1-x2| + |y1-y2|."""
|
||||
if not cell or not exit:
|
||||
return 0
|
||||
return abs(cell.x - exit.x) + abs(cell.y - exit.y)
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
|
||||
if not start or not exit:
|
||||
return [], 0
|
||||
|
||||
# Куча: (f_score, счётчик, клетка, путь, g_score)
|
||||
# f = g + h, где g — пройденный путь, h — эвристика
|
||||
counter = 0
|
||||
open_set = [(self._heuristic(start, exit), counter, start, [start], 0)]
|
||||
visited = set()
|
||||
visited_count = 0
|
||||
g_scores = {start: 0}
|
||||
|
||||
while open_set:
|
||||
_, _, current, path, g_score = heapq.heappop(open_set)
|
||||
|
||||
if current in visited:
|
||||
continue
|
||||
|
||||
visited.add(current)
|
||||
visited_count += 1
|
||||
|
||||
if current == exit:
|
||||
return path, visited_count
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
if neighbor in visited:
|
||||
continue
|
||||
|
||||
tentative_g = g_score + 1
|
||||
|
||||
if neighbor not in g_scores or tentative_g < g_scores[neighbor]:
|
||||
g_scores[neighbor] = tentative_g
|
||||
f_score = tentative_g + self._heuristic(neighbor, exit)
|
||||
counter += 1
|
||||
heapq.heappush(open_set, (f_score, counter, neighbor, path + [neighbor], tentative_g))
|
||||
|
||||
return [], visited_count
|
||||
|
||||
|
||||
class DijkstraStrategy(PathFindingStrategy):
|
||||
"""
|
||||
Дейкстра — для взвешенных графов.
|
||||
В базовом варианте (вес=1) работает как BFS, но с приоритетной очередью.
|
||||
"""
|
||||
|
||||
def get_name(self) -> str:
|
||||
return "Dijkstra"
|
||||
|
||||
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> Tuple[List[Cell], int]:
|
||||
if not start or not exit:
|
||||
return [], 0
|
||||
|
||||
# Куча: (расстояние, счётчик, клетка, путь)
|
||||
counter = 0
|
||||
pq = [(0, counter, start, [start])]
|
||||
distances = {start: 0}
|
||||
visited = set()
|
||||
visited_count = 0
|
||||
|
||||
while pq:
|
||||
dist, _, current, path = heapq.heappop(pq)
|
||||
|
||||
if current in visited:
|
||||
continue
|
||||
|
||||
visited.add(current)
|
||||
visited_count += 1
|
||||
|
||||
if current == exit:
|
||||
return path, visited_count
|
||||
|
||||
for neighbor in maze.get_neighbors(current):
|
||||
weight = neighbor.weight
|
||||
new_dist = dist + weight
|
||||
|
||||
if neighbor not in distances or new_dist < distances[neighbor]:
|
||||
distances[neighbor] = new_dist
|
||||
counter += 1
|
||||
heapq.heappush(pq, (new_dist, counter, neighbor, path + [neighbor]))
|
||||
|
||||
return [], visited_count
|
||||
0
pogodinda/lab2/tests/__init__.py
Normal file
0
pogodinda/lab2/tests/__init__.py
Normal file
26
pogodinda/lab2/tests/test_builder.py
Normal file
26
pogodinda/lab2/tests/test_builder.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from src.maze_builder import TextFileMazeBuilder, RandomMazeBuilder
|
||||
# Тест 1: Загрузка из файла
|
||||
print("=" * 50)
|
||||
print("ТЕСТ 1: Загрузка из файла")
|
||||
print("=" * 50)
|
||||
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("data/demo_maze.txt")
|
||||
|
||||
print(maze)
|
||||
print(f"\nРазмер: {maze.width}x{maze.height}")
|
||||
print(f"Старт: ({maze.start.x}, {maze.start.y})")
|
||||
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
|
||||
|
||||
# Тест 2: Случайный лабиринт
|
||||
print("\n" + "=" * 50)
|
||||
print("ТЕСТ 2: Случайный лабиринт 21x11")
|
||||
print("=" * 50)
|
||||
|
||||
random_builder = RandomMazeBuilder(21, 11)
|
||||
random_maze = random_builder.build_from_file()
|
||||
|
||||
print(random_maze)
|
||||
print(f"\nРазмер: {random_maze.width}x{random_maze.height}")
|
||||
print(f"Старт: ({random_maze.start.x}, {random_maze.start.y})")
|
||||
print(f"Выход: ({random_maze.exit.x}, {random_maze.exit.y})")
|
||||
19
pogodinda/lab2/tests/test_maze.py
Normal file
19
pogodinda/lab2/tests/test_maze.py
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
from src.maze import Maze, Cell
|
||||
|
||||
maze = Maze(5, 5)
|
||||
|
||||
# Заполняем границы стенами
|
||||
for y in range(5):
|
||||
for x in range(5):
|
||||
if x == 0 or x == 4 or y == 0 or y == 4:
|
||||
maze.set_cell(x, y, Cell(x, y, is_wall=True))
|
||||
|
||||
# Старт, выход, одна стена внутри
|
||||
maze.set_cell(1, 1, Cell(1, 1, is_start=True))
|
||||
maze.set_cell(3, 3, Cell(3, 3, is_exit=True))
|
||||
maze.set_cell(2, 2, Cell(2, 2, is_wall=True))
|
||||
|
||||
print(maze)
|
||||
print("Старт:", maze.start)
|
||||
print("Выход:", maze.exit)
|
||||
print("Соседи старта:", maze.get_neighbors(maze.start))
|
||||
45
pogodinda/lab2/tests/test_observer_command.py
Normal file
45
pogodinda/lab2/tests/test_observer_command.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from src.maze_builder import TextFileMazeBuilder
|
||||
from src.maze_solver import MazeSolver
|
||||
from src.pathfinding import AStarStrategy
|
||||
from src.observer import ConsoleView
|
||||
from src.commands import Player, MoveCommand
|
||||
|
||||
# Загружаем лабиринт
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("demo_maze.txt")
|
||||
|
||||
print("=" * 50)
|
||||
print("ТЕСТ OBSERVER")
|
||||
print("=" * 50)
|
||||
|
||||
solver = MazeSolver(maze, AStarStrategy())
|
||||
console = ConsoleView()
|
||||
solver.add_observer(console)
|
||||
|
||||
stats = solver.solve("demo_maze")
|
||||
console.render_stats(stats)
|
||||
|
||||
print(f"\nСобытия: {console.events}")
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print("ТЕСТ COMMAND")
|
||||
print("=" * 50)
|
||||
|
||||
player = Player(maze.start)
|
||||
print(f"Начальная позиция: {player}")
|
||||
console.render(maze, player.current_cell)
|
||||
|
||||
cmd1 = MoveCommand(player, maze, 'S')
|
||||
success = cmd1.execute()
|
||||
print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}")
|
||||
|
||||
cmd2 = MoveCommand(player, maze, 'S')
|
||||
success = cmd2.execute()
|
||||
print(f"Движение S: {'успешно' if success else 'не удалось'} → {player}")
|
||||
|
||||
console.render(maze, player.current_cell)
|
||||
|
||||
print("\nОтмена последнего хода:")
|
||||
cmd2.undo()
|
||||
print(f"После undo: {player}")
|
||||
console.render(maze, player.current_cell)
|
||||
58
pogodinda/lab2/tests/test_solver.py
Normal file
58
pogodinda/lab2/tests/test_solver.py
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
from src.maze_builder import TextFileMazeBuilder
|
||||
from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
from src.maze_solver import MazeSolver
|
||||
|
||||
# Загружаем лабиринт
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("demo_maze.txt")
|
||||
|
||||
print("Лабиринт:")
|
||||
print(maze)
|
||||
print(f"\nСтарт: ({maze.start.x}, {maze.start.y})")
|
||||
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
|
||||
|
||||
# Создаём solver без стратегии
|
||||
solver = MazeSolver(maze)
|
||||
|
||||
# Тест 1: BFS
|
||||
print(f"\n{'='*50}")
|
||||
print("ТЕСТ 1: BFS")
|
||||
solver.set_strategy(BFSStrategy())
|
||||
stats = solver.solve("demo_maze")
|
||||
print(f"Время: {stats.time_ms:.4f} мс")
|
||||
print(f"Посещено: {stats.visited_cells}")
|
||||
print(f"Длина пути: {stats.path_length}")
|
||||
|
||||
# Тест 2: DFS
|
||||
print(f"\n{'='*50}")
|
||||
print("ТЕСТ 2: DFS")
|
||||
solver.set_strategy(DFSStrategy())
|
||||
stats = solver.solve("demo_maze")
|
||||
print(f"Время: {stats.time_ms:.4f} мс")
|
||||
print(f"Посещено: {stats.visited_cells}")
|
||||
print(f"Длина пути: {stats.path_length}")
|
||||
|
||||
# Тест 3: A*
|
||||
print(f"\n{'='*50}")
|
||||
print("ТЕСТ 3: A*")
|
||||
solver.set_strategy(AStarStrategy())
|
||||
stats = solver.solve("demo_maze")
|
||||
print(f"Время: {stats.time_ms:.4f} мс")
|
||||
print(f"Посещено: {stats.visited_cells}")
|
||||
print(f"Длина пути: {stats.path_length}")
|
||||
|
||||
# Тест 4: Дейкстра
|
||||
print(f"\n{'='*50}")
|
||||
print("ТЕСТ 4: Дейкстра")
|
||||
solver.set_strategy(DijkstraStrategy())
|
||||
stats = solver.solve("demo_maze")
|
||||
print(f"Время: {stats.time_ms:.4f} мс")
|
||||
print(f"Посещено: {stats.visited_cells}")
|
||||
print(f"Длина пути: {stats.path_length}")
|
||||
|
||||
# Тест 5: Динамическая смена алгоритма
|
||||
print(f"\n{'='*50}")
|
||||
print("ТЕСТ 5: Смена алгоритма на лету")
|
||||
print("Было:", solver.strategy.get_name())
|
||||
solver.set_strategy(BFSStrategy())
|
||||
print("Стало:", solver.strategy.get_name())
|
||||
34
pogodinda/lab2/tests/test_strategy.py
Normal file
34
pogodinda/lab2/tests/test_strategy.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from src.maze_builder import TextFileMazeBuilder
|
||||
from src.pathfinding import BFSStrategy, DFSStrategy, AStarStrategy, DijkstraStrategy
|
||||
|
||||
# Загружаем лабиринт
|
||||
builder = TextFileMazeBuilder()
|
||||
maze = builder.build_from_file("data/demo_maze.txt")
|
||||
|
||||
print("Лабиринт:")
|
||||
print(maze)
|
||||
print(f"\nСтарт: ({maze.start.x}, {maze.start.y})")
|
||||
print(f"Выход: ({maze.exit.x}, {maze.exit.y})")
|
||||
|
||||
# Тестируем все алгоритмы
|
||||
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
|
||||
|
||||
for strategy in strategies:
|
||||
print(f"\n{'='*40}")
|
||||
print(f"Алгоритм: {strategy.get_name()}")
|
||||
print('='*40)
|
||||
|
||||
path, visited = strategy.find_path(maze, maze.start, maze.exit)
|
||||
|
||||
print(f"Посещено клеток: {visited}")
|
||||
print(f"Длина пути: {len(path)}")
|
||||
|
||||
if path:
|
||||
print("Путь найден!") # ← убрал f, или добавь переменную
|
||||
if len(path) > 10:
|
||||
print(f"Начало: {[(c.x, c.y) for c in path[:5]]}...")
|
||||
print(f"Конец: ...{[(c.x, c.y) for c in path[-5:]]}")
|
||||
else:
|
||||
print(f"Путь: {[(c.x, c.y) for c in path]}")
|
||||
else:
|
||||
print("Путь не найден")
|
||||
Loading…
Reference in New Issue
Block a user