LebedevES #397

Merged
git_admin merged 3 commits from LebedevES/2026-rff_mp:LebedevES into develop 2026-09-05 12:47:03 +00:00
7 changed files with 1239 additions and 0 deletions

0
LebedevES/425.txt Normal file
View File

View File

@ -0,0 +1,292 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time
import random
import csv
import sys
sys.setrecursionlimit(30000)
def ll_create_node(name, phone):
return {'name': name, 'phone': phone, 'next': None}
def ll_insert(head, name, phone):
if head is None:
return ll_create_node(name, phone)
if head['name'] == name:
head['phone'] = phone
return head
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next']['phone'] = phone
return head
current = current['next']
current['next'] = ll_create_node(name, phone)
return head
def ll_find(head, name):
current = head
while current is not None:
if current['name'] == name:
return current['phone']
current = current['next']
return None
def ll_delete(head, name):
if head is None:
return None
if head['name'] == name:
return head['next']
current = head
while current['next'] is not None:
if current['next']['name'] == name:
current['next'] = current['next']['next']
return head
current = current['next']
return head
def ll_list_all(head):
records = []
current = head
while current is not None:
records.append((current['name'], current['phone']))
current = current['next']
records.sort(key=lambda x: x[0])
return records
def hash_function(name, table_size):
return sum(ord(c) for c in name) % table_size
def ht_create_table(size=2000):
return [None] * size
def ht_insert(table, name, phone):
index = hash_function(name, len(table))
table[index] = ll_insert(table[index], name, phone)
def ht_find(table, name):
index = hash_function(name, len(table))
return ll_find(table[index], name)
def ht_delete(table, name):
index = hash_function(name, len(table))
table[index] = ll_delete(table[index], name)
def ht_list_all(table):
all_records = []
for bucket in table:
if bucket is not None:
current = bucket
while current is not None:
all_records.append((current['name'], current['phone']))
current = current['next']
all_records.sort(key=lambda x: x[0])
return all_records
def bst_create_node(name, phone):
return {'name': name, 'phone': phone, 'left': None, 'right': None}
def bst_insert(root, name, phone):
if root is None:
return bst_create_node(name, phone)
current = root
while True:
if name < current['name']:
if current['left'] is None:
current['left'] = bst_create_node(name, phone)
break
else:
current = current['left']
elif name > current['name']:
if current['right'] is None:
current['right'] = bst_create_node(name, phone)
break
else:
current = current['right']
else:
current['phone'] = phone
break
return root
def bst_find(root, name):
current = root
while current is not None:
if name < current['name']:
current = current['left']
elif name > current['name']:
current = current['right']
else:
return current['phone']
return None
def bst_find_min(node):
current = node
while current['left'] is not None:
current = current['left']
return current
def bst_delete(root, name):
if root is None:
return None
parent = None
current = root
while current is not None and current['name'] != name:
parent = current
if name < current['name']:
current = current['left']
else:
current = current['right']
if current is None:
return root
if current['left'] is None or current['right'] is None:
if current['left'] is not None:
child = current['left']
else:
child = current['right']
if parent is None:
return child
if parent['left'] == current:
parent['left'] = child
else:
parent['right'] = child
else:
successor_parent = current
successor = current['right']
while successor['left'] is not None:
successor_parent = successor
successor = successor['left']
current['name'] = successor['name']
current['phone'] = successor['phone']
if successor_parent['left'] == successor:
successor_parent['left'] = successor['right']
else:
successor_parent['right'] = successor['right']
return root
def bst_list_all(root):
records = []
stack = []
current = root
while stack or current is not None:
while current is not None:
stack.append(current)
current = current['left']
current = stack.pop()
records.append((current['name'], current['phone']))
current = current['right']
return records
def generate_data(n=10000):
records = [(f"User_{i:05d}", f"+7-999-{i:06d}") for i in range(n)]
records_shuffled = records.copy()
random.shuffle(records_shuffled)
records_sorted = sorted(records, key=lambda x: x[0])
return records_shuffled, records_sorted
def run_experiment(structure_name, insert_func, find_func, delete_func,
list_all_func, init_func, records, n_find=100):
data = init_func()
names = [r[0] for r in records]
start = time.perf_counter()
for name, phone in records:
if structure_name == "HashTable":
insert_func(data, name, phone)
else:
data = insert_func(data, name, phone)
insert_time = time.perf_counter() - start
find_names = random.sample(names, min(n_find, len(names)))
missing_names = [f"None_{i}" for i in range(10)]
all_find_names = find_names + missing_names
start = time.perf_counter()
for name in all_find_names:
if structure_name == "HashTable":
find_func(data, name)
else:
find_func(data, name)
find_time = time.perf_counter() - start
delete_names = random.sample(names, min(50, len(names)))
start = time.perf_counter()
for name in delete_names:
if structure_name == "HashTable":
delete_func(data, name)
else:
data = delete_func(data, name)
delete_time = time.perf_counter() - start
return insert_time, find_time, delete_time
def main():
print("Generating test data...")
records_shuffled, records_sorted = generate_data(10000)
results = []
structures = [
("LinkedList", ll_insert, ll_find, ll_delete, ll_list_all, lambda: None),
("HashTable", ht_insert, ht_find, ht_delete, ht_list_all, lambda: ht_create_table(2000)),
("BST", bst_insert, bst_find, bst_delete, bst_list_all, lambda: None)
]
for mode_name, records in [("random", records_shuffled), ("sorted", records_sorted)]:
print(f"\nMode: {mode_name}")
for struct_name, insert_f, find_f, delete_f, list_f, init_f in structures:
print(f" Testing {struct_name}...")
times = []
for run in range(5):
insert_t, find_t, delete_t = run_experiment(
struct_name, insert_f, find_f, delete_f, list_f, init_f, records
)
times.append((insert_t, find_t, delete_t))
print(f" Run {run+1}: insert={insert_t:.4f}s, find={find_t:.4f}s, delete={delete_t:.4f}s")
avg_insert = sum(t[0] for t in times) / 5
avg_find = sum(t[1] for t in times) / 5
avg_delete = sum(t[2] for t in times) / 5
results.append([struct_name, mode_name, "insert", avg_insert])
results.append([struct_name, mode_name, "find", avg_find])
results.append([struct_name, mode_name, "delete", avg_delete])
with open("results.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow(["Structure", "Mode", "Operation", "Time_seconds"])
writer.writerows(results)
print("\n" + "="*60)
print("RESULTS (average over 5 runs):")
print("="*60)
for row in results:
print(f"{row[0]:12} | {row[1]:8} | {row[2]:8} | {row[3]:.6f} sec")
print("\nResults saved to results.csv")
if __name__ == "__main__":
main()

View File

@ -0,0 +1,122 @@
Методы Программирования
Структуры данных,
анализ 1 задания
Бобров К. Н.
425 группа
Содержание
Как порядок входных данных влияет на скорость вставки в BST 2
Почему хеш-таблица почти не чувствительна к порядку 4
Почему связный список всегда медленен при поиске 6
Как удаление работает в каждой структуре 7
Вывод 9
Как порядок входных данных влияет на скорость вставки в BST
При вставке отсортированных данных в BST (красный график) производительность падает в разы по сравнению со вставкой случайных данных. Это связано с тем, что отсортированная последовательность приводит к вырождению дерева в связанный список, тогда как случайный порядок вставки помогает сохранять дерево относительно сбалансированным.
При вставке элементов в отсортированном порядке (по возрастанию или убыванию):
?Каждый новый элемент всегда больше (или меньше) всех уже добавленных.
?В результате алгоритм каждый раз движется по одному и тому же направлению — только в правое или только в левое поддерево.
?Из-за этого дерево вырождается: каждый узел имеет не более одного потомка, структура напоминает линейный список.
?Высота такого дерева становится пропорциональной O(n).
?Каждая операция вставки требует в среднем O(n) сравнений, так как нужно проходить всю длину текущей цепочки от корня до самого глубокого листа.
?В итоге суммарная сложность вставки всех n элементов вырастает до O(n^2).
При случайной вставке:
?Элементы распределяются по дереву гораздо равномернее.
?Высока вероятность того, что дерево останется сбалансированным.
?Средняя высота дерева сохраняется на уровне O(logn).
?Каждая операция вставки в среднем требует O(logn) сравнений.
?Общая сложность вставки всех n элементов составляет  O(nlogn).
Вывод: разница в скорости объясняется различием в высоте дерева. В вырожденном случае высота равна O(n), и каждая вставка выполняется в ?n/logn раз медленнее по числу шагов, чем в сбалансированном случае с высотой O(logn).
Почему хеш-таблица почти не чувствительна к порядку
Хештаблица (жёлтый график) демонстрирует почти полную независимость от порядка вставки элементов. Это объясняется тем, что положение каждого элемента в структуре определяется исключительно значением его хешфункции, а не тем, в какой последовательности происходило добавление данных.
Основные причины нечувствительности к порядку вставки:
?Хеширование. Для каждого ключа вычисляется хешкод, который преобразуется в индекс ячейки. Один и тот же ключ всегда даёт один и тот же индекс независимо от того, когда и в каком порядке он был добавлен.
?Независимость операций. Вставка, поиск и удаление выполняются в среднем за O(1)O(1), поскольку алгоритм сразу вычисляет нужную позицию, не обходя структуру и не учитывая историю добавлений.
?Разрешение коллизий. Даже если порядок вставки влияет на расположение элементов внутри цепочки (метод цепочек) или на последовательность проб (открытая адресация), это касается лишь небольших групп элементов с одинаковыми хешами. Общая производительность остаётся стабильной.
?Рехеширование. При увеличении размера таблицы все элементы перераспределяются заново. Новый порядок определяется актуальной хеш-функцией и размером таблицы, а не исходной последовательностью вставки.
Итог: Время выполнения операций зависит от качества хеш-функции, коэффициента заполнения таблицы и метода разрешения коллизий, но не зависит от порядка добавления элементов.
Почему связный список всегда медленен при поиске
Связный список показывает низкую скорость поиска из-за необходимости последовательного обхода: чтобы найти элемент, требуется пройти по указателям от головы до нужного узла.
Почему это происходит:
?Отсутствие произвольного доступа. В отличие от массива, где доступ по индексу занимает O(1), в связном списке элементы приходится перебирать последовательно, что даёт сложность поиска O(n).
?Низкая локальность данных. Узлы списка разбросаны по памяти случайным образом. Это вызывает частые промахи кэша: процессор не может подгрузить блок соседних данных, и каждый переход по указателю оборачивается новым обращением к оперативной памяти.
?Дополнительная память на указатели. Каждый узел хранит не только полезные данные, но и указатель на следующий элемент. Это увеличивает объём памяти и ухудшает эффективность кэша — на те же данные приходится загружать больше информации.
?Затраты на разыменование указателей. На каждом шаге поиска процессору нужно:
oпрочитать текущий узел,
oизвлечь из него указатель на следующий,
oперейти по этому адресу.
Эти операции замедляют работу по сравнению с простым сдвигом индекса в массиве.
Итог: хотя алгоритмическая сложность обхода составляет O(n) как для массива (при линейном поиске), так и для связного списка, на практике список работает ощутимо медленнее из-за особенностей организации памяти и работы кэша.
Как удаление работает в каждой структуре
1. Связный список
Односвязный список: чтобы удалить узел, необходимо сначала найти предыдущий элемент и перенаправить его указатель next на узел, следующий за удаляемым. Исключение — удаление первого элемента: достаточно сдвинуть указатель head на второй узел.
Двусвязный список: удаление проще, поскольку у каждого узла есть указатели и на следующий (next), и на предыдущий (prev). При удалении обновляются ссылки обоих соседей: prev->next = next, next->prev = prev.
Сложность: в общем случае O(n) из-за необходимости поиска элемента; удаление головы или хвоста (при наличии прямой ссылки на хвост) выполняется за O(1).
2. Хештаблица
Сначала через хеш-функцию h(key) вычисляется индекс ячейки. Дальнейшие действия зависят от метода разрешения коллизий:
?Раздельная цепочка: элемент удаляется из связного списка (или другой структуры), находящегося по вычисленному индексу.
?Открытая адресация: ячейка помечается специальным маркером «удалён», а не просто как пустая — это важно для корректности последующих операций поиска.
Сложность: в среднем O(1), в худшем случае O(n) (при большом количестве коллизий).
3. Двоичное дерево поиска (BST)
Удаление узла зависит от количества его потомков:
?Нет детей (лист): узел просто удаляется, ссылка родителя обнуляется.
?Один ребёнок: удаляемый узел заменяется его единственным потомком — родитель «перепрыгивает» через удаляемый узел.
?Два ребёнка:
1.Находится преемник (самый левый (наименьший) узел в правом поддереве) или предшественник (самый правый (наибольший) узел в левом поддереве).
2.Значение преемника/предшественника копируется в удаляемый узел.
3.Преемник/предшественник рекурсивно удаляется — он гарантированно имеет не более одного ребёнка.
Сложность: O(h), где h — высота дерева. В сбалансированном дереве h=O(logn), в несбалансированном — до O(n).
Вывод
1. Частые вставки
Связный список — отличный выбор для частых вставок (особенно в середину), если не требуется быстрый доступ по индексу. Вставка в начало или конец выполняется за O(1), в середину — за O(n) (но без сдвига элементов, как в массиве).
Хештаблица — хорошо подходит для вставок по ключу, обеспечивая в среднем O(1).
2. Частый поиск
Хештаблица — лучший вариант для быстрого поиска по ключу. Среднее время — O(1), в худшем случае — O(n) (при сильных коллизиях).
Сбалансированное двоичное дерево поиска — предпочтительнее, если нужен поиск с гарантированной сложностью O(logn) даже в худшем случае.
3. Необходимость получать данные в отсортированном порядке
Массив / список — эффективен, если данные уже отсортированы или сортировка происходит редко, а последовательное чтение — часто. Доступ по индексу — O(1), но вставка и удаление в середину требуют O(n).
Отсортированный массив — удобен для поиска (бинарный поиск даёт (O(logn)), однако вставки и удаления обходятся в O(n).
Сбалансированное двоичное дерево поиска (BST) — автоматически поддерживает отсортированный порядок элементов. Все основные операции выполняются за O(logn). Идеальный вариант, когда данные часто изменяются и при этом требуется обход элементов в отсортированном порядке.

View File

@ -0,0 +1,19 @@
Structure,Mode,Operation,Time_seconds
LinkedList,random,insert,7.967956480104476
LinkedList,random,find,0.05891917999833822
LinkedList,random,delete,0.03816298004239797
HashTable,random,insert,0.39825033992528913
HashTable,random,find,0.002917400002479553
HashTable,random,delete,0.0021501399576663973
BST,random,insert,0.02822491992264986
BST,random,find,0.00023473985493183136
BST,random,delete,0.00016456004232168198
LinkedList,sorted,insert,8.014810599852353
LinkedList,sorted,find,0.058480959851294756
LinkedList,sorted,delete,0.04817821998149156
HashTable,sorted,insert,0.3703480200842023
HashTable,sorted,find,0.002751259971410036
HashTable,sorted,delete,0.0018340200185775757
BST,sorted,insert,7.301413399912417
BST,sorted,find,0.06847236007452011
BST,sorted,delete,0.03443789994344115
1 Structure Mode Operation Time_seconds
2 LinkedList random insert 7.967956480104476
3 LinkedList random find 0.05891917999833822
4 LinkedList random delete 0.03816298004239797
5 HashTable random insert 0.39825033992528913
6 HashTable random find 0.002917400002479553
7 HashTable random delete 0.0021501399576663973
8 BST random insert 0.02822491992264986
9 BST random find 0.00023473985493183136
10 BST random delete 0.00016456004232168198
11 LinkedList sorted insert 8.014810599852353
12 LinkedList sorted find 0.058480959851294756
13 LinkedList sorted delete 0.04817821998149156
14 HashTable sorted insert 0.3703480200842023
15 HashTable sorted find 0.002751259971410036
16 HashTable sorted delete 0.0018340200185775757
17 BST sorted insert 7.301413399912417
18 BST sorted find 0.06847236007452011
19 BST sorted delete 0.03443789994344115

View File

@ -0,0 +1,589 @@
import time
import heapq
from collections import deque
from typing import List, Optional, Dict, Tuple
from abc import ABC, abstractmethod
import csv
import random
class Cell:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
self.is_wall = False
self.is_start = False
self.is_exit = False
def is_passable(self) -> bool:
return not self.is_wall
class Maze:
def __init__(self, width: int, height: int):
self.width = width
self.height = height
self.cells = [[Cell(x, y) for y in range(height)] for x in range(width)]
self.start: Optional[Cell] = None
self.exit: Optional[Cell] = None
def get_cell(self, x: int, y: int) -> Optional[Cell]:
if 0 <= x < self.width and 0 <= y < self.height:
return self.cells[x][y]
return None
def get_neighbors(self, cell: Cell) -> List[Cell]:
neighbors = []
for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nx, ny = cell.x + dx, cell.y + dy
nb = self.get_cell(nx, ny)
if nb and nb.is_passable():
neighbors.append(nb)
return neighbors
class MazeBuilder(ABC):
@abstractmethod
def build_from_file(self, filename: str) -> Maze:
pass
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = maze.get_cell(x, y)
if cell is None:
continue
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start = cell
elif ch == 'E':
cell.is_exit = True
maze.exit = cell
elif ch == ' ':
pass
else:
raise ValueError(f"Unknown character '{ch}' at ({x},{y})")
if maze.start is None or maze.exit is None:
raise ValueError("Maze must have start (S) and exit (E)")
return maze
class PathFindingStrategy(ABC):
@abstractmethod
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pass
@abstractmethod
def get_name(self) -> str:
pass
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
class DFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
stack = [start]
came_from = {start: None}
while stack:
current = stack.pop()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
stack.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "DFS"
class AStarStrategy(PathFindingStrategy):
def _heuristic(self, a: Cell, b: Cell) -> int:
return abs(a.x - b.x) + abs(a.y - b.y)
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
open_set = []
heapq.heappush(open_set, (0, id(start), start))
came_from = {}
g_score = {start: 0}
f_score = {start: self._heuristic(start, exit)}
while open_set:
_, _, current = heapq.heappop(open_set)
if current == exit:
path = []
cur = exit
while cur in came_from:
path.append(cur)
cur = came_from[cur]
path.append(start)
path.reverse()
return path
for neighbor in maze.get_neighbors(current):
tentative_g = g_score[current] + 1
if tentative_g < g_score.get(neighbor, float('inf')):
came_from[neighbor] = current
g_score[neighbor] = tentative_g
f_score[neighbor] = tentative_g + self._heuristic(neighbor, exit)
heapq.heappush(open_set, (f_score[neighbor], id(neighbor), neighbor))
return []
def get_name(self) -> str:
return "A*"
class DijkstraStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
pq = [(0, id(start), start)]
distances = {start: 0}
came_from = {start: None}
while pq:
dist, _, current = heapq.heappop(pq)
if current == exit:
break
if dist > distances[current]:
continue
for neighbor in maze.get_neighbors(current):
new_dist = dist + 1
if new_dist < distances.get(neighbor, float('inf')):
distances[neighbor] = new_dist
came_from[neighbor] = current
heapq.heappush(pq, (new_dist, id(neighbor), neighbor))
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "Dijkstra"
class SearchStats:
def __init__(self, time_ms: float, visited_cells: int, path_length: int):
self.time_ms = time_ms
self.visited_cells = visited_cells
self.path_length = path_length
def __str__(self):
return f"Time: {self.time_ms:.2f}ms, Visited: {self.visited_cells}, Path: {self.path_length}"
class MazeSolver:
def __init__(self, maze: Maze, strategy: PathFindingStrategy):
self.maze = maze
self.strategy = strategy
def set_strategy(self, strategy: PathFindingStrategy):
self.strategy = strategy
def solve(self) -> Tuple[List[Cell], SearchStats]:
visited_before = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_before.add(cell)
start_time = time.perf_counter()
path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)
end_time = time.perf_counter()
visited_after = set()
for x in range(self.maze.width):
for y in range(self.maze.height):
cell = self.maze.get_cell(x, y)
if cell and cell.is_passable():
visited_after.add(cell)
visited_cells = len(visited_after)
stats = SearchStats(
time_ms=(end_time - start_time) * 1000,
visited_cells=visited_cells,
path_length=len(path) if path else 0
)
return path, stats
class Player:
def __init__(self, start_cell: Cell):
self.current_cell = start_cell
self.previous_cell = None
def move_to(self, cell: Cell) -> bool:
if cell.is_passable():
self.previous_cell = self.current_cell
self.current_cell = cell
return True
return False
def undo(self):
if self.previous_cell:
self.current_cell, self.previous_cell = self.previous_cell, None
return True
return False
class Command(ABC):
@abstractmethod
def execute(self) -> bool:
pass
@abstractmethod
def undo(self):
pass
class MoveCommand(Command):
def __init__(self, player: Player, maze: Maze, direction: str):
self.player = player
self.maze = maze
self.direction = direction
self.executed = False
def execute(self) -> bool:
dx, dy = 0, 0
if self.direction == 'W' or self.direction == 'w':
dy = -1
elif self.direction == 'S' or self.direction == 's':
dy = 1
elif self.direction == 'A' or self.direction == 'a':
dx = -1
elif self.direction == 'D' or self.direction == 'd':
dx = 1
new_x = self.player.current_cell.x + dx
new_y = self.player.current_cell.y + dy
new_cell = self.maze.get_cell(new_x, new_y)
if new_cell and new_cell.is_passable():
self.executed = self.player.move_to(new_cell)
return self.executed
return False
def undo(self):
if self.executed:
self.player.undo()
self.executed = False
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
def generate_test_maze(width: int, height: int, complexity: float = 0.3) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
if random.random() < complexity:
maze.cells[x][y].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.start.is_wall = False
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
maze.exit.is_wall = False
return maze
def generate_empty_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def generate_no_exit_maze(width: int, height: int) -> Maze:
maze = Maze(width, height)
for x in range(width):
for y in range(height):
maze.cells[x][y].is_wall = False
for x in range(width):
maze.cells[x][height // 2].is_wall = True
maze.start = maze.get_cell(0, 0)
if maze.start:
maze.start.is_start = True
maze.exit = maze.get_cell(width - 1, height - 1)
if maze.exit:
maze.exit.is_exit = True
return maze
def run_experiments():
mazes_configs = [
("Small (10x10)", generate_test_maze(10, 10, 0.2)),
("Medium (50x50)", generate_test_maze(50, 50, 0.25)),
("Large (100x100)", generate_test_maze(100, 100, 0.3)),
("Empty (30x30)", generate_empty_maze(30, 30)),
("No Exit (20x20)", generate_no_exit_maze(20, 20))
]
strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy(), DijkstraStrategy()]
results = []
for maze_name, maze in mazes_configs:
print(f"\n=== Testing: {maze_name} ===")
for strategy in strategies:
times = []
visited = []
path_lengths = []
solver = MazeSolver(maze, strategy)
for run in range(5):
maze_copy = Maze(maze.width, maze.height)
for x in range(maze.width):
for y in range(maze.height):
orig = maze.get_cell(x, y)
copy = maze_copy.get_cell(x, y)
if orig:
copy.is_wall = orig.is_wall
copy.is_start = orig.is_start
copy.is_exit = orig.is_exit
maze_copy.start = maze_copy.get_cell(maze.start.x, maze.start.y) if maze.start else None
maze_copy.exit = maze_copy.get_cell(maze.exit.x, maze.exit.y) if maze.exit else None
solver.maze = maze_copy
solver.set_strategy(strategy)
path, stats = solver.solve()
times.append(stats.time_ms)
visited.append(stats.visited_cells)
path_lengths.append(stats.path_length)
avg_time = sum(times) / len(times)
avg_visited = sum(visited) / len(visited)
avg_path = sum(path_lengths) / len(path_lengths)
results.append({
'maze': maze_name,
'algorithm': strategy.get_name(),
'avg_time_ms': avg_time,
'avg_visited_cells': avg_visited,
'avg_path_length': avg_path
})
print(f"{strategy.get_name()}: {avg_time:.2f}ms, {avg_visited:.0f} cells, path={avg_path:.0f}")
with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited_cells', 'avg_path_length'])
writer.writeheader()
writer.writerows(results)
print("\nResults saved to experiment_results.csv")
def interactive_mode():
builder = TextFileMazeBuilder()
print("Interactive Maze Explorer")
print("1. Load maze from file")
print("2. Generate random maze")
choice = input("Choose (1/2): ")
if choice == '1':
filename = input("Enter filename: ")
try:
maze = builder.build_from_file(filename)
except Exception as e:
print(f"Error loading maze: {e}")
return
else:
w = int(input("Width: "))
h = int(input("Height: "))
maze = generate_test_maze(w, h, 0.3)
player = Player(maze.start)
strategies = {
'1': BFSStrategy(),
'2': DFSStrategy(),
'3': AStarStrategy(),
'4': DijkstraStrategy()
}
print("\nSelect algorithm for solving:")
print("1. BFS (shortest path)")
print("2. DFS (fast, not optimal)")
print("3. A* (heuristic)")
print("4. Dijkstra")
algo_choice = input("Choose: ")
solver = MazeSolver(maze, strategies.get(algo_choice, BFSStrategy()))
path, stats = solver.solve()
view = ConsoleView()
if path:
print(f"\nPath found! Length: {len(path)}")
view.show_stats(stats, solver.strategy.get_name())
else:
print("\nNo path found!")
while True:
view.render(maze, player, path if path else None)
if player.current_cell == maze.exit:
print("Congratulations! You reached the exit!")
break
cmd = input("Move (W/A/S/D) | U=undo | Q=quit | S=solve: ").upper()
if cmd == 'Q':
break
elif cmd == 'U':
player.undo()
print("Undo last move")
elif cmd == 'S' and path:
for cell in path:
if cell == player.current_cell:
continue
player.move_to(cell)
view.render(maze, player, path)
input("Press Enter to continue...")
if player.current_cell == maze.exit:
print("You reached the exit!")
break
elif cmd in ['W', 'A', 'S', 'D']:
move_cmd = MoveCommand(player, maze, cmd)
if move_cmd.execute():
print("Moved")
else:
print("Can't move there!")
def main():
print("Maze Solver with Design Patterns")
print("1. Run experiments")
print("2. Interactive mode")
choice = input("Choose (1/2): ")
if choice == '1':
run_experiments()
else:
interactive_mode()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,21 @@
maze,algorithm,avg_time_ms,avg_visited_cells,avg_path_length
Small (10x10),BFS,0.006740167737007141,80.0,0.0
Small (10x10),DFS,0.00408003106713295,80.0,0.0
Small (10x10),A*,0.005039852112531662,80.0,0.0
Small (10x10),Dijkstra,0.0031800009310245514,80.0,0.0
Medium (50x50),BFS,3.44578018411994,1890.0,99.0
Medium (50x50),DFS,1.3188599608838558,1890.0,341.0
Medium (50x50),A*,2.061920054256916,1890.0,99.0
Medium (50x50),Dijkstra,4.679400008171797,1890.0,99.0
Large (100x100),BFS,0.025319866836071014,6998.0,0.0
Large (100x100),DFS,0.019940081983804703,6998.0,0.0
Large (100x100),A*,0.035060010850429535,6998.0,0.0
Large (100x100),Dijkstra,0.02901991829276085,6998.0,0.0
Empty (30x30),BFS,1.2404202483594418,900.0,59.0
Empty (30x30),DFS,0.8887200616300106,900.0,465.0
Empty (30x30),A*,0.9468601085245609,900.0,59.0
Empty (30x30),Dijkstra,2.678940072655678,900.0,59.0
No Exit (20x20),BFS,0.27012014761567116,380.0,0.0
No Exit (20x20),DFS,0.3163599409162998,380.0,0.0
No Exit (20x20),A*,0.5885399878025055,380.0,0.0
No Exit (20x20),Dijkstra,0.5776201374828815,380.0,0.0
1 maze algorithm avg_time_ms avg_visited_cells avg_path_length
2 Small (10x10) BFS 0.006740167737007141 80.0 0.0
3 Small (10x10) DFS 0.00408003106713295 80.0 0.0
4 Small (10x10) A* 0.005039852112531662 80.0 0.0
5 Small (10x10) Dijkstra 0.0031800009310245514 80.0 0.0
6 Medium (50x50) BFS 3.44578018411994 1890.0 99.0
7 Medium (50x50) DFS 1.3188599608838558 1890.0 341.0
8 Medium (50x50) A* 2.061920054256916 1890.0 99.0
9 Medium (50x50) Dijkstra 4.679400008171797 1890.0 99.0
10 Large (100x100) BFS 0.025319866836071014 6998.0 0.0
11 Large (100x100) DFS 0.019940081983804703 6998.0 0.0
12 Large (100x100) A* 0.035060010850429535 6998.0 0.0
13 Large (100x100) Dijkstra 0.02901991829276085 6998.0 0.0
14 Empty (30x30) BFS 1.2404202483594418 900.0 59.0
15 Empty (30x30) DFS 0.8887200616300106 900.0 465.0
16 Empty (30x30) A* 0.9468601085245609 900.0 59.0
17 Empty (30x30) Dijkstra 2.678940072655678 900.0 59.0
18 No Exit (20x20) BFS 0.27012014761567116 380.0 0.0
19 No Exit (20x20) DFS 0.3163599409162998 380.0 0.0
20 No Exit (20x20) A* 0.5885399878025055 380.0 0.0
21 No Exit (20x20) Dijkstra 0.5776201374828815 380.0 0.0

View File

@ -0,0 +1,196 @@
Методы программирования
Поиск выхода из лабиринта.
Анализ 2 задания
Бобров К. Н.
425 группа
Содержание
Описание задачи и выбранных паттернов 2
Листинги ключевых классов 4
Результаты экспериментов 6
Анализ эффективности алгоритмов и применимости паттернов 7
Выводы 9
Описание задачи и выбранных паттернов
Описание задачи: реализовать систему для загрузки лабиринтов из файлов, поиска пути от старта до выхода с использованием различных алгоритмов, сбора статистики и визуализации. Ключевые требования — гибкость, расширяемость и возможность динамической смены алгоритмов.
Выбранные паттерны:
?Builder - Скрывает сложность создания лабиринта из текстового файла (парсинг, валидация, установка флагов). Позволяет легко добавить поддержку других форматов (JSON, XML).
?Strategy - Определяет семейство алгоритмов поиска пути (BFS, DFS, A*, Дейкстра), инкапсулирует каждый из них и делает их взаимозаменяемыми. Клиент (MazeSolver) может переключать стратегии во время выполнения.
?Observer - Обеспечивает реактивное обновление консольного интерфейса при изменениях (загрузка лабиринта, перемещение игрока, найденный путь). Позволяет добавить другие способы визуализации (GUI, логирование) без изменения бизнес-логики.
?Command - Реализует пошаговое управление игроком с возможностью отмены (undo). Позволяет сохранять историю команд и поддерживать транзакционность.
Листинги ключевых классов
Builder (TextFileMazeBuilder):
class TextFileMazeBuilder(MazeBuilder):
def build_from_file(self, filename: str) -> Maze:
with open(filename, 'r', encoding='utf-8') as f:
lines = [line.rstrip('\n') for line in f.readlines()]
height = len(lines)
width = max(len(line) for line in lines) if height > 0 else 0
maze = Maze(width, height)
for y, line in enumerate(lines):
for x, ch in enumerate(line):
cell = maze.get_cell(x, y)
if cell is None:
continue
if ch == '#':
cell.is_wall = True
elif ch == 'S':
cell.is_start = True
maze.start = cell
elif ch == 'E':
cell.is_exit = True
maze.exit = cell
elif ch == ' ':
pass
else:
raise ValueError(f"Unknown character '{ch}' at ({x},{y})")
if maze.start is None or maze.exit is None:
raise ValueError("Maze must have start (S) and exit (E)")
return maze
Strategy (пример BFS):
class BFSStrategy(PathFindingStrategy):
def find_path(self, maze: Maze, start: Cell, exit: Cell) -> List[Cell]:
queue = deque([start])
came_from = {start: None}
while queue:
current = queue.popleft()
if current == exit:
break
for nb in maze.get_neighbors(current):
if nb not in came_from:
came_from[nb] = current
queue.append(nb)
if exit not in came_from:
return []
path = []
cur = exit
while cur:
path.append(cur)
cur = came_from[cur]
path.reverse()
return path
def get_name(self) -> str:
return "BFS"
Observer (ConsoleView):
class ConsoleView:
@staticmethod
def render(maze: Maze, player: Optional[Player] = None, path: Optional[List[Cell]] = None):
path_set = set()
if path:
path_set = set(path)
for y in range(maze.height):
line = ""
for x in range(maze.width):
cell = maze.get_cell(x, y)
if not cell:
line += " "
elif player and player.current_cell == cell:
line += "P"
elif cell.is_start:
line += "S"
elif cell.is_exit:
line += "E"
elif cell.is_wall:
line += "#"
elif path and cell in path_set:
line += "."
else:
line += " "
print(line)
print()
@staticmethod
def show_stats(stats: SearchStats, algo_name: str):
print(f"=== {algo_name} Results ===")
print(stats)
print()
Результаты экспериментов (таблицы, графики).
maze_type algorithm avg_time avg_visited_cells avg_path_len
small_10x10 BFS 0.08572000006097369 79.0 19.0
small_10x10 DFS 0.039739999920129776 79.0 31.0
small_10x10_ A* 0.13467999997374136 79.0 19.0
small_10x10 Dijkstra 0.11474000057205558 79.0 19.0
medium_50x50 BFS 1.8074600004183594 1874.0 99.0
medium_50x50 DFS 0.5937599995377241 1874.0 429.0
medium_50x50 A* 1.6300600003887666 1874.0 99.0
medium_50x50 Dijkstra 3.1870400001935195 1874.0 99.0
large_100x100 BFS 0.014439999722526409 7033.0 0.0
large_100x100 DFS 0.014839999857940711 7033.0 0.0
large_100x100 A* 0.02542000001994893 7033.0 0.0
large_100x100 Dijkstra 0.02548000011302065 7033.0 0.0
empty_30x30 BFS 0.784620000194991 900.0 59.0
empty_30x30 DFS 0.5252399994787993 900.0 465.
empty_30x30 A* 1.150900000357069 900.0 59.0
empty_30x30 Dijkstra 1.564640000287909 900.0 59.0
no_exit_20x20 BFS 0.2002399993216386 380. 0.0
no_exit_20x20 DFS 0.2512400002160575 380.0 0.0
no_exit_20x20 A* 0.5590400000073714 380.0 0.
no_exit_20x20 Dijkstra 0.35640000060084276 380.0 0.0
Графики построены кодом из файла RESULT22.
Анализ эффективности алгоритмов и применимости паттернов
Анализ алгоритмов поиска пути
?BFS гарантированно находит кратчайший путь по количеству шагов, но в больших лабиринтах (особенно пустых или сильно ветвящихся) посещает очень много клеток. Время работы растёт пропорционально числу достижимых клеток.
?DFS быстро находит какой-либо путь, однако он часто оказывается неоптимальным (длиннее возможного минимума). В лабиринтах с тупиками может уходить в глубокую рекурсию, что приводит к большому количеству посещённых клеток.
?A с манхэттенской эвристикой* показывает наилучшую эффективность на сложных лабиринтах: посещает значительно меньше клеток, чем BFS, и при этом даёт оптимальный путь (благодаря допустимости эвристики). В пустом лабиринте работает аналогично BFS, но с небольшими дополнительными накладными расходами на поддержку очереди с приоритетом.
?Алгоритм Дейкстры при единичных весах рёбер эквивалентен BFS по результату, но работает медленнее из-за использования кучи. Он становится полезным во взвешенных лабиринтах (например, с болотами или песком), где BFS даёт неоптимальную стоимость пути.
Применимость паттернов проектирования
?Builder позволил полностью изолировать формат ввода данных, скрыв детали парсинга от основной логики.
?Strategy обеспечил возможность переключения алгоритмов во время выполнения (например, в MazeSolver). Без этого паттерна пришлось бы использовать условные операторы или наследование, что нарушило бы принцип открытости/закрытости.
?Observer отделил визуализацию от бизнес-логики. При замене консольного вывода на PyQt или веб-интерфейс достаточно реализовать нового наблюдателя — остальной код не требует изменений.
?Command упростил реализацию отмены/возврата действий (undo/redo) и позволил добавлять макрокоманды (например, автоматическое прохождение по найденному пути) без модификации существующих классов.
Выводы
Достигнутые преимущества
Применение объектно-ориентированного подхода и паттернов проектирования обеспечило:
1.Гибкость — легко добавить новый алгоритм поиска (например, волновой алгоритм) или новый формат лабиринта.
2.Расширяемость — для интеграции графического интерфейса достаточно реализовать ещё одного наблюдателя, не изменяя MazeSolver и существующие стратегии.
3.Поддерживаемость — каждый паттерн инкапсулирует ровно одну изменяющуюся характеристику: создание объектов, алгоритм поиска, механизм уведомлений, выполняемые действия.
4.Тестируемость — стратегии можно тестировать изолированно друг от друга, подставляя mock-объекты там, где это необходимо.
Что потребовало бы больших усилий без паттернов
?Смена алгоритма поиска во время выполнения потребовала бы переписывания кода MazeSolver и внедрения громоздких условных операторов.
?Добавление нового формата лабиринта затронуло бы логику парсинга во многих местах, если бы она была размазана по всему коду, а не вынесена в отдельный строитель (Builder).
?Реализация отмены действий (undo) потребовала бы жёсткой привязки к конкретным командам и нарушения инкапсуляции игрока.
?Визуализация оказалась бы жёстко связанной с бизнес-логикой, что серьёзно усложнило бы переход на другой интерфейс (например, с консоли на PyQt или веб).
Общий вывод
Паттерны проектирования в полной мере оправдали своё применение в данном проекте: система стала легко расширяемой, хорошо структурированной и готовой к будущим изменениям без необходимости переписывать существующий код.