Merge pull request '[1] task 1' (#207) from osipovamd/2026-rff_mp:osipovamd into develop

Reviewed-on: #207
This commit is contained in:
AndreyUrs 2026-05-30 11:32:16 +00:00
commit f907f973d9
15 changed files with 1544 additions and 0 deletions

BIN
osipovamd/docs/Task1.docx Normal file

Binary file not shown.

View File

@ -0,0 +1,19 @@
Структура,Режим,Операция,Замер1,Замер2,Замер3,Среднее
LinkedList,случайный,вставка,0.017595,0.018368,0.016781,0.017581
LinkedList,случайный,поиск,0.002300,0.002235,0.002287,0.002274
LinkedList,случайный,удаление,0.000868,0.000819,0.000819,0.000835
LinkedList,отсортированный,вставка,0.014638,0.014335,0.014226,0.014400
LinkedList,отсортированный,поиск,0.001955,0.001930,0.001897,0.001928
LinkedList,отсортированный,удаление,0.000975,0.000984,0.000998,0.000986
HashTable,случайный,вставка,0.001593,0.001598,0.001441,0.001544
HashTable,случайный,поиск,0.000180,0.000156,0.000154,0.000163
HashTable,случайный,удаление,0.000070,0.000068,0.000069,0.000069
HashTable,отсортированный,вставка,0.001369,0.001375,0.001383,0.001376
HashTable,отсортированный,поиск,0.000168,0.000160,0.000148,0.000159
HashTable,отсортированный,удаление,0.000080,0.000077,0.000076,0.000078
BST,случайный,вставка,0.000513,0.000501,0.000514,0.000509
BST,случайный,поиск,0.000077,0.000059,0.000054,0.000063
BST,случайный,удаление,0.000044,0.000042,0.000039,0.000042
BST,отсортированный,вставка,0.019008,0.018696,0.018868,0.018857
BST,отсортированный,поиск,0.001828,0.001813,0.001811,0.001817
BST,отсортированный,удаление,0.001735,0.001842,0.001617,0.001731
1 Структура Режим Операция Замер1 Замер2 Замер3 Среднее
2 LinkedList случайный вставка 0.017595 0.018368 0.016781 0.017581
3 LinkedList случайный поиск 0.002300 0.002235 0.002287 0.002274
4 LinkedList случайный удаление 0.000868 0.000819 0.000819 0.000835
5 LinkedList отсортированный вставка 0.014638 0.014335 0.014226 0.014400
6 LinkedList отсортированный поиск 0.001955 0.001930 0.001897 0.001928
7 LinkedList отсортированный удаление 0.000975 0.000984 0.000998 0.000986
8 HashTable случайный вставка 0.001593 0.001598 0.001441 0.001544
9 HashTable случайный поиск 0.000180 0.000156 0.000154 0.000163
10 HashTable случайный удаление 0.000070 0.000068 0.000069 0.000069
11 HashTable отсортированный вставка 0.001369 0.001375 0.001383 0.001376
12 HashTable отсортированный поиск 0.000168 0.000160 0.000148 0.000159
13 HashTable отсортированный удаление 0.000080 0.000077 0.000076 0.000078
14 BST случайный вставка 0.000513 0.000501 0.000514 0.000509
15 BST случайный поиск 0.000077 0.000059 0.000054 0.000063
16 BST случайный удаление 0.000044 0.000042 0.000039 0.000042
17 BST отсортированный вставка 0.019008 0.018696 0.018868 0.018857
18 BST отсортированный поиск 0.001828 0.001813 0.001811 0.001817
19 BST отсортированный удаление 0.001735 0.001842 0.001617 0.001731

481
osipovamd/docs/task1.py Normal file
View File

@ -0,0 +1,481 @@
import time
import random
import csv
import matplotlib.pyplot as plt
import numpy as np
import sys
from collections import defaultdict
# Увеличиваем лимит рекурсии для BST
sys.setrecursionlimit(10000)
def ll_insert(head, name, phone):
current = head
while current:
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
new_node = {'name': name, 'phone': phone, 'next': head}
return new_node
def ll_find(head, name):
current = head
while current:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if not head:
return None
if head['name'] == name:
return head['next']
current = head
while current['next']:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
records = []
current = head
while current:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def hash_function(name, size):
return sum(ord(c) for c in name) % size
def ht_create(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):
records = []
for bucket in buckets:
current = bucket
while current:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def bst_insert(root, name, phone):
"""Итеративная вставка для избежания RecursionError"""
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
break
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
break
current = current['right']
else:
current['phone'] = phone
break
return root
def bst_find(root, name):
current = root
while current:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_find_min(node):
current = node
while current and current['left']:
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 = []
stack = []
current = root
while stack or current:
while current:
stack.append(current)
current = current['left']
current = stack.pop()
records.append((current['name'], current['phone']))
current = current['right']
return records
def copy_linked_list(head):
if not head:
return None
new_head = {'name': head['name'], 'phone': head['phone'], 'next': None}
current_new = new_head
current_old = head['next']
while current_old:
current_new['next'] = {'name': current_old['name'], 'phone': current_old['phone'], 'next': None}
current_new = current_new['next']
current_old = current_old['next']
return new_head
def copy_bst(node):
if not node:
return None
return {
'name': node['name'],
'phone': node['phone'],
'left': copy_bst(node['left']),
'right': copy_bst(node['right'])
}
def generate_test_data(N=10000):
names = [f"User_{i:05d}" for i in range(N)]
records = [(name, f"+7-999-{random.randint(1000000, 9999999)}") for name in names]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def get_test_queries(records, num_existing=100, num_nonexisting=10):
existing_names = [name for name, _ in random.sample(records, min(num_existing, len(records)))]
nonexisting_names = [f"None_{i:05d}" for i in range(num_nonexisting)]
queries = existing_names + nonexisting_names
random.shuffle(queries)
return queries
def get_delete_names(records, num_to_delete=50):
return [name for name, _ in random.sample(records, min(num_to_delete, len(records)))]
def measure_insertion(structure_type, records, repeats=3):
times = []
for _ in range(repeats):
if structure_type == "LinkedList":
structure = None
insert_func = ll_insert
elif structure_type == "HashTable":
structure = ht_create(2000)
insert_func = ht_insert
elif structure_type == "BST":
structure = None
insert_func = bst_insert
else:
raise ValueError(f"Unknown structure: {structure_type}")
start = time.perf_counter()
for name, phone in records:
if structure_type == "HashTable":
insert_func(structure, name, phone)
else:
structure = insert_func(structure, name, phone)
end = time.perf_counter()
times.append(end - start)
return times
def measure_search(structure_type, structure, queries, repeats=3):
times = []
for _ in range(repeats):
start = time.perf_counter()
for name in queries:
if structure_type == "LinkedList":
ll_find(structure, name)
elif structure_type == "HashTable":
ht_find(structure, name)
elif structure_type == "BST":
bst_find(structure, name)
end = time.perf_counter()
times.append(end - start)
return times
def measure_deletion(structure_type, structure, names_to_delete, repeats=3):
times = []
for _ in range(repeats):
if structure_type == "LinkedList":
temp_structure = copy_linked_list(structure)
delete_func = ll_delete
elif structure_type == "HashTable":
temp_structure = structure.copy()
for i in range(len(temp_structure)):
if temp_structure[i]:
temp_structure[i] = copy_linked_list(temp_structure[i])
delete_func = ht_delete
elif structure_type == "BST":
temp_structure = copy_bst(structure)
delete_func = bst_delete
start = time.perf_counter()
for name in names_to_delete:
if structure_type == "HashTable":
delete_func(temp_structure, name)
else:
temp_structure = delete_func(temp_structure, name)
end = time.perf_counter()
times.append(end - start)
return times
def run_experiment(N=2000):
print(f"Генерация тестовых данных (N={N})...")
records_shuffled, records_sorted = generate_test_data(N)
queries = get_test_queries(records_shuffled, num_existing=100, num_nonexisting=10)
delete_names = get_delete_names(records_shuffled, num_to_delete=50)
structures = ["LinkedList", "HashTable", "BST"]
modes = ["случайный", "отсортированный"]
results = []
print("\nНачало экспериментов:")
for structure in structures:
print(f"\nТестирование {structure}...")
for mode in modes:
print(f" Режим: {mode}")
records = records_shuffled if mode == "случайный" else records_sorted
print(f" Измерение вставки...")
try:
insert_times = measure_insertion(structure, records, repeats=3)
avg_insert = sum(insert_times) / len(insert_times)
except RecursionError:
print(f" ОШИБКА: Превышена глубина рекурсии при вставке в {structure} для {mode} режима")
continue
print(f" Создание финальной структуры...")
if structure == "LinkedList":
final_structure = None
for name, phone in records:
final_structure = ll_insert(final_structure, name, phone)
elif structure == "HashTable":
final_structure = ht_create(2000)
for name, phone in records:
ht_insert(final_structure, name, phone)
elif structure == "BST":
final_structure = None
for name, phone in records:
final_structure = bst_insert(final_structure, name, phone)
print(f" Измерение поиска...")
search_times = measure_search(structure, final_structure, queries, repeats=3)
avg_search = sum(search_times) / len(search_times)
print(f" Измерение удаления...")
deletion_times = measure_deletion(structure, final_structure, delete_names, repeats=3)
avg_deletion = sum(deletion_times) / len(deletion_times)
results.append({
"Структура": structure,
"Режим": mode,
"Операция": "вставка",
"Замеры": insert_times,
"Среднее": avg_insert
})
results.append({
"Структура": structure,
"Режим": mode,
"Операция": "поиск",
"Замеры": search_times,
"Среднее": avg_search
})
results.append({
"Структура": structure,
"Режим": mode,
"Операция": "удаление",
"Замеры": deletion_times,
"Среднее": avg_deletion
})
print(f" Вставка: {avg_insert:.6f} сек")
print(f" Поиск: {avg_search:.6f} сек")
print(f" Удаление: {avg_deletion:.6f} сек")
return results
import os
import csv
from datetime import datetime
def save_to_csv(results, filename="results.csv"):
save_dir = "/Users/mariiaos/2026-rff_mp/osipovamd/docs"
filepath = os.path.join(save_dir, filename)
with open(filepath, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Структура", "Режим", "Операция", "Замер1", "Замер2", "Замер3", "Среднее"])
for res in results:
row = [
res["Структура"],
res["Режим"],
res["Операция"],
*[f"{t:.6f}" for t in res["Замеры"]],
f"{res['Среднее']:.6f}"
]
writer.writerow(row)
print(f"\nРезультаты сохранены в: {filepath}")
return filepath
def plot_results(results):
if not results:
print("Нет данных для построения графиков!")
return
plt.style.use('seaborn-v0_8-darkgrid')
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
operations = ["вставка", "поиск", "удаление"]
structures = ["LinkedList", "HashTable", "BST"]
modes = ["случайный", "отсортированный"]
colors = {'LinkedList': '#FF6B6B', 'HashTable': '#4ECDC4', 'BST': '#45B7D1'}
for idx, operation in enumerate(operations):
ax = axes[idx]
x = np.arange(len(modes))
width = 0.25
multiplier = 0
for structure in structures:
values = []
for mode in modes:
found = False
for res in results:
if (res["Структура"] == structure and
res["Режим"] == mode and
res["Операция"] == operation):
values.append(res["Среднее"])
found = True
break
if not found:
values.append(0)
if max(values) > 0:
offset = width * multiplier
bars = ax.bar(x + offset, values, width, label=structure, color=colors[structure])
multiplier += 1
ax.set_xlabel('Режим данных', fontsize=12)
ax.set_ylabel('Время (секунды)', fontsize=12)
ax.set_title(f'{operation.capitalize()}', fontsize=14, fontweight='bold')
ax.set_xticks(x + width)
ax.set_xticklabels(modes)
ax.legend(loc='upper left')
ax.grid(True, alpha=0.3)
plt.suptitle('Сравнение производительности структур данных',
fontsize=16, fontweight='bold')
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=300, bbox_inches='tight')
plt.show()
if __name__ == "__main__":
print("тестирование производительности структур данных")
results = run_experiment(N=1000)
save_to_csv(results)
if results:
print("\nПостроение графиков...")
plot_results(results)
print("Сводная таблица результатов (среднее время в секундах)")
print(f"{'Структура':<12} {'Режим':<12} {'Вставка':<10} {'Поиск':<10} {'Удаление':<10}")
for structure in ["LinkedList", "HashTable", "BST"]:
for mode in ["случайный", "отсортированный"]:
insert_time = search_time = delete_time = 0
for res in results:
if res["Структура"] == structure and res["Режим"] == mode:
if res["Операция"] == "вставка":
insert_time = res["Среднее"]
elif res["Операция"] == "поиск":
search_time = res["Среднее"]
elif res["Операция"] == "удаление":
delete_time = res["Среднее"]
if insert_time > 0 or search_time > 0 or delete_time > 0:
print(f"{structure:<12} {mode:<12} {insert_time:<10.6f} {search_time:<10.6f} {delete_time:<10.6f}")
else:
print("\nЭксперимент не дал результатов из-за ошибок.")
print("\nЭксперимент завершён!")

View File

@ -0,0 +1,283 @@
"""
Экспериментальный запуск для всех лабиринтов и алгоритмов
Создание CSV и графиков
"""
import os
import csv
from datetime import datetime
from maze_model import Maze
from maze_builder import TextFileMazeBuilder
from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy
from maze_solver import MazeSolver, SearchStats
class ExperimentRunner:
def __init__(self):
self.all_results = []
self.labirints = {
'labirint1.txt': 'Маленький (10x10) с простым путём',
'labirint2.txt': 'Средний (50x50) с тупиками',
'labirint3.txt': 'Большой (100x100) запутанный',
'labirint4.txt': 'Пустой (20x20) без стен',
'labirint5.txt': 'Без выхода (20x20)'
}
self.strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
def run_all_experiments(self):
"""Запускает эксперименты для всех лабиринтов и алгоритмов"""
print("\n" + "="*70)
print("ЗАПУСК ЭКСПЕРИМЕНТОВ")
print("="*70)
builder = TextFileMazeBuilder()
for filename, description in self.labirints.items():
if not os.path.exists(filename):
print(f"\n⚠️ Файл {filename} не найден, пропускаем...")
continue
print(f"\n📁 Лабиринт: {description}")
print(f" Файл: {filename}")
print("-" * 50)
try:
maze = builder.build_from_file(filename)
maze_name = filename.replace('.txt', '')
for strategy in self.strategies:
print(f" Тестирование {strategy.get_name()}...", end=" ", flush=True)
solver = MazeSolver(maze, maze_name, strategy)
path, stats = solver.solve_with_stats()
self.all_results.append({
'лабиринт': description,
'стратегия': stats.algorithm_name,
'время_мс': stats.execution_time_ms,
'посещено_клеток': stats.visited_cells,
'длина_пути': stats.path_length,
'путь_найден': 'Да' if stats.path_found else 'Нет',
'размер': stats.maze_size
})
print(f"готово! время={stats.execution_time_ms:.3f}мс, путь={stats.path_length}")
except Exception as e:
print(f" ❌ Ошибка: {e}")
print("\n" + "="*70)
print("ЭКСПЕРИМЕНТЫ ЗАВЕРШЕНЫ")
print("="*70)
def save_to_csv(self, filename="experiment_results.csv"):
"""Сохраняет результаты в CSV"""
if not self.all_results:
print("Нет результатов для сохранения!")
return
with open(filename, 'w', newline='', encoding='utf-8-sig') as f:
fieldnames = ['лабиринт', 'стратегия', 'время_мс', 'посещено_клеток', 'длина_пути', 'путь_найден', 'размер']
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(self.all_results)
print(f"\n✅ Результаты сохранены в {filename}")
# Показываем содержимое CSV
print("\n" + "="*70)
print("СОДЕРЖИМОЕ CSV ФАЙЛА:")
print("="*70)
with open(filename, 'r', encoding='utf-8-sig') as f:
print(f.read())
def create_charts(self):
"""Создаёт графики для каждого лабиринта"""
try:
import matplotlib.pyplot as plt
import numpy as np
print("\n" + "="*70)
print("ПОСТРОЕНИЕ ГРАФИКОВ")
print("="*70)
# Группируем результаты по лабиринтам
results_by_maze = {}
for result in self.all_results:
maze = result['лабиринт']
if maze not in results_by_maze:
results_by_maze[maze] = []
results_by_maze[maze].append(result)
# Для каждого лабиринта создаём отдельный график
for maze_name, maze_results in results_by_maze.items():
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
fig.suptitle(f'Сравнение алгоритмов: {maze_name}', fontsize=14, fontweight='bold')
algorithms = [r['стратегия'] for r in maze_results]
# График 1: Время выполнения
times = [r['время_мс'] for r in maze_results]
bars1 = axes[0].bar(algorithms, times, color=['blue', 'green', 'red'])
axes[0].set_ylabel('Время (мс)')
axes[0].set_title('Время выполнения')
axes[0].tick_params(axis='x', rotation=15)
for bar, val in zip(bars1, times):
axes[0].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.3f}', ha='center', va='bottom', fontsize=9)
# График 2: Посещённые клетки
visited = [r['посещено_клеток'] for r in maze_results]
bars2 = axes[1].bar(algorithms, visited, color=['blue', 'green', 'red'])
axes[1].set_ylabel('Количество клеток')
axes[1].set_title('Посещённые клетки')
axes[1].tick_params(axis='x', rotation=15)
for bar, val in zip(bars2, visited):
axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
f'{val:.0f}', ha='center', va='bottom', fontsize=9)
# График 3: Длина пути
lengths = [r['длина_пути'] for r in maze_results]
bars3 = axes[2].bar(algorithms, lengths, color=['blue', 'green', 'red'])
axes[2].set_ylabel('Шагов')
axes[2].set_title('Длина найденного пути')
axes[2].tick_params(axis='x', rotation=15)
for bar, val in zip(bars3, lengths):
axes[2].text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.0f}', ha='center', va='bottom', fontsize=9)
plt.tight_layout()
# Сохраняем график
safe_name = maze_name.replace(' ', '_').replace('(', '').replace(')', '').replace('×', 'x')
filename = f"chart_{safe_name}.png"
plt.savefig(filename, dpi=150, bbox_inches='tight')
print(f"✅ Сохранён: {filename}")
plt.close()
# Общий сводный график
self._create_summary_chart()
except ImportError:
print("\n⚠️ Для построения графиков установите matplotlib:")
print(" pip install matplotlib numpy")
def _create_summary_chart(self):
"""Создаёт сводный график по всем лабиринтам"""
import matplotlib.pyplot as plt
import numpy as np
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Сводное сравнение алгоритмов по всем лабиринтам', fontsize=14, fontweight='bold')
# Получаем уникальные лабиринты и алгоритмы
mazes = list(set([r['лабиринт'] for r in self.all_results]))
algorithms = ['BFS (Поиск в ширину)', 'DFS (Поиск в глубину)', 'A* (A-Star)']
# 1. Время по лабиринтам
ax1 = axes[0, 0]
x = np.arange(len(mazes))
width = 0.25
for i, algo in enumerate(algorithms):
times = []
for maze in mazes:
result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None)
times.append(result['время_мс'] if result else 0)
bars = ax1.bar(x + (i - 1) * width, times, width, label=algo)
for bar, val in zip(bars, times):
if val > 0:
ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.5,
f'{val:.1f}', ha='center', va='bottom', fontsize=7)
ax1.set_xlabel('Лабиринт')
ax1.set_ylabel('Время (мс)')
ax1.set_title('Сравнение времени выполнения')
ax1.set_xticks(x)
ax1.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right')
ax1.legend()
# 2. Посещённые клетки
ax2 = axes[0, 1]
for i, algo in enumerate(algorithms):
visited = []
for maze in mazes:
result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None)
visited.append(result['посещено_клеток'] if result else 0)
bars = ax2.bar(x + (i - 1) * width, visited, width, label=algo)
for bar, val in zip(bars, visited):
if val > 0:
ax2.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 5,
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
ax2.set_xlabel('Лабиринт')
ax2.set_ylabel('Посещённые клетки')
ax2.set_title('Сравнение посещённых клеток')
ax2.set_xticks(x)
ax2.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right')
ax2.legend()
# 3. Длина пути
ax3 = axes[1, 0]
for i, algo in enumerate(algorithms):
lengths = []
for maze in mazes:
result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None)
lengths.append(result['длина_пути'] if result else 0)
bars = ax3.bar(x + (i - 1) * width, lengths, width, label=algo)
for bar, val in zip(bars, lengths):
if val > 0:
ax3.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 1,
f'{val:.0f}', ha='center', va='bottom', fontsize=7)
ax3.set_xlabel('Лабиринт')
ax3.set_ylabel('Длина пути (шагов)')
ax3.set_title('Сравнение длины пути')
ax3.set_xticks(x)
ax3.set_xticklabels([m[:20] for m in mazes], rotation=45, ha='right')
ax3.legend()
# 4. Таблица результатов
ax4 = axes[1, 1]
ax4.axis('tight')
ax4.axis('off')
# Создаём таблицу
table_data = []
for maze in mazes:
row = [maze[:25]]
for algo in algorithms:
result = next((r for r in self.all_results if r['лабиринт'] == maze and r['стратегия'] == algo), None)
if result:
row.append(f"{result['время_мс']:.1f}мс")
else:
row.append("-")
table_data.append(row)
columns = ['Лабиринт', 'BFS', 'DFS', 'A*']
table = ax4.table(cellText=table_data, colLabels=columns, cellLoc='center', loc='center')
table.auto_set_font_size(False)
table.set_fontsize(9)
table.scale(1.2, 1.5)
ax4.set_title('Сводная таблица (время в мс)', fontsize=10)
plt.tight_layout()
plt.savefig('summary_chart.png', dpi=150, bbox_inches='tight')
print("Сохранён: summary_chart.png")
plt.close()
def main():
runner = ExperimentRunner()
runner.run_all_experiments()
runner.save_to_csv("experiment_results.csv")
runner.create_charts()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,18 @@
лабиринт,стратегия,время_мс,длина_пути,путь_найден,размер,датаремя
labirint1,BFS (Поиск в ширину),0.211,14,Да,10x10,2026-05-27 22:26:56
labirint2,BFS (Поиск в ширину),0.858,0,Нет,50x51,2026-05-27 22:27:10
labirint3,BFS (Поиск в ширину),0.055,0,Нет,100x100,2026-05-27 22:27:26
labirint4,BFS (Поиск в ширину),1.326,35,Да,20x20,2026-05-27 22:27:35
labirint5,BFS (Поиск в ширину),0.373,36,Да,21x20,2026-05-27 22:27:44
labirint1,DFS (Поиск в глубину),0.135,14,Да,10x10,2026-05-27 22:28:16
labirint2,DFS (Поиск в глубину),0.797,0,Нет,50x51,2026-05-27 22:28:25
labirint3,DFS (Поиск в глубину),0.047,0,Нет,100x100,2026-05-27 22:28:31
labirint4,DFS (Поиск в глубину),0.88,171,Да,20x20,2026-05-27 22:28:36
labirint5,DFS (Поиск в глубину),0.772,36,Да,21x20,2026-05-27 22:28:41
labirint1,A* (A-Star),0.311,14,Да,10x10,2026-05-27 22:28:45
labirint2,A* (A-Star),1.318,0,Нет,50x51,2026-05-27 22:28:50
labirint3,A* (A-Star),0.055,0,Нет,100x100,2026-05-27 22:28:55
labirint4,A* (A-Star),2.301,35,Да,20x20,2026-05-27 22:29:00
labirint5,A* (A-Star),0.684,36,Да,21x20,2026-05-27 22:29:04
labirint1,A* (A-Star),0.316,14,Да,10x10,2026-05-27 22:36:50
labirint1,DFS (Поиск в глубину),0.133,14,Да,10x10,2026-05-27 22:41:42
1 лабиринт стратегия время_мс длина_пути путь_найден размер дата_время
2 labirint1 BFS (Поиск в ширину) 0.211 14 Да 10x10 2026-05-27 22:26:56
3 labirint2 BFS (Поиск в ширину) 0.858 0 Нет 50x51 2026-05-27 22:27:10
4 labirint3 BFS (Поиск в ширину) 0.055 0 Нет 100x100 2026-05-27 22:27:26
5 labirint4 BFS (Поиск в ширину) 1.326 35 Да 20x20 2026-05-27 22:27:35
6 labirint5 BFS (Поиск в ширину) 0.373 36 Да 21x20 2026-05-27 22:27:44
7 labirint1 DFS (Поиск в глубину) 0.135 14 Да 10x10 2026-05-27 22:28:16
8 labirint2 DFS (Поиск в глубину) 0.797 0 Нет 50x51 2026-05-27 22:28:25
9 labirint3 DFS (Поиск в глубину) 0.047 0 Нет 100x100 2026-05-27 22:28:31
10 labirint4 DFS (Поиск в глубину) 0.88 171 Да 20x20 2026-05-27 22:28:36
11 labirint5 DFS (Поиск в глубину) 0.772 36 Да 21x20 2026-05-27 22:28:41
12 labirint1 A* (A-Star) 0.311 14 Да 10x10 2026-05-27 22:28:45
13 labirint2 A* (A-Star) 1.318 0 Нет 50x51 2026-05-27 22:28:50
14 labirint3 A* (A-Star) 0.055 0 Нет 100x100 2026-05-27 22:28:55
15 labirint4 A* (A-Star) 2.301 35 Да 20x20 2026-05-27 22:29:00
16 labirint5 A* (A-Star) 0.684 36 Да 21x20 2026-05-27 22:29:04
17 labirint1 A* (A-Star) 0.316 14 Да 10x10 2026-05-27 22:36:50
18 labirint1 DFS (Поиск в глубину) 0.133 14 Да 10x10 2026-05-27 22:41:42

