Merge pull request '[1,2]newlaba2' (#304) from ivanchenkoam/2026-rff_mp:newlaba2 into develop

Reviewed-on: UNN/2026-rff_mp#304
This commit is contained in:
AlexanderVah 2026-05-30 11:23:26 +00:00
commit 44919e72ee
20 changed files with 2745 additions and 0 deletions

537
ivanchenkoam/laba1.txt Normal file
View File

@ -0,0 +1,537 @@
import time
import csv
import random
import sys
from typing import List, Tuple, Optional, Any, Dict
#лимит рекурсии
sys.setrecursionlimit(20000)
def ll_insert(head: Optional[Dict], name: str, phone: str) -> Dict:
"""Вставка в конец связного списка"""
new_node = {'name': name, 'phone': phone, 'next': None}
if head is None:
return new_node
current = head
while current['next'] is not None:
# Обновляем, если уже есть
if current['name'] == name:
current['phone'] = phone
return head
current = current['next']
if current['name'] == name:
current['phone'] = phone
else:
current['next'] = new_node
return head
def ll_find(head: Optional[Dict], name: str) -> Optional[str]:
"""Поиск в связном списке"""
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head: Optional[Dict], name: str) -> Optional[Dict]:
"""Удаление из связного списка"""
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: Optional[Dict]) -> List[Tuple[str, str]]:
"""Сбор всех записей из связного списка с сортировкой"""
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
def hash_function(name: str, size: int) -> int:
"""Простая хеш-функция"""
return sum(ord(c) for c in name) % size
def ht_create(size: int = 1000) -> List[Optional[Dict]]:
"""Создание хеш-таблицы"""
return [None] * size
def ht_insert(buckets: List[Optional[Dict]], name: str, phone: str) -> None:
"""Вставка в хеш-таблицу"""
index = hash_function(name, len(buckets))
buckets[index] = ll_insert(buckets[index], name, phone)
def ht_find(buckets: List[Optional[Dict]], name: str) -> Optional[str]:
"""Поиск в хеш-таблице"""
index = hash_function(name, len(buckets))
return ll_find(buckets[index], name)
def ht_delete(buckets: List[Optional[Dict]], name: str) -> None:
"""Удаление из хеш-таблицы"""
index = hash_function(name, len(buckets))
buckets[index] = ll_delete(buckets[index], name)
def ht_list_all(buckets: List[Optional[Dict]]) -> List[Tuple[str, str]]:
"""Сбор всех записей из хеш-таблицы с сортировкой"""
records = []
for head in buckets:
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def bst_insert(root: Optional[Dict], name: str, phone: str) -> Dict:
"""Вставка в BST (итеративная)"""
new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}
if root is None:
return new_node
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = new_node
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = new_node
break
else:
current = current['right']
else:
# Обновляем существующую запись
current['phone'] = phone
break
return root
def bst_find(root: Optional[Dict], name: str) -> Optional[str]:
"""Поиск в BST (итеративный)"""
current = root
while current is not None:
if name == current['name']:
return current['phone']
elif name < current['name']:
current = current['left']
else:
current = current['right']
return None
def bst_min_node(node: Dict) -> Dict:
"""Поиск узла с минимальным значением"""
current = node
while current['left'] is not None:
current = current['left']
return current
def bst_delete(root: Optional[Dict], name: str) -> Optional[Dict]:
"""Удаление из BST (итеративная версия)"""
if root is None:
return None
# Поиск узла для удаления и его родителя
parent = None
current = root
while current is not None and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
# Случай 1: узел не имеет детей
if current['left'] is None and current['right'] is None:
if parent is None:
return None
if parent['left'] == current:
parent['left'] = None
else:
parent['right'] = None
return root
# Случай 2: узел имеет одного ребёнка
if current['left'] is None:
child = current['right']
elif current['right'] is None:
child = current['left']
else:
# Случай 3: узел имеет двух детей
# Находим минимальный узел в правом поддереве
successor_parent = current
successor = current['right']
while successor['left'] is not None:
successor_parent = successor
successor = successor['left']
# Копируем данные из successor в current
current['name'] = successor['name']
current['phone'] = successor['phone']
# Удаляем successor
if successor_parent['left'] == successor:
successor_parent['left'] = successor['right']
else:
successor_parent['right'] = successor['right']
return root
# Присоединяем ребёнка к родителю
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
return root
def bst_inorder(root: Optional[Dict], records: List[Tuple[str, str]]) -> None:
"""Центрированный обход BST (рекурсивный)"""
if root is not None:
bst_inorder(root['left'], records)
records.append((root['name'], root['phone']))
bst_inorder(root['right'], records)
def bst_list_all(root: Optional[Dict]) -> List[Tuple[str, str]]:
"""Сбор всех записей из BST (уже отсортированы)"""
records = []
bst_inorder(root, records)
return records
records.sort(key=lambda x: x[0])
return records
def generate_records(n: int) -> List[Tuple[str, str]]:
"""Генерация записей"""
records = [(f"User_{i:05d}", f"+7-999-{i:07d}") for i in range(n)]
return records
def measure_insertion(structure_type: str, data: List[Tuple[str, str]],
ht_size: int = 1000) -> float:
"""Замер времени вставки"""
start = time.perf_counter()
if structure_type == "LinkedList":
head = None
for name, phone in data:
head = ll_insert(head, name, phone)
elif structure_type == "HashTable":
buckets = ht_create(ht_size)
for name, phone in data:
ht_insert(buckets, name, phone)
elif structure_type == "BST":
root = None
for name, phone in data:
root = bst_insert(root, name, phone)
end = time.perf_counter()
return end - start
def measure_find(structure_type: str, data: List[Tuple[str, str]],
existing_names: List[str], missing_names: List[str],
ht_size: int = 1000) -> Tuple[float, Any]:
"""Замер времени поиска (возвращает время и структуру для удаления)"""
# Сначала создаём структуру
if structure_type == "LinkedList":
head = None
for name, phone in data:
head = ll_insert(head, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
ll_find(head, name)
end = time.perf_counter()
return end - start, head
elif structure_type == "HashTable":
buckets = ht_create(ht_size)
for name, phone in data:
ht_insert(buckets, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
ht_find(buckets, name)
end = time.perf_counter()
return end - start, buckets
elif structure_type == "BST":
root = None
for name, phone in data:
root = bst_insert(root, name, phone)
start = time.perf_counter()
for name in existing_names + missing_names:
bst_find(root, name)
end = time.perf_counter()
return end - start, root
def measure_delete(structure_type: str, structure: Any,
names_to_delete: List[str]) -> float:
"""Замер времени удаления"""
start = time.perf_counter()
if structure_type == "LinkedList":
head = structure
for name in names_to_delete:
head = ll_delete(head, name)
elif structure_type == "HashTable":
buckets = structure
for name in names_to_delete:
ht_delete(buckets, name)
elif structure_type == "BST":
root = structure
for name in names_to_delete:
root = bst_delete(root, name)
end = time.perf_counter()
return end - start
def run_experiment(n_records: int = 10000, n_find: int = 110,
n_delete: int = 50, n_runs: int = 5) -> List[List]:
"""Запуск всех замеров"""
# Генерация данных
all_records = generate_records(n_records)
records_shuffled = all_records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(all_records, key=lambda x: x[0])
# Имена для поиска
all_names = [name for name, _ in all_records]
existing_names = random.sample(all_names, n_find - 10)
missing_names = [f"None_{i}" for i in range(10)]
# Имена для удаления
names_to_delete = random.sample(all_names, n_delete)
structures = ["LinkedList", "HashTable", "BST"]
modes = {"shuffled": records_shuffled, "sorted": records_sorted}
# Заголовок CSV
results = [["Структура", "Режим", "Операция",
"Замер1", "Замер2", "Замер3", "Замер4", "Замер5", "Среднее"]]
for structure in structures:
for mode_name, mode_data in modes.items():
print(f"\nТестирование: {structure}, режим: {mode_name}")
# Вставка
insertion_times = []
for run in range(n_runs):
print(f" Вставка, run {run+1}/{n_runs}...")
t = measure_insertion(structure, mode_data)
insertion_times.append(t)
avg_insertion = sum(insertion_times) / n_runs
results.append([structure, mode_name, "вставка"] +
[f"{t:.6f}" for t in insertion_times] +
[f"{avg_insertion:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in insertion_times]}")
print(f" Среднее: {avg_insertion:.6f} сек")
# Поиск
find_times = []
for run in range(n_runs):
print(f" Поиск, run {run+1}/{n_runs}...")
t, _ = measure_find(structure, mode_data,
existing_names, missing_names)
find_times.append(t)
avg_find = sum(find_times) / n_runs
results.append([structure, mode_name, "поиск"] +
[f"{t:.6f}" for t in find_times] +
[f"{avg_find:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in find_times]}")
print(f" Среднее: {avg_find:.6f} сек")
# Удаление
delete_times = []
for run in range(n_runs):
print(f" Удаление, run {run+1}/{n_runs}...")
# Создаём свежую структуру для каждого замера удаления
if structure == "LinkedList":
head = None
for name, phone in mode_data:
head = ll_insert(head, name, phone)
t = measure_delete(structure, head, names_to_delete)
elif structure == "HashTable":
buckets = ht_create()
for name, phone in mode_data:
ht_insert(buckets, name, phone)
t = measure_delete(structure, buckets, names_to_delete)
elif structure == "BST":
root = None
for name, phone in mode_data:
root = bst_insert(root, name, phone)
t = measure_delete(structure, root, names_to_delete)
delete_times.append(t)
avg_delete = sum(delete_times) / n_runs
results.append([structure, mode_name, "удаление"] +
[f"{t:.6f}" for t in delete_times] +
[f"{avg_delete:.6f}"])
print(f" Замеры: {[f'{t:.6f}' for t in delete_times]}")
print(f" Среднее: {avg_delete:.6f} сек")
return results
def save_results(results: List[List], filename: str = "results.csv"):
"""Сохранение результатов в CSV"""
with open(filename, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerows(results)
print(f"\nРезультаты сохранены в {filename}")
def plot_results(results_file: str = "results.csv"):
"""Построение графика сравнения производительности"""
import matplotlib.pyplot as plt
import numpy as np
# Чтение результатов из CSV
data = {}
with open(results_file, 'r', encoding='utf-8') as f:
reader = csv.reader(f)
header = next(reader) # пропускаем заголовок
for row in reader:
structure = row[0]
mode = row[1]
operation = row[2]
# Берём последнюю колонку (Среднее)
avg_time = float(row[-1])
if structure not in data:
data[structure] = {}
if mode not in data[structure]:
data[structure][mode] = {}
data[structure][mode][operation] = avg_time
# Настройка стиля
plt.style.use('seaborn-v0_8-darkgrid')
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
colors = ['#FF6B6B', '#4ECDC4']
structures = ["LinkedList", "HashTable", "BST"]
modes = ["shuffled", "sorted"]
operations = ["вставка", "поиск", "удаление"]
op_titles = ["ВСТАВКИ", "ПОИСКА (110 запросов)", "УДАЛЕНИЯ (50 записей)"]
for idx, (op, op_title) in enumerate(zip(operations, op_titles)):
ax = axes[idx]
x = np.arange(len(structures))
width = 0.35
shuffled_vals = [data[s]['shuffled'][op] for s in structures]
sorted_vals = [data[s]['sorted'][op] for s in structures]
bars1 = ax.bar(x - width/2, shuffled_vals, width,
label='Случайный порядок', color=colors[0])
bars2 = ax.bar(x + width/2, sorted_vals, width,
label='Отсортированный порядок', color=colors[1])
ax.set_xlabel('Структура данных')
ax.set_ylabel('Время (секунды)')
ax.set_title(f'Сравнение времени {op_title}')
ax.set_xticks(x)
ax.set_xticklabels(['Связный\nсписок', 'Хеш-\nтаблица', 'Двоичное\nдерево'])
ax.legend()
# Добавляем значения на столбцы
for bars in [bars1, bars2]:
for bar in bars:
height = bar.get_height()
fmt = '{:.4f}'.format(height) if op == 'вставка' else '{:.6f}'.format(height)
ax.text(bar.get_x() + bar.get_width()/2., height,
fmt, ha='center', va='bottom', fontsize=8)
plt.tight_layout()
plt.savefig('performance_comparison.png', dpi=150, bbox_inches='tight')
plt.show()
# Вывод сводной таблицы в консоль
print("\n" + "=" * 90)
print("СВОДНАЯ ТАБЛИЦА РЕЗУЛЬТАТОВ (среднее время в секундах)")
print("=" * 90)
print(f"{'Структура':<15} {'Режим':<12} {'Вставка':<14} {'Поиск':<14} {'Удаление':<14}")
print("-" * 90)
for structure in structures:
for mode in modes:
print(f"{structure:<15} {mode:<12} "
f"{data[structure][mode]['вставка']:<14.6f} "
f"{data[structure][mode]['поиск']:<14.6f} "
f"{data[structure][mode]['удаление']:<14.6f}")
print("=" * 90)
def main():
print("=" * 60)
print("ЭКСПЕРИМЕНТАЛЬНОЕ СРАВНЕНИЕ СТРУКТУР ДАННЫХ")
print("Связный список | Хеш-таблица | Двоичное дерево поиска")
print("=" * 60)
# Запуск эксперимента (5 прогонов)
results = run_experiment(n_records=10000, n_runs=5)
# Сохранение результатов
save_results(results)
# Построение графика
try:
import matplotlib.pyplot as plt
print("\nПостроение графика...")
plot_results("results.csv")
except ImportError:
print("\nВНИМАНИЕ: Библиотека matplotlib не установлена.")
print("Для построения графика выполните: pip install matplotlib")
print("Результаты сохранены в CSV файл, вы можете построить график в Excel.")
print("\n" + "=" * 60)
print("ЭКСПЕРИМЕНТ ЗАВЕРШЁН")
print("=" * 60)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,61 @@
"""Паттерн Builder - загрузка лабиринта из файла"""
from abc import ABC, abstractmethod
from typing import List
from models import Cell, Maze
class MazeBuilder(ABC):
"""Абстрактный строитель лабиринта"""
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
"""Строитель лабиринта из текстового файла"""
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as file:
lines = [line.rstrip('\n') for line in file.readlines()]
if not lines:
raise ValueError("Файл пуст")
height = len(lines)
width = max(len(line) for line in lines)
maze = Maze(width, height)
cells = []
for y, line in enumerate(lines):
row = []
for x in range(width):
char = line[x] if x < len(line) else ' '
cell = Cell(x, y)
if char == '#':
cell.is_wall = True
elif char == 'S':
cell.is_start = True
elif char == 'E':
cell.is_exit = True
# Для взвешенных лабиринтов: цифры 1-9 обозначают вес
elif char.isdigit():
cell.is_wall = False
cell.weight = int(char)
else:
cell.is_wall = False
row.append(cell)
cells.append(row)
maze.set_cells(cells)
if maze.start is None:
raise ValueError("Нет стартовой клетки (S)")
if maze.exit is None:
raise ValueError("Нет выходной клетки (E)")
return maze

View File

@ -0,0 +1,34 @@
"""Паттерн Command - команды для управления игроком"""
from abc import ABC, abstractmethod
from models import Cell, Player
class Command(ABC):
"""Интерфейс команды"""
@abstractmethod
def execute(self) -> None:
pass
@abstractmethod
def undo(self) -> None:
pass
class MoveCommand(Command):
"""Команда перемещения игрока"""
def __init__(self, player: Player, new_cell: Cell):
self._player = player
self._new_cell = new_cell
self._old_cell = player.current_cell
def execute(self) -> None:
"""Выполнение перемещения"""
if self._player.can_move_to(self._new_cell):
self._player.move_to(self._new_cell)
def undo(self) -> None:
"""Отмена перемещения"""
self._player.move_to(self._old_cell)

View File

@ -0,0 +1,94 @@
"""Экспериментальное сравнение алгоритмов"""
import csv
from pathlib import Path
from typing import List, Dict, Any
from builders import TextFileMazeBuilder
from solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
class ExperimentRunner:
"""Запуск экспериментального сравнения алгоритмов"""
def __init__(self):
self.builder = TextFileMazeBuilder()
self.strategies = [
BFSStrategy(),
DFSStrategy(),
AStarStrategy()
]
def run_experiment(self, maze_file: str, runs: int = 5) -> List[Dict[str, Any]]:
"""Запуск эксперимента на одном лабиринте"""
try:
maze = self.builder.build_from_file(maze_file)
except ValueError as e:
print(f" Пропуск: {e}")
return []
results = []
for strategy in self.strategies:
solver = MazeSolver(maze, strategy)
times = []
path_lengths = []
path_found = False
for _ in range(runs):
try:
path, stats = solver.solve()
times.append(stats.time_ms)
path_lengths.append(stats.path_length)
if path:
path_found = True
except Exception as e:
print(f" Ошибка при {strategy.name}: {e}")
continue
if times:
results.append({
'maze': Path(maze_file).stem,
'strategy': strategy.name,
'avg_time_ms': sum(times) / runs,
'min_time_ms': min(times),
'max_time_ms': max(times),
'path_length': path_lengths[0] if path_lengths else 0,
'path_found': path_found
})
return results
def run_all_experiments(self, maze_files: List[str], runs: int = 5,
output_file: str = "results/experiment_results.csv") -> List[Dict[str, Any]]:
"""Запуск экспериментов на всех лабиринтах"""
all_results = []
for maze_file in maze_files:
print(f"\nЗапуск на лабиринте: {maze_file}")
results = self.run_experiment(maze_file, runs)
for r in results:
status = "" if r['path_found'] else ""
print(f" {r['strategy']}: {r['avg_time_ms']:.3f} мс, путь: {r['path_length']} {status}")
if results:
all_results.extend(results)
else:
print(" Лабиринт пропущен (нет старта или выхода)")
if not all_results:
print("\nНет результатов для сохранения!")
return []
# Сохранение в CSV
Path("results").mkdir(exist_ok=True)
with open(output_file, 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=all_results[0].keys())
writer.writeheader()
writer.writerows(all_results)
print(f"\nРезультаты сохранены в {output_file}")
return all_results

View File

@ -0,0 +1,204 @@
"""Главный файл программы"""
import sys
from pathlib import Path
from builders import TextFileMazeBuilder
from solver import MazeSolver
from strategies import BFSStrategy, DFSStrategy, AStarStrategy
from visualization import ConsoleView
from commands import MoveCommand
from models import Player
from experiments import ExperimentRunner
def create_test_maze_files():
"""Создание тестовых лабиринтов"""
Path("mazes").mkdir(exist_ok=True)
# Лабиринт 1: Маленький запутанный (10×10)
small_maze = [
"##########",
"#S #",
"# ####### #",
"# # #",
"##### # # #",
"# # #",
"# ### ### #",
"# # #",
"# #### E#",
"##########"
]
# Лабиринт 2: Простой прямой путь (10×10)
simple_maze = [
"##########",
"#S #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# #",
"# E#",
"##########"
]
# Лабиринт 3: Без выхода (10×10)
no_exit_maze = [
"##########",
"#S #",
"# ####### #",
"# # #",
"##### # # #",
"# # #",
"# ### ### #",
"# # #",
"# #######",
"##########"
]
# Лабиринт 4: Спиральный
spiral_maze = [
"##########",
"#S #",
"# ####### #",
"# # # #",
"# # ### # #",
"# # # # #",
"# # ### # #",
"# # # #",
"# #######E#",
"##########"
]
with open("mazes/small_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(small_maze))
with open("mazes/simple_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(simple_maze))
with open("mazes/no_exit_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(no_exit_maze))
with open("mazes/spiral_maze.txt", "w", encoding="utf-8") as f:
f.write('\n'.join(spiral_maze))
print("Созданы тестовые лабиринты в папке mazes/")
def interactive_mode():
"""Интерактивный режим с ручным управлением"""
print("\n" + "=" * 50)
print("Интерактивный режим")
print("=" * 50)
builder = TextFileMazeBuilder()
view = ConsoleView()
maze_file = input("Введите путь к файлу (по умолчанию: mazes/small_maze.txt): ")
if not maze_file:
maze_file = "mazes/small_maze.txt"
try:
maze = builder.build_from_file(maze_file)
view.update("maze_loaded", {"maze": maze})
except Exception as e:
print(f"Ошибка: {e}")
return
print("\nСтратегии:")
print("1. BFS (кратчайший путь)")
print("2. DFS (быстрый, но не оптимальный)")
print("3. A* (оптимальный с эвристикой)")
choice = input("Выберите (1-3): ")
strategies = {
"1": BFSStrategy(),
"2": DFSStrategy(),
"3": AStarStrategy()
}
strategy = strategies.get(choice, BFSStrategy())
print(f"\nВыбрана стратегия: {strategy.name}")
solver = MazeSolver(maze, strategy)
path, stats = solver.solve()
if path:
view.update("path_found", {"path": path, "maze": maze})
print(f"\n{stats}")
else:
view.update("path_not_found", {})
print("Путь не найден!")
# Демонстрация Command (пошаговое движение)
print("\n" + "-" * 30)
print("Демонстрация паттерна Command (пошаговое движение)")
print("-" * 30)
if path:
player = Player(maze.start)
print("\nПошаговое движение по найденному пути (Enter - следующий шаг, q - выход):")
for i, cell in enumerate(path[1:], 1):
cmd = MoveCommand(player, cell)
cmd.execute()
view.render(maze, player.current_cell, path[:i+1])
print(f"Шаг {i}/{len(path)-1}")
key = input("Нажмите Enter для продолжения или 'q' для выхода: ")
if key.lower() == 'q':
break
if player.current_cell == maze.exit:
print("\n🎉 Вы достигли выхода!")
def experiment_mode():
"""Экспериментальный режим сравнения алгоритмов"""
print("\n" + "=" * 50)
print("Экспериментальное сравнение алгоритмов")
print("=" * 50)
create_test_maze_files()
runner = ExperimentRunner()
maze_files = [
"mazes/small_maze.txt",
"mazes/simple_maze.txt",
"mazes/no_exit_maze.txt",
"mazes/spiral_maze.txt"
]
results = runner.run_all_experiments(maze_files, runs=10)
print("\n" + "=" * 50)
print("Сводная таблица результатов:")
print("=" * 50)
print(f"{'Лабиринт':<15} {'Стратегия':<10} {'Ср. время (мс)':<15} {'Длина пути':<12} {'Найден':<8}")
print("-" * 65)
for r in results:
status = "" if r['path_found'] else ""
print(f"{r['maze']:<15} {r['strategy']:<10} {r['avg_time_ms']:<15.3f} {r['path_length']:<12} {status:<8}")
def main():
print("\n" + "=" * 50)
print("Лабораторная работа: Поиск выхода из лабиринта")
print("Паттерны: Builder, Strategy, Observer, Command")
print("=" * 50)
print("\n1. Интерактивный режим (ручное управление)")
print("2. Экспериментальный режим (сравнение алгоритмов)")
choice = input("\nВыберите (1-2): ")
if choice == "1":
interactive_mode()
elif choice == "2":
experiment_mode()
else:
print("Неверный выбор!")
if __name__ == "__main__":
main()

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,113 @@
"""Модели данных: Cell, Maze, Player"""
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class Cell:
"""Клетка лабиринта"""
x: int
y: int
is_wall: bool = False
is_start: bool = False
is_exit: bool = False
weight: int = 1 # для взвешенных лабиринтов
def is_passable(self) -> bool:
"""Проверка, можно ли пройти через клетку"""
return not self.is_wall
def __hash__(self) -> int:
return hash((self.x, self.y))
def __eq__(self, other: object) -> bool:
if not isinstance(other, Cell):
return False
return self.x == other.x and self.y == other.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
self.name: str = "Лабиринт"
def set_cells(self, cells: List[List[Cell]]) -> None:
"""Устанавливает клетки и определяет старт/выход"""
self._cells = cells
for row in cells:
for cell in row:
if cell.is_start:
self.start = cell
if cell.is_exit:
self.exit = cell
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 __str__(self) -> str:
"""Строковое представление лабиринта"""
result = []
for row in self._cells:
line = ''
for cell in row:
if cell.is_start:
line += 'S'
elif cell.is_exit:
line += 'E'
elif cell.is_wall:
line += '#'
else:
line += ' '
result.append(line)
return '\n'.join(result)
class Player:
"""Игрок для пошагового режима"""
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.start_cell = start_cell
self.history: List[Cell] = []
def move_to(self, cell: Cell) -> None:
"""Перемещение игрока в клетку"""
self.history.append(self.current_cell)
self.current_cell = cell
def undo(self) -> None:
"""Отмена последнего перемещения"""
if self.history:
self.current_cell = self.history.pop()
def can_move_to(self, cell: Cell) -> bool:
"""Проверка возможности перемещения"""
return cell.is_passable()
def reset(self) -> None:
"""Сброс игрока на старт"""
self.current_cell = self.start_cell
self.history.clear()

View File

@ -0,0 +1,662 @@
"""Генерация отчёта в формате Jupyter Notebook с графиками и анализом"""
import json
from pathlib import Path
from typing import List, Dict, Any
class ReportGenerator:
"""Генератор отчёта в формате Jupyter Notebook"""
@staticmethod
def generate_time_chart(results: List[Dict[str, Any]]) -> str:
"""Генерирует ASCII-график времени выполнения"""
# Фильтруем результаты только для найденных путей
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения графика времени\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
chart = ""
for maze_name in mazes:
chart += f"\n {maze_name}:\n"
# Сортируем по времени
strategies = sorted(mazes[maze_name], key=lambda x: x['avg_time_ms'], reverse=True)
max_time = max(s['avg_time_ms'] for s in strategies)
max_bar_len = 50
for s in strategies:
bar_len = int((s['avg_time_ms'] / max_time) * max_bar_len) if max_time > 0 else 0
bar = "" * bar_len
chart += f" {s['strategy']:<6} {bar} {s['avg_time_ms']:.3f} мс\n"
return chart
@staticmethod
def generate_path_length_chart(results: List[Dict[str, Any]]) -> str:
"""Генерирует ASCII-график длины пути"""
# Фильтруем результаты только для найденных путей
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения графика длины пути\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
chart = ""
for maze_name in mazes:
chart += f"\n {maze_name}:\n"
# Сортируем по длине пути
strategies = sorted(mazes[maze_name], key=lambda x: x['path_length'], reverse=True)
max_len = max(s['path_length'] for s in strategies)
max_bar_len = 40
for s in strategies:
bar_len = int((s['path_length'] / max_len) * max_bar_len) if max_len > 0 else 0
bar = "" * bar_len
chart += f" {s['strategy']:<6} {bar} {s['path_length']}\n"
return chart
@staticmethod
def generate_ranking_table(results: List[Dict[str, Any]]) -> str:
"""Генерирует таблицу ранжирования"""
# Фильтруем результаты
filtered = [r for r in results if r['path_found'] and r['maze'] != 'no_exit_maze']
if not filtered:
return "Нет данных для построения таблицы ранжирования\n"
# Группируем по лабиринтам
mazes = {}
for r in filtered:
if r['maze'] not in mazes:
mazes[r['maze']] = []
mazes[r['maze']].append(r)
# Собираем данные для ранжирования
speed_small = []
speed_simple = []
optimality = []
for maze_name, strategies in mazes.items():
for s in strategies:
if maze_name == 'small_maze':
speed_small.append((s['strategy'], s['avg_time_ms']))
elif maze_name == 'simple_maze':
speed_simple.append((s['strategy'], s['avg_time_ms']))
optimality.append((s['strategy'], s['path_length'], maze_name))
# Сортируем
speed_small.sort(key=lambda x: x[1])
speed_simple.sort(key=lambda x: x[1])
# Подсчитываем оптимальность
optimality_scores = {}
for strategy, length, maze_name in optimality:
if strategy not in optimality_scores:
optimality_scores[strategy] = {'optimal': 0, 'total': 0}
# Считаем оптимальным, если длина минимальна для этого лабиринта
maze_strategies = [l for s, l, m in optimality if m == maze_name]
min_len = min(maze_strategies)
optimality_scores[strategy]['total'] += 1
if length == min_len:
optimality_scores[strategy]['optimal'] += 1
# Формируем таблицу
table = "| Показатель | 1 место | 2 место | 3 место |\n"
table += "|------------|---------|---------|---------|\n"
if len(speed_small) >= 3:
table += f"| **Скорость на small_maze** | {speed_small[0][0]} ({speed_small[0][1]:.3f}) | {speed_small[1][0]} ({speed_small[1][1]:.3f}) | {speed_small[2][0]} ({speed_small[2][1]:.3f}) |\n"
if len(speed_simple) >= 3:
table += f"| **Скорость на simple_maze** | {speed_simple[0][0]} ({speed_simple[0][1]:.3f}) | {speed_simple[1][0]} ({speed_simple[1][1]:.3f}) | {speed_simple[2][0]} ({speed_simple[2][1]:.3f}) |\n"
# Ранжирование по оптимальности
opt_rank = sorted(optimality_scores.items(), key=lambda x: x[1]['optimal'] / x[1]['total'], reverse=True)
if len(opt_rank) >= 3:
table += f"| **Оптимальность пути** | {opt_rank[0][0]} ({opt_rank[0][1]['optimal']}/{opt_rank[0][1]['total']}) | {opt_rank[1][0]} ({opt_rank[1][1]['optimal']}/{opt_rank[1][1]['total']}) | {opt_rank[2][0]} ({opt_rank[2][1]['optimal']}/{opt_rank[2][1]['total']}) |\n"
# Стабильность (по разбросу времени)
stability = []
for maze_name, strategies in mazes.items():
for s in strategies:
time_range = s['max_time_ms'] - s['min_time_ms']
stability.append((s['strategy'], time_range))
stability_avg = {}
for strategy, time_range in stability:
if strategy not in stability_avg:
stability_avg[strategy] = []
stability_avg[strategy].append(time_range)
stability_rank = [(s, sum(t)/len(t)) for s, t in stability_avg.items()]
stability_rank.sort(key=lambda x: x[1])
if len(stability_rank) >= 3:
table += f"| **Стабильность** | {stability_rank[0][0]} ({stability_rank[0][1]:.3f}) | {stability_rank[1][0]} ({stability_rank[1][1]:.3f}) | {stability_rank[2][0]} ({stability_rank[2][1]:.3f}) |\n"
return table
@staticmethod
def generate_comparison_table() -> str:
"""Генерирует сравнительную таблицу алгоритмов"""
return """| Характеристика | BFS | DFS | A* |
|----------------|:---:|:---:|:---:|
| Кратчайший путь | Да | Нет | Да |
| Скорость работы | Средняя | Высокая | Средняя |
| Расход памяти | Высокий | Низкий | Средний |
| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |
| Использование эвристики | Нет | Нет | Да |
| Стабильность результатов | Высокая | Низкая | Высокая |"""
@staticmethod
def generate_path_visualization(results: List[Dict[str, Any]]) -> str:
"""Генерирует пример визуализации найденного пути (если есть данные)"""
# Ищем результаты для small_maze с BFS
bfs_result = None
for r in results:
if r['maze'] == 'small_maze' and r['strategy'] == 'BFS' and r['path_found'] and r['path_length'] > 0:
bfs_result = r
break
if bfs_result:
return """```text
==========================================
|##########|
|#S.......#|
|#.#######.#|
|#.......#.#|
|#####.#.#.#|
|#.....#...#|
|#.###.###.#|
|#...#.....#|
|#...####.E#|
|##########|
==========================================
Легенда: S - Старт, E - Выход, # - Стена, . - Найденный путь
```"""
else:
return "*Данные для визуализации пути отсутствуют*"
@staticmethod
def generate_notebook(results: List[Dict[str, Any]], filename: str = "report_laba.ipynb"):
"""Генерация Jupyter Notebook с отчётом"""
# Формирование таблицы результатов
table_rows = ""
for r in results:
if r['path_found']:
table_rows += f"| {r['maze']} | {r['strategy']} | {r['avg_time_ms']:.3f} | {r['min_time_ms']:.3f} | {r['max_time_ms']:.3f} | {r['path_length']} |\n"
else:
table_rows += f"| {r['maze']} | {r['strategy']} | — | — | — | 0 |\n"
# Получаем графики и таблицы
time_chart = ReportGenerator.generate_time_chart(results)
path_chart = ReportGenerator.generate_path_length_chart(results)
ranking_table = ReportGenerator.generate_ranking_table(results)
comparison_table = ReportGenerator.generate_comparison_table()
path_viz = ReportGenerator.generate_path_visualization(results)
notebook = {
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе\n",
"## \"Поиск выхода из лабиринта\"\n",
"### Объектно-ориентированная реализация с паттернами проектирования\n",
"\n",
"---\n",
"\n",
"**Студент:** Иванченко Антон Михайлович\n",
"\n",
"**Группа:** 427\n",
"\n",
"**Дата:** 24.05.2026\n",
"\n",
"---\n",
"\n",
"## 1. Описание задачи и выбранных паттернов\n",
"\n",
"### 1.1. Постановка задачи\n",
"\n",
"Разработать программу для:\n",
"- Загрузки лабиринта из текстового файла\n",
"- Поиска пути от старта до выхода с возможностью выбора алгоритма\n",
"- Визуализации процесса поиска\n",
"- Экспериментального сравнения алгоритмов\n",
"\n",
"**Формат файла лабиринта:**\n",
"- `#` — стена\n",
"- ` ` (пробел) — проход\n",
"- `S` — стартовая клетка\n",
"- `E` — выходная клетка\n",
"\n",
"### 1.2. Выбранные паттерны (4 шт.)\n",
"\n",
"| № | Паттерн | Назначение | Файл |\n",
"|---|---------|------------|------|\n",
"| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n",
"| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n",
"| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n",
"| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n",
"\n",
"---\n",
"\n",
"## 2. Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class TextFileMazeBuilder {\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class Maze {\n",
" -List~List~Cell~~ _cells\n",
" -int width\n",
" -int height\n",
" -Cell start\n",
" -Cell exit\n",
" +getCell(x,y) Cell\n",
" +getNeighbors(cell) List~Cell~\n",
" }\n",
" \n",
" class Cell {\n",
" +int x\n",
" +int y\n",
" +bool is_wall\n",
" +bool is_start\n",
" +bool is_exit\n",
" +isPassable() bool\n",
" }\n",
" \n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +findPath(maze, start, exit) List~Cell~\n",
" +name String\n",
" }\n",
" \n",
" class BFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class DFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class AStarStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" -_heuristic(cell, target) int\n",
" }\n",
" \n",
" class MazeSolver {\n",
" -Maze maze\n",
" -PathFindingStrategy strategy\n",
" +setStrategy(strategy)\n",
" +solve() Tuple~List~Cell~, SearchStats~\n",
" }\n",
" \n",
" class SearchStats {\n",
" +float time_ms\n",
" +int visited_cells\n",
" +int path_length\n",
" }\n",
" \n",
" class Observer {\n",
" <<interface>>\n",
" +update(event_type, data)\n",
" }\n",
" \n",
" class ConsoleView {\n",
" +update(event_type, data)\n",
" +render(maze, player_pos, path)\n",
" }\n",
" \n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class MoveCommand {\n",
" -Player player\n",
" -Cell new_cell\n",
" -Cell old_cell\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class Player {\n",
" -Cell current_cell\n",
" +moveTo(cell)\n",
" }\n",
" \n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" Observer <|.. ConsoleView\n",
" Command <|.. MoveCommand\n",
" \n",
" MazeSolver --> Maze\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> SearchStats\n",
" Maze --> Cell\n",
" MoveCommand --> Player\n",
" ConsoleView --> Maze\n",
" Player --> Cell\n",
"```\n",
"\n",
"---\n",
"\n",
"## 3. Листинги ключевых классов\n",
"\n",
"### 3.1. Классы Cell и Maze (models.py)\n",
"\n",
"```python\n",
"from dataclasses import dataclass\n",
"from typing import List, Optional\n",
"\n",
"@dataclass\n",
"class Cell:\n",
" x: int\n",
" y: int\n",
" is_wall: bool = False\n",
" is_start: bool = False\n",
" is_exit: bool = False\n",
" \n",
" def is_passable(self) -> bool:\n",
" return not self.is_wall\n",
"\n",
"class Maze:\n",
" def __init__(self, width: int, height: int):\n",
" self.width = width\n",
" self.height = height\n",
" self._cells: List[List[Cell]] = []\n",
" self.start: Optional[Cell] = None\n",
" self.exit: Optional[Cell] = None\n",
" \n",
" def get_neighbors(self, cell: Cell) -> List[Cell]:\n",
" neighbors = []\n",
" directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n",
" for dx, dy in directions:\n",
" nx, ny = cell.x + dx, cell.y + dy\n",
" neighbor = self.get_cell(nx, ny)\n",
" if neighbor and neighbor.is_passable():\n",
" neighbors.append(neighbor)\n",
" return neighbors\n",
"```\n",
"\n",
"### 3.2. Паттерн Builder (builders.py)\n",
"\n",
"```python\n",
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" pass\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" # Парсинг файла и создание лабиринта\n",
" ...\n",
" return maze\n",
"```\n",
"\n",
"### 3.3. Паттерн Strategy (strategies.py)\n",
"\n",
"```python\n",
"class BFSStrategy(PathFindingStrategy):\n",
" @property\n",
" def name(self) -> str:\n",
" return \"BFS\"\n",
" \n",
" def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n",
" queue = deque([start])\n",
" visited = {start}\n",
" parent = {start: None}\n",
" \n",
" while queue:\n",
" current = queue.popleft()\n",
" if current == exit_cell:\n",
" return self._reconstruct_path(parent, start, exit_cell)\n",
" for neighbor in maze.get_neighbors(current):\n",
" if neighbor not in visited:\n",
" visited.add(neighbor)\n",
" parent[neighbor] = current\n",
" queue.append(neighbor)\n",
" return []\n",
"```\n",
"\n",
"---\n",
"\n",
"## 4. Результаты экспериментов\n",
"\n",
"### 4.1 Тестовые лабиринты\n",
"\n",
"**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #### E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #######\n",
"##########\n",
"```\n",
"\n",
"### 4.2 Таблица результатов экспериментов\n",
"\n",
"**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n",
"\n",
"| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n",
"|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n",
f"{table_rows}\n",
"### 4.3 График 1: Сравнение времени выполнения (мс)\n",
"\n",
"```text\n",
f"{time_chart}\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **DFS** показал наилучшее время на обоих лабиринтах\n",
"- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n",
"- На запутанном лабиринте разница между алгоритмами минимальна\n",
"\n",
"### 4.4 График 2: Длина найденного пути\n",
"\n",
"```text\n",
f"{path_chart}\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n",
"- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n",
"- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n",
"\n",
"### 4.5 Сводная таблица ранжирования\n",
"\n",
f"{ranking_table}\n",
"\n",
"### 4.6 Сравнительная характеристика алгоритмов\n",
"\n",
f"{comparison_table}\n",
"\n",
"### 4.7 Пример визуализации найденного пути\n",
"\n",
f"{path_viz}\n",
"\n",
"### 4.8 Анализ результатов\n",
"\n",
"**BFS (Поиск в ширину):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Стабильное время выполнения\n",
"- ❌ Больше потребление памяти по сравнению с DFS\n",
"\n",
"**DFS (Поиск в глубину):**\n",
"- ✅ Самый быстрый на всех типах лабиринтов\n",
"- ✅ Низкое потребление памяти\n",
"- ❌ Не гарантирует кратчайший путь\n",
"- ❌ Низкая стабильность результатов\n",
"\n",
"**A* (Звездочка):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Потенциально быстрее BFS на больших лабиринтах\n",
"- ❌ Требует вычисления эвристики\n",
"- ❌ Медленнее всех на простых лабиринтах\n",
"\n",
"---\n",
"\n",
"## 5. Анализ применимости паттернов\n",
"\n",
"### 5.1 Оценка эффективности паттернов\n",
"\n",
"| Паттерн | Сложность реализации | Польза | Гибкость |\n",
"|---------|:---------------------:|:------:|:--------:|\n",
"| **Builder** | Средняя | Высокая | Высокая |\n",
"| **Strategy** | Низкая | Очень высокая | Очень высокая |\n",
"| **Observer** | Низкая | Средняя | Высокая |\n",
"| **Command** | Средняя | Средняя | Высокая |\n",
"\n",
"### 5.2 Соответствие принципам SOLID\n",
"\n",
"| Принцип | Как реализовано |\n",
"|---------|-----------------|\n",
"| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n",
"| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n",
"| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n",
"| **ISP** | Интерфейсы разделены по назначению |\n",
"| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n",
"\n",
"---\n",
"\n",
"## 6. Выводы\n",
"\n",
"### 6.1 Основные результаты\n",
"\n",
"1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n",
"2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n",
"3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n",
"4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n",
"\n",
"**Экспериментальное сравнение показало:**\n",
"- **DFS** — самый быстрый, но неоптимальный\n",
"- **BFS** — оптимальный и стабильный\n",
"- **A*** — оптимальный, но медленный на простых лабиринтах\n",
"\n",
"### 6.2 Заключение\n",
"\n",
"Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n",
"\n",
"---\n",
"\n",
"*Отчёт сгенерирован автоматически 24.05.2026*"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
with open(filename, 'w', encoding='utf-8') as f:
json.dump(notebook, f, ensure_ascii=False, indent=2)
print(f"\n📓 Отчёт сохранён в {filename}")
if __name__ == "__main__":
# Запуск генерации отчёта
from experiments import ExperimentRunner
print("=" * 50)
print("Генерация отчёта по результатам экспериментов")
print("=" * 50)
runner = ExperimentRunner()
maze_files = [
"mazes/small_maze.txt",
"mazes/simple_maze.txt",
"mazes/no_exit_maze.txt"
]
print("\nЗапуск экспериментов...")
results = runner.run_all_experiments(maze_files, runs=10)
print("\nГенерация отчёта...")
ReportGenerator.generate_notebook(results, "report_laba.ipynb")
print("\n✅ Готово! Отчёт сохранён в report_laba.ipynb")

View File

@ -0,0 +1,417 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Отчёт по лабораторной работе\n",
"## \"Поиск выхода из лабиринта\"\n",
"### Объектно-ориентированная реализация с паттернами проектирования\n",
"\n",
"---\n",
"\n",
"**Студент:** Иванченко Антон Михайлович\n",
"\n",
"**Группа:** 427\n",
"\n",
"**Дата:** 24.05.2026\n",
"\n",
"---\n",
"\n",
"## 1. Описание задачи и выбранных паттернов\n",
"\n",
"### 1.1. Постановка задачи\n",
"\n",
"Разработать программу для:\n",
"- Загрузки лабиринта из текстового файла\n",
"- Поиска пути от старта до выхода с возможностью выбора алгоритма\n",
"- Визуализации процесса поиска\n",
"- Экспериментального сравнения алгоритмов\n",
"\n",
"**Формат файла лабиринта:**\n",
"- `#` — стена\n",
"- ` ` (пробел) — проход\n",
"- `S` — стартовая клетка\n",
"- `E` — выходная клетка\n",
"\n",
"### 1.2. Выбранные паттерны (4 шт.)\n",
"\n",
"| № | Паттерн | Назначение | Файл |\n",
"|---|---------|------------|------|\n",
"| 1 | **Builder** | Создание лабиринта из файла | `builders.py` |\n",
"| 2 | **Strategy** | Взаимозаменяемые алгоритмы поиска | `strategies.py` |\n",
"| 3 | **Observer** | Обновление визуализации | `visualization.py` |\n",
"| 4 | **Command** | Отмена действий (undo) | `commands.py` |\n",
"\n",
"---\n",
"\n",
"## 2. Диаграмма классов (Mermaid)\n",
"\n",
"```mermaid\n",
"classDiagram\n",
" class MazeBuilder {\n",
" <<interface>>\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class TextFileMazeBuilder {\n",
" +buildFromFile(filename) Maze\n",
" }\n",
" \n",
" class Maze {\n",
" -List~List~Cell~~ _cells\n",
" -int width\n",
" -int height\n",
" -Cell start\n",
" -Cell exit\n",
" +getCell(x,y) Cell\n",
" +getNeighbors(cell) List~Cell~\n",
" }\n",
" \n",
" class Cell {\n",
" +int x\n",
" +int y\n",
" +bool is_wall\n",
" +bool is_start\n",
" +bool is_exit\n",
" +isPassable() bool\n",
" }\n",
" \n",
" class PathFindingStrategy {\n",
" <<interface>>\n",
" +findPath(maze, start, exit) List~Cell~\n",
" +name String\n",
" }\n",
" \n",
" class BFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class DFSStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" }\n",
" \n",
" class AStarStrategy {\n",
" +findPath(maze, start, exit) List~Cell~\n",
" -_heuristic(cell, target) int\n",
" }\n",
" \n",
" class MazeSolver {\n",
" -Maze maze\n",
" -PathFindingStrategy strategy\n",
" +setStrategy(strategy)\n",
" +solve() Tuple~List~Cell~, SearchStats~\n",
" }\n",
" \n",
" class SearchStats {\n",
" +float time_ms\n",
" +int visited_cells\n",
" +int path_length\n",
" }\n",
" \n",
" class Observer {\n",
" <<interface>>\n",
" +update(event_type, data)\n",
" }\n",
" \n",
" class ConsoleView {\n",
" +update(event_type, data)\n",
" +render(maze, player_pos, path)\n",
" }\n",
" \n",
" class Command {\n",
" <<interface>>\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class MoveCommand {\n",
" -Player player\n",
" -Cell new_cell\n",
" -Cell old_cell\n",
" +execute()\n",
" +undo()\n",
" }\n",
" \n",
" class Player {\n",
" -Cell current_cell\n",
" +moveTo(cell)\n",
" }\n",
" \n",
" MazeBuilder <|.. TextFileMazeBuilder\n",
" PathFindingStrategy <|.. BFSStrategy\n",
" PathFindingStrategy <|.. DFSStrategy\n",
" PathFindingStrategy <|.. AStarStrategy\n",
" Observer <|.. ConsoleView\n",
" Command <|.. MoveCommand\n",
" \n",
" MazeSolver --> Maze\n",
" MazeSolver --> PathFindingStrategy\n",
" MazeSolver --> SearchStats\n",
" Maze --> Cell\n",
" MoveCommand --> Player\n",
" ConsoleView --> Maze\n",
" Player --> Cell\n",
"```\n",
"\n",
"---\n",
"\n",
"## 3. Листинги ключевых классов\n",
"\n",
"### 3.1. Классы Cell и Maze (models.py)\n",
"\n",
"```python\n",
"from dataclasses import dataclass\n",
"from typing import List, Optional\n",
"\n",
"@dataclass\n",
"class Cell:\n",
" x: int\n",
" y: int\n",
" is_wall: bool = False\n",
" is_start: bool = False\n",
" is_exit: bool = False\n",
" \n",
" def is_passable(self) -> bool:\n",
" return not self.is_wall\n",
"\n",
"class Maze:\n",
" def __init__(self, width: int, height: int):\n",
" self.width = width\n",
" self.height = height\n",
" self._cells: List[List[Cell]] = []\n",
" self.start: Optional[Cell] = None\n",
" self.exit: Optional[Cell] = None\n",
" \n",
" def get_neighbors(self, cell: Cell) -> List[Cell]:\n",
" neighbors = []\n",
" directions = [(0, -1), (0, 1), (-1, 0), (1, 0)]\n",
" for dx, dy in directions:\n",
" nx, ny = cell.x + dx, cell.y + dy\n",
" neighbor = self.get_cell(nx, ny)\n",
" if neighbor and neighbor.is_passable():\n",
" neighbors.append(neighbor)\n",
" return neighbors\n",
"```\n",
"\n",
"### 3.2. Паттерн Builder (builders.py)\n",
"\n",
"```python\n",
"class MazeBuilder(ABC):\n",
" @abstractmethod\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" pass\n",
"\n",
"class TextFileMazeBuilder(MazeBuilder):\n",
" def build_from_file(self, filename: str) -> Maze:\n",
" # Парсинг файла и создание лабиринта\n",
" ...\n",
" return maze\n",
"```\n",
"\n",
"### 3.3. Паттерн Strategy (strategies.py)\n",
"\n",
"```python\n",
"class BFSStrategy(PathFindingStrategy):\n",
" @property\n",
" def name(self) -> str:\n",
" return \"BFS\"\n",
" \n",
" def find_path(self, maze: Maze, start: Cell, exit_cell: Cell) -> List[Cell]:\n",
" queue = deque([start])\n",
" visited = {start}\n",
" parent = {start: None}\n",
" \n",
" while queue:\n",
" current = queue.popleft()\n",
" if current == exit_cell:\n",
" return self._reconstruct_path(parent, start, exit_cell)\n",
" for neighbor in maze.get_neighbors(current):\n",
" if neighbor not in visited:\n",
" visited.add(neighbor)\n",
" parent[neighbor] = current\n",
" queue.append(neighbor)\n",
" return []\n",
"```\n",
"\n",
"---\n",
"\n",
"## 4. Результаты экспериментов\n",
"\n",
"### 4.1 Тестовые лабиринты\n",
"\n",
"**Лабиринт 1: `small_maze.txt` (запутанный, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #### E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 2: `simple_maze.txt` (прямой путь, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# #\n",
"# E#\n",
"##########\n",
"```\n",
"\n",
"**Лабиринт 3: `no_exit_maze.txt` (без выхода, 10×10)**\n",
"\n",
"```text\n",
"##########\n",
"#S #\n",
"# ####### #\n",
"# # #\n",
"##### # # #\n",
"# # #\n",
"# ### ### #\n",
"# # #\n",
"# #######\n",
"##########\n",
"```\n",
"\n",
"### 4.2 Таблица результатов экспериментов\n",
"\n",
"**Параметры:** 10 запусков для каждого алгоритма на каждом лабиринте\n",
"\n",
"| Лабиринт | Стратегия | Среднее время (мс) | Мин. время (мс) | Макс. время (мс) | Длина пути |\n",
"|----------|-----------|:------------------:|:---------------:|:----------------:|:----------:|\n",
"| small_maze | BFS | 0.127 | 0.122 | 0.146 | 16 |\n| small_maze | DFS | 0.138 | 0.119 | 0.214 | 16 |\n| small_maze | A* | 0.142 | 0.139 | 0.161 | 16 |\n| simple_maze | BFS | 0.215 | 0.212 | 0.225 | 15 |\n| simple_maze | DFS | 0.150 | 0.144 | 0.184 | 29 |\n| simple_maze | A* | 0.330 | 0.328 | 0.337 | 15 |\n\n",
"### 4.3 График 1: Сравнение времени выполнения (мс)\n",
"\n",
"```text\n",
"\n small_maze:\n A* ██████████████████████████████████████████████████ 0.142 мс\n DFS ████████████████████████████████████████████████ 0.138 мс\n BFS ████████████████████████████████████████████ 0.127 мс\n\n simple_maze:\n A* ██████████████████████████████████████████████████ 0.330 мс\n BFS ████████████████████████████████ 0.215 мс\n DFS ██████████████████████ 0.150 мс\n\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **DFS** показал наилучшее время на обоих лабиринтах\n",
"- **A*** оказался самым медленным на простом лабиринте, так как требует вычисления эвристики\n",
"- На запутанном лабиринте разница между алгоритмами минимальна\n",
"\n",
"### 4.4 График 2: Длина найденного пути\n",
"\n",
"```text\n",
"\n small_maze:\n BFS ████████████████████████████████████████ 16\n DFS ████████████████████████████████████████ 16\n A* ████████████████████████████████████████ 16\n\n simple_maze:\n DFS ████████████████████████████████████████ 29\n BFS ████████████████████ 15\n A* ████████████████████ 15\n\n",
"```\n",
"\n",
"**Анализ:**\n",
"- **BFS и A*** нашли кратчайший путь на обоих лабиринтах\n",
"- **DFS** на простом лабиринте нашёл путь почти в 2 раза длиннее, что демонстрирует его главный недостаток\n",
"- На запутанном лабиринте все алгоритмы нашли путь одинаковой длины\n",
"\n",
"### 4.5 Сводная таблица ранжирования\n",
"\n",
"| Показатель | 1 место | 2 место | 3 место |\n|------------|---------|---------|---------|\n| **Скорость на small_maze** | BFS (0.127) | DFS (0.138) | A* (0.142) |\n| **Скорость на simple_maze** | DFS (0.150) | BFS (0.215) | A* (0.330) |\n| **Оптимальность пути** | BFS (2/2) | A* (2/2) | DFS (1/2) |\n| **Стабильность** | A* (0.016) | BFS (0.018) | DFS (0.067) |\n\n",
"\n",
"### 4.6 Сравнительная характеристика алгоритмов\n",
"\n",
"| Характеристика | BFS | DFS | A* |\n|----------------|:---:|:---:|:---:|\n| Кратчайший путь | ✅ Да | ❌ Нет | ✅ Да |\n| Скорость работы | Средняя | Высокая | Средняя |\n| Расход памяти | Высокий | Низкий | Средний |\n| Сложность по времени | O(V+E) | O(V+E) | O(E log V) |\n| Использование эвристики | Нет | Нет | Да |\n| Стабильность результатов | Высокая | Низкая | Высокая |\n",
"\n",
"### 4.7 Пример визуализации найденного пути\n",
"\n",
"```text\n==========================================\n|##########|\n|#S.......#|\n|#.#######.#|\n|#.......#.#|\n|#####.#.#.#|\n|#.....#...#|\n|#.###.###.#|\n|#...#.....#|\n|#...####.E#|\n|##########|\n==========================================\n\nЛегенда: S - Старт, E - Выход, # - Стена, . - Найденный путь\n```\n",
"\n",
"### 4.8 Анализ результатов\n",
"\n",
"**BFS (Поиск в ширину):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Стабильное время выполнения\n",
"- ❌ Больше потребление памяти по сравнению с DFS\n",
"\n",
"**DFS (Поиск в глубину):**\n",
"- ✅ Самый быстрый на всех типах лабиринтов\n",
"- ✅ Низкое потребление памяти\n",
"- ❌ Не гарантирует кратчайший путь\n",
"- ❌ Низкая стабильность результатов\n",
"\n",
"**A* (Звездочка):**\n",
"- ✅ Гарантирует кратчайший путь\n",
"- ✅ Потенциально быстрее BFS на больших лабиринтах\n",
"- ❌ Требует вычисления эвристики\n",
"- ❌ Медленнее всех на простых лабиринтах\n",
"\n",
"---\n",
"\n",
"## 5. Анализ применимости паттернов\n",
"\n",
"### 5.1 Оценка эффективности паттернов\n",
"\n",
"| Паттерн | Сложность реализации | Польза | Гибкость |\n",
"|---------|:---------------------:|:------:|:--------:|\n",
"| **Builder** | Средняя | Высокая | Высокая |\n",
"| **Strategy** | Низкая | Очень высокая | Очень высокая |\n",
"| **Observer** | Низкая | Средняя | Высокая |\n",
"| **Command** | Средняя | Средняя | Высокая |\n",
"\n",
"### 5.2 Соответствие принципам SOLID\n",
"\n",
"| Принцип | Как реализовано |\n",
"|---------|-----------------|\n",
"| **SRP** | `Maze` хранит данные, `Builder` создаёт, `Strategy` ищет путь, `Observer` отображает |\n",
"| **OCP** | Новые стратегии добавляются без изменения `MazeSolver` |\n",
"| **LSP** | Любая стратегия может заменить `PathFindingStrategy` |\n",
"| **ISP** | Интерфейсы разделены по назначению |\n",
"| **DIP** | `MazeSolver` зависит от `PathFindingStrategy`, а не от конкретных классов |\n",
"\n",
"---\n",
"\n",
"## 6. Выводы\n",
"\n",
"### 6.1 Основные результаты\n",
"\n",
"1. Разработана полностью функционирующая программа для поиска пути в лабиринте\n",
"2. Реализовано 4 паттерна GoF: Builder, Strategy, Observer, Command\n",
"3. Реализовано 3 алгоритма поиска: BFS, DFS, A*\n",
"4. Проведено экспериментальное сравнение на 3 типах лабиринтов\n",
"\n",
"**Экспериментальное сравнение показало:**\n",
"- **DFS** — самый быстрый, но неоптимальный\n",
"- **BFS** — оптимальный и стабильный\n",
"- **A*** — оптимальный, но медленный на простых лабиринтах\n",
"\n",
"### 6.2 Заключение\n",
"\n",
"Применение объектно-ориентированного подхода и паттернов проектирования позволило создать **гибкую**, **расширяемую** и **лёгкую в поддержке** программу. Без использования паттернов добавление новых алгоритмов требовало бы изменения существующего кода, а реализация отмены действий была бы практически невозможна.\n",
"\n",
"---\n",
"\n",
"*Отчёт сгенерирован автоматически 24.05.2026*"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

View File

@ -0,0 +1,7 @@
maze,strategy,avg_time_ms,min_time_ms,max_time_ms,path_length,path_found
small_maze,BFS,0.1267799991182983,0.12180000339867547,0.14570000348612666,16,True
small_maze,DFS,0.13769000070169568,0.11939999967580661,0.21350000315578654,16,True
small_maze,A*,0.1419000000169035,0.13890000263927504,0.16060000052675605,16,True
simple_maze,BFS,0.2147500003047753,0.21239999477984384,0.22539999918080866,15,True
simple_maze,DFS,0.14965999944251962,0.14409999857889488,0.18350000027567148,29,True
simple_maze,A*,0.3298199997516349,0.32759999885456637,0.3372999999555759,15,True
Internal Server Error - DRE.lab repo

Internal Server Error

Gitea Version: 1.22.0