View File

@ -0,0 +1,10 @@
##########
#S #
# ##### #
# # #
# ### # #
# # # #
# # ### #
# # #
# #####E#
##########

View File

@ -0,0 +1,51 @@
##################################################
#S #
# ############################################# #
# # # #
# # ######################################### # #
# # # # # #
# # # ##################################### # # #
# # # # # # # #
# # # # ################################# # # # #
# # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # # ##### # # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # # # #
# # # # # # # # # # ######### # # # # # # # # # #
# # # # # # # # # # # # # # # # # # # #
# # # # # # # # # ############# # # # # # # # # #
# # # # # # # # # # # # # # # # # #
# # # # # # # # ################# # # # # # # # #
# # # # # # # # # # # # # # # #
# # # # # # # ##################### # # # # # # #
# # # # # # # # # # # # # #
# # # # # # ######################### # # # # # #
# # # # # # # # # # # #
# # # # # ############################# # # # # #
# # # # # # # # # #
# # # # ################################# # # # #
# # # # # # # #
# # # ##################################### # # #
# # # # # #
# # ######################################### # #
# # # #
# ############################################# #
# #
##################################################
# E#
##################################################

View File

@ -0,0 +1,100 @@
####################################################################################################
####################################################################################################
##S# # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
####################################################################################################
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
####################################################################################################
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
####################################################################################################
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
####################################################################################################
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
####################################################################################################
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
####################################################################################################
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
####################################################################################################
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
####################################################################################################
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
####################################################################################################
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
####################################################################################################
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
####################################################################################################
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
####################################################################################################
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
####################################################################################################
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # #
### # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ##
## # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # #
## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # # # ## # # ## # E#
####################################################################################################

View File

@ -0,0 +1,20 @@
####################
#S #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# #
# E#
####################

View File

@ -0,0 +1,20 @@
####################
#S #
# ############### #
# # # #
# # ########### # #
# # # # # #
# # # ####### # # #
# # # # # # # #
# # # # ### # # # #
# # # # # # # # #
# # # # ##### # # #
# # # # # # #
# # # ######### # #
# # # # #
# # ############# #
# # #
# ###################
# #
# E#
####################

View File

@ -0,0 +1,161 @@
import os
import csv
from datetime import datetime
from maze_model import Maze
from maze_builder import TextFileMazeBuilder
from pathfinding_strategies import BFSStrategy, DFSStrategy, AStarStrategy
from maze_solver import MazeSolver
def get_maze_file():
maze_files = [f for f in os.listdir('.') if f.endswith('.txt') and f != 'experiment_results.csv']
if not maze_files:
print("\nНет файлов лабиринтов! Поместите файлы labirint1.txt и т.д. в папку.")
exit(1)
print("\nДоступные файлы лабиринтов:")
for i, f in enumerate(maze_files, 1):
print(f" {i}. {f}")
while True:
choice = input(f"\nВыберите файл (1-{len(maze_files)}): ").strip()
try:
idx = int(choice) - 1
if 0 <= idx < len(maze_files):
return maze_files[idx]
except ValueError:
pass
print(f"Неверный выбор. Введите число от 1 до {len(maze_files)}")
def display_maze_with_path(maze: Maze, path=None):
print("\n+" + "-" * maze.width + "+")
for y in range(maze.height):
row = "|"
for x in range(maze.width):
cell = maze.get_cell(x, y)
if cell == maze.start_cell:
row += "S"
elif cell == maze.exit_cell:
row += "E"
elif path and cell in path:
row += "."
elif cell.is_wall:
row += "#"
else:
row += " "
row += "|"
print(row)
print("+" + "-" * maze.width + "+")
def save_to_csv(maze_name: str, algorithm_name: str, time_ms: float, path_length: int, path_found: bool, maze_size: str):
csv_filename = "experiment_results.csv"
file_exists = os.path.exists(csv_filename)
with open(csv_filename, 'a', newline='', encoding='utf-8-sig') as f:
writer = csv.writer(f)
if not file_exists:
writer.writerow(['лабиринт', 'стратегия', 'время_мс', 'длина_пути', 'путь_найден', 'размер', 'датаремя'])
writer.writerow([
maze_name,
algorithm_name,
round(time_ms, 3),
path_length,
'Да' if path_found else 'Нет',
maze_size,
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
])
print(f"Результат сохранён в {csv_filename}")
def print_all_results():
csv_filename = "experiment_results.csv"
with open(csv_filename, 'r', encoding='utf-8-sig') as f:
reader = csv.reader(f)
headers = next(reader)
print(f"{headers[0]:<25} {headers[1]:<22} {headers[2]:<10} {headers[3]:<10} {headers[4]:<8} {headers[5]:<10}")
for row in reader:
print(f"{row[0]:<25} {row[1]:<22} {row[2]:<10} {row[3]:<10} {row[4]:<8} {row[5]:<10}")
def main():
filename = get_maze_file()
try:
builder = TextFileMazeBuilder()
maze = builder.build_from_file(filename)
maze_name = filename.replace('.txt', '')
maze_size = f"{maze.width}x{maze.height}"
print(f"\nЛабиринт загружен из файла: {filename}")
print(f" Размер: {maze_size}")
print(f" Старт: ({maze.start_cell.x}, {maze.start_cell.y})")
print(f" Выход: ({maze.exit_cell.x}, {maze.exit_cell.y})")
except FileNotFoundError:
print(f"\nФайл '{filename}' не найден!")
return
except ValueError as e:
print(f"\nОшибка в файле лабиринта: {e}")
return
print("\nЗагруженный лабиринт:")
maze.display()
print("ВЫБОР АЛГОРИТМА ПОИСКА")
print("1. BFS (Поиск в ширину)")
print("2. DFS (Поиск в глубину)")
print("3. A* (A-Star)")
choice = input("\nВыберите алгоритм (1-3): ").strip()
if choice == '1':
strategy = BFSStrategy()
elif choice == '2':
strategy = DFSStrategy()
elif choice == '3':
strategy = AStarStrategy()
else:
print("Неверный выбор!")
return
solver = MazeSolver(maze, strategy)
path, stats = solver.solve_with_stats()
print(stats.detailed_report())
if path:
print("\nЛабиринт с найденным путём (точки):")
display_maze_with_path(maze, path)
else:
print("Путь не найден!")
# Сохраняем результат в CSV
save_to_csv(
maze_name=maze_name,
algorithm_name=stats.algorithm_name,
time_ms=stats.execution_time_ms,
path_length=stats.path_length,
path_found=stats.path_found,
maze_size=maze_size
)
print("\nПрограмма завершена!")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,84 @@
from abc import ABC, abstractmethod
from maze_model import Maze
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
@abstractmethod
def build_from_string(self, content: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def __init__(self):
self._maze = None
self._lines = []
self._start_found = False
self._exit_found = False
def build_from_file(self, filename: str) -> Maze:
try:
with open(filename, 'r', encoding='utf-8') as file:
content = file.read()
return self.build_from_string(content)
except FileNotFoundError:
raise FileNotFoundError(f"Файл не найден: {filename}")
def build_from_string(self, content: str) -> Maze:
self._reset()
self._lines = [line.rstrip('\n\r') for line in content.split('\n')]
while self._lines and not self._lines[-1].strip():
self._lines.pop()
if not self._lines:
raise ValueError("Пустой файл лабиринта")
height = len(self._lines)
width = max(len(line) for line in self._lines)
for i, line in enumerate(self._lines):
if len(line) != width:
self._lines[i] = line.ljust(width)
self._maze = Maze(width, height)
for y, line in enumerate(self._lines):
for x, char in enumerate(line):
self._parse_cell(x, y, char)
self._validate_maze()
return self._maze
def _reset(self):
self._maze = None
self._lines = []
self._start_found = False
self._exit_found = False
def _parse_cell(self, x: int, y: int, char: str):
cell = self._maze.get_cell(x, y)
if char == '#':
cell.is_wall = True
elif char == 'S':
if self._start_found:
raise ValueError(f"Найден второй старт в ({x}, {y})")
self._maze.set_start(x, y)
self._start_found = True
elif char == 'E':
if self._exit_found:
raise ValueError(f"Найден второй выход в ({x}, {y})")
self._maze.set_exit(x, y)
self._exit_found = True
elif char == ' ':
pass
else:
raise ValueError(f"Неизвестный символ '{char}' в ({x}, {y})")
def _validate_maze(self):
if not self._start_found:
raise ValueError("В лабиринте не найден старт (символ 'S')")
if not self._exit_found:
raise ValueError("В лабиринте не найден выход (символ 'E')")

View File

@ -0,0 +1,103 @@
from typing import List, Optional
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
def __repr__(self) -> str:
if self.is_start:
return "S"
elif self.is_exit:
return "E"
elif self.is_wall:
return "#"
else:
return "."
def __eq__(self, other) -> bool:
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.y
def __hash__(self) -> int:
return hash((self.x, self.y))
def __lt__(self, other):
return (self.x, self.y) < (other.x, other.y)
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self._cells: List[List[Cell]] = []
self.start_cell: Optional[Cell] = None
self.exit_cell: 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 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 set_wall(self, x: int, y: int, is_wall: bool = True) -> None:
cell = self.get_cell(x, y)
if cell:
cell.is_wall = is_wall
def set_start(self, x: int, y: int) -> None:
cell = self.get_cell(x, y)
if cell:
if self.start_cell:
self.start_cell.is_start = False
cell.is_start = True
self.start_cell = cell
def set_exit(self, x: int, y: int) -> None:
cell = self.get_cell(x, y)
if cell:
if self.exit_cell:
self.exit_cell.is_exit = False
cell.is_exit = True
self.exit_cell = cell
def display(self) -> None:
print("+" + "-" * self.width + "+")
for y in range(self.height):
row_str = "|"
for x in range(self.width):
cell = self._cells[y][x]
if cell.is_start:
row_str += "S"
elif cell.is_exit:
row_str += "E"
elif cell.is_wall:
row_str += "#"
else:
row_str += " "
row_str += "|"
print(row_str)
print("+" + "-" * self.width + "+")

View File

@ -0,0 +1,71 @@
import time
from typing import List, Optional
from dataclasses import dataclass
from maze_model import Maze, Cell
from pathfinding_strategies import PathFindingStrategy
@dataclass
class SearchStats:
algorithm_name: str
path_length: int
execution_time_ms: float
path_found: bool
def __post_init__(self):
self.execution_time_ms = round(self.execution_time_ms, 3)
def summary(self) -> str:
if self.path_found:
return f"{self.algorithm_name}: путь найден | длина={self.path_length} | время={self.execution_time_ms} мс"
return f"{self.algorithm_name}: путь НЕ найден | время={self.execution_time_ms} мс"
def detailed_report(self) -> str:
separator = "=" * 50
report = f"\n{separator}\nОтчёт о поиске пути\n{separator}\n"
report += f"Алгоритм: {self.algorithm_name}\n"
report += f"Путь найден: {'Да' if self.path_found else 'Нет'}\n"
if self.path_found:
report += f"Длина пути: {self.path_length} шагов\n"
report += f"Время выполнения: {self.execution_time_ms} мс\n{separator}"
return report
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy = None):
self._maze = maze
self._strategy = strategy
self._last_stats: Optional[SearchStats] = None
self._validate_maze()
def _validate_maze(self):
if not self._maze.start_cell:
raise ValueError("Лабиринт не имеет стартовой клетки")
if not self._maze.exit_cell:
raise ValueError("Лабиринт не имеет выходной клетки")
def set_strategy(self, strategy: PathFindingStrategy) -> None:
self._strategy = strategy
def solve(self) -> List[Cell]:
if self._strategy is None:
raise ValueError("Стратегия не установлена")
start_time = time.perf_counter()
path = self._strategy.find_path(self._maze, self._maze.start_cell, self._maze.exit_cell)
end_time = time.perf_counter()
self._last_stats = SearchStats(
algorithm_name=self._strategy.get_name(),
path_length=len(path),
execution_time_ms=(end_time - start_time) * 1000,
path_found=len(path) > 0
)
return path
def solve_with_stats(self) -> tuple:
path = self.solve()
return path, self._last_stats
def get_last_stats(self) -> Optional[SearchStats]:
return self._last_stats

View File

@ -0,0 +1,123 @@
from abc import ABC, abstractmethod
from typing import List, Dict, Optional
from collections import deque
import heapq
from maze_model import Maze, Cell
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
queue = deque([start])
came_from: Dict[Cell, Optional[Cell]] = {start: None}
while queue:
current = queue.popleft()
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
queue.append(neighbor)
return []
def _reconstruct_path(self, came_from, start, exit_cell):
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
def get_name(self) -> str:
return "BFS (Поиск в ширину)"
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
stack = [start]
came_from: Dict[Cell, Optional[Cell]] = {start: None}
while stack:
current = stack.pop()
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
if neighbor not in came_from:
came_from[neighbor] = current
stack.append(neighbor)
return []
def _reconstruct_path(self, came_from, start, exit_cell):
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
def get_name(self) -> str:
return "DFS (Поиск в глубину)"
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, cell: Cell, target: Cell) -> int:
return abs(cell.x - target.x) + abs(cell.y - target.y)
def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:
if start == exit_cell:
return [start]
counter = 0
open_set = [(0, counter, start)]
g_score: Dict[Cell, float] = {start: 0}
came_from: Dict[Cell, Optional[Cell]] = {start: None}
open_set_cells = {start}
while open_set:
_, _, current = heapq.heappop(open_set)
open_set_cells.remove(current)
if current == exit_cell:
return self._reconstruct_path(came_from, start, exit_cell)
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if neighbor not in g_score or tentative_g < g_score[neighbor]:
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f = tentative_g + self._heuristic(neighbor, exit_cell)
if neighbor not in open_set_cells:
counter += 1
heapq.heappush(open_set, (f, counter, neighbor))
open_set_cells.add(neighbor)
return []
def _reconstruct_path(self, came_from, start, exit_cell):
path = []
current = exit_cell
while current is not None:
path.append(current)
current = came_from.get(current)
path.reverse()
return path
def get_name(self) -> str:
return "A* (A-Star)"