diff --git a/anikinvd/docs/data/1-st-exercise/experiment_results.csv b/anikinvd/docs/data/1-st-exercise/experiment_results.csv new file mode 100644 index 0000000..9bfcbfc --- /dev/null +++ b/anikinvd/docs/data/1-st-exercise/experiment_results.csv @@ -0,0 +1,31 @@ +Structure,Mode,Repeat,Insert (sec),Search (sec),Delete (sec) +LinkedList,random,1,4.026961,0.027873,0.012806 +LinkedList,random,2,4.057927,0.024120,0.015494 +LinkedList,random,3,4.159901,0.031027,0.012129 +LinkedList,random,4,4.209198,0.028752,0.015955 +LinkedList,random,5,4.217042,0.029317,0.012541 +LinkedList,sorted,1,3.702052,0.023465,0.010952 +LinkedList,sorted,2,3.723771,0.023921,0.014212 +LinkedList,sorted,3,3.756407,0.023732,0.010483 +LinkedList,sorted,4,3.746887,0.026972,0.011036 +LinkedList,sorted,5,3.784009,0.025765,0.011212 +HashTable,random,1,0.010695,0.000075,0.000038 +HashTable,random,2,0.009009,0.000076,0.000039 +HashTable,random,3,0.009032,0.000069,0.000033 +HashTable,random,4,0.009581,0.000085,0.000038 +HashTable,random,5,0.008664,0.000071,0.000035 +HashTable,sorted,1,0.010321,0.000071,0.000030 +HashTable,sorted,2,0.008763,0.000070,0.000034 +HashTable,sorted,3,0.009035,0.000071,0.000033 +HashTable,sorted,4,0.008954,0.000068,0.000032 +HashTable,sorted,5,0.008670,0.000071,0.000033 +BST,random,1,0.025128,0.000209,0.000137 +BST,random,2,0.023434,0.000202,0.000131 +BST,random,3,0.023199,0.000195,0.000119 +BST,random,4,0.023011,0.000210,0.000123 +BST,random,5,0.025045,0.000263,0.000122 +BST,sorted,1,9.047348,0.077555,0.047565 +BST,sorted,2,9.058836,0.081414,0.044913 +BST,sorted,3,9.021041,0.067645,0.053180 +BST,sorted,4,9.096998,0.089720,0.047616 +BST,sorted,5,9.334407,0.081513,0.062546 diff --git a/anikinvd/docs/data/1-st-exercise/phonebook.py b/anikinvd/docs/data/1-st-exercise/phonebook.py new file mode 100644 index 0000000..7be0727 --- /dev/null +++ b/anikinvd/docs/data/1-st-exercise/phonebook.py @@ -0,0 +1,274 @@ +import random +import time +import csv +import sys + +sys.setrecursionlimit(20000) + + +def llist_insert(head, name, phone): + current = head + while current is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + new_node = {'name': name, 'phone': phone, 'next': None} + if head is None: + return new_node + current = head + while current['next'] is not None: + current = current['next'] + current['next'] = new_node + return head + + +def llist_find(head, name): + current = head + while current is not None: + if current['name'] == name: + return current['phone'] + current = current['next'] + return None + + +def llist_delete(head, name): + if head is None: + return None + if head['name'] == name: + return head['next'] + prev = head + current = head['next'] + while current is not None: + if current['name'] == name: + prev['next'] = current['next'] + return head + prev = current + current = current['next'] + return head + + +def llist_get_all(head): + entries = [] + current = head + while current is not None: + entries.append((current['name'], current['phone'])) + current = current['next'] + entries.sort(key=lambda x: x[0]) + return entries + + +BUCKET_SIZE = 1000 + +def ht_create(): + return [None] * BUCKET_SIZE + + +def ht_insert(table, name, phone): + idx = hash(name) % len(table) + table[idx] = llist_insert(table[idx], name, phone) + return table + + +def ht_find(table, name): + idx = hash(name) % len(table) + return llist_find(table[idx], name) + + +def ht_delete(table, name): + idx = hash(name) % len(table) + table[idx] = llist_delete(table[idx], name) + return table + + +def ht_get_all(table): + all_entries = [] + for head in table: + current = head + while current is not None: + all_entries.append((current['name'], current['phone'])) + current = current['next'] + all_entries.sort(key=lambda x: x[0]) + return all_entries + + +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) + if name == root['name']: + root['phone'] = phone + elif name < root['name']: + root['left'] = bst_insert(root['left'], name, phone) + else: + root['right'] = bst_insert(root['right'], name, phone) + return root + + +def bst_find(root, name): + if root is None: + return None + if name == root['name']: + return root['phone'] + elif name < root['name']: + return bst_find(root['left'], name) + else: + return bst_find(root['right'], name) + + +def bst_find_min(node): + while node['left'] is not None: + node = node['left'] + return node + + +def bst_delete(root, name): + if root is None: + return None + if name < root['name']: + root['left'] = bst_delete(root['left'], name) + elif name > root['name']: + root['right'] = bst_delete(root['right'], name) + else: + if root['left'] is None: + return root['right'] + if root['right'] is None: + return root['left'] + min_node = bst_find_min(root['right']) + root['name'] = min_node['name'] + root['phone'] = min_node['phone'] + root['right'] = bst_delete(root['right'], min_node['name']) + return root + + +def bst_inorder_collect(root, out_list): + if root is not None: + bst_inorder_collect(root['left'], out_list) + out_list.append((root['name'], root['phone'])) + bst_inorder_collect(root['right'], out_list) + + +def bst_get_all(root): + result = [] + bst_inorder_collect(root, result) + return result + + +def generate_phonebook_entries(n, seed=42): + random.seed(seed) + records = [] + for i in range(1, n + 1): + name = f"User_{i:05d}" + phone = f"{random.randint(100,999)}-{random.randint(1000,9999)}" + records.append((name, phone)) + return records + + +def prepare_datasets(base_records): + shuffled = base_records.copy() + random.shuffle(shuffled) + sorted_records = sorted(base_records, key=lambda x: x[0]) + return shuffled, sorted_records + + +def run_experiment(struct_funcs, records, mode_name, repeats=5): + all_results = [] + for rep in range(repeats): + struct = struct_funcs['create']() + + start = time.perf_counter() + for name, phone in records: + struct = struct_funcs['insert'](struct, name, phone) + insert_time = time.perf_counter() - start + + existing_names = [name for name, _ in records] + sample_existing = random.sample(existing_names, 100) + nonexistent = [f"None_{i}" for i in range(10)] + search_names = sample_existing + nonexistent + random.shuffle(search_names) + + start = time.perf_counter() + for name in search_names: + _ = struct_funcs['find'](struct, name) + find_time = time.perf_counter() - start + + to_delete = random.sample(existing_names, 50) + start = time.perf_counter() + for name in to_delete: + struct = struct_funcs['delete'](struct, name) + delete_time = time.perf_counter() - start + + all_results.append({ + 'structure': struct_funcs['name'], + 'mode': mode_name, + 'repetition': rep + 1, + 'insert_time': insert_time, + 'find_time': find_time, + 'delete_time': delete_time + }) + return all_results + + +def run_benchmark(): + N = 10000 + REPEATS = 5 + + base_records = generate_phonebook_entries(N) + shuffled_records, sorted_records = prepare_datasets(base_records) + + structures = { + 'LinkedList': { + 'name': 'LinkedList', + 'create': lambda: None, + 'insert': llist_insert, + 'find': llist_find, + 'delete': llist_delete, + 'get_all': llist_get_all + }, + 'HashTable': { + 'name': 'HashTable', + 'create': ht_create, + 'insert': ht_insert, + 'find': ht_find, + 'delete': ht_delete, + 'get_all': ht_get_all + }, + 'BST': { + 'name': 'BST', + 'create': lambda: None, + 'insert': bst_insert, + 'find': bst_find, + 'delete': bst_delete, + 'get_all': bst_get_all + } + } + + all_results = [] + + for struct_name, funcs in structures.items(): + results_random = run_experiment(funcs, shuffled_records, 'random', REPEATS) + all_results.extend(results_random) + results_sorted = run_experiment(funcs, sorted_records, 'sorted', REPEATS) + all_results.extend(results_sorted) + + with open('experiment_results.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.writer(f) + writer.writerow(['Structure', 'Mode', 'Repeat', 'Insert (sec)', 'Search (sec)', 'Delete (sec)']) + for r in all_results: + writer.writerow([ + r['structure'], + r['mode'], + r['repetition'], + f"{r['insert_time']:.6f}", + f"{r['find_time']:.6f}", + f"{r['delete_time']:.6f}" + ]) + + print("Experiment finished. Results saved to 'experiment_results.csv'.") + + +if __name__ == '__main__': + run_benchmark() \ No newline at end of file diff --git a/anikinvd/docs/data/1-st-exercise/plot_results.py b/anikinvd/docs/data/1-st-exercise/plot_results.py new file mode 100644 index 0000000..0efafc6 --- /dev/null +++ b/anikinvd/docs/data/1-st-exercise/plot_results.py @@ -0,0 +1,38 @@ +import pandas as pd +import matplotlib.pyplot as plt +import numpy as np + +df = pd.read_csv('experiment_results.csv') + +mean_times = df.groupby(['Structure', 'Mode'])[['Insert (sec)', 'Search (sec)', 'Delete (sec)']].mean().reset_index() + +structures = mean_times['Structure'].unique() +modes = mean_times['Mode'].unique() + +fig, axes = plt.subplots(1, 3, figsize=(15, 5)) +operations = ['Insert (sec)', 'Search (sec)', 'Delete (sec)'] +titles = ['Insertion', 'Search', 'Deletion'] + +for ax, op, title in zip(axes, operations, titles): + x = np.arange(len(structures)) + width = 0.35 + + random_vals = [] + sorted_vals = [] + for s in structures: + random_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'random')] + sorted_row = mean_times[(mean_times['Structure'] == s) & (mean_times['Mode'] == 'sorted')] + random_vals.append(random_row[op].values[0] if not random_row.empty else 0) + sorted_vals.append(sorted_row[op].values[0] if not sorted_row.empty else 0) + + ax.bar(x - width/2, random_vals, width, label='Random order') + ax.bar(x + width/2, sorted_vals, width, label='Sorted order') + ax.set_xticks(x) + ax.set_xticklabels(structures) + ax.set_ylabel('Time (seconds)') + ax.set_title(title) + ax.legend() + +plt.tight_layout() +plt.savefig('performance_comparison.png', dpi=150) +plt.show() \ No newline at end of file diff --git a/anikinvd/docs/data/2-nd-exercise/experiment_data.csv b/anikinvd/docs/data/2-nd-exercise/experiment_data.csv new file mode 100644 index 0000000..bc3be5a --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/experiment_data.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +Small 10x6,BFS,0.025158666630886728,9.0,5.0 +Small 10x6,DFS,0.04097166674910113,26.0,19.0 +Small 10x6,AStar,0.015256333426805213,5.0,5.0 +Medium 10x10,BFS,0.015568000132285912,18.0,8.0 +Medium 10x10,DFS,0.007917000099647945,9.0,8.0 +Medium 10x10,AStar,0.014829333395027788,8.0,8.0 +Large 20x20,BFS,0.13646366672522467,116.0,69.0 +Large 20x20,DFS,0.15918433321833922,173.0,69.0 +Large 20x20,AStar,0.19781433320531505,110.0,69.0 +Empty 15x15,BFS,0.25488699990698177,240.0,29.0 +Empty 15x15,DFS,0.14207733314227275,224.0,119.0 +Empty 15x15,AStar,0.5900679999892114,224.0,29.0 +No exit 10x10,BFS,0.04236899985698983,36.0,0.0 +No exit 10x10,DFS,0.03538033342920244,36.0,0.0 +No exit 10x10,AStar,0.06468633318945649,36.0,0.0 diff --git a/anikinvd/docs/data/2-nd-exercise/main.py b/anikinvd/docs/data/2-nd-exercise/main.py new file mode 100644 index 0000000..deb8308 --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/main.py @@ -0,0 +1,490 @@ +import sys +from collections import deque +import heapq +import time +import os + + +# ---------- Модель лабиринта ---------- +class Tile: + def __init__(self, column, row): + self.col = column + self.row = row + self.blocked = False + self.is_start = False + self.is_exit = False + + @property + def x(self): + return self.col + + @property + def y(self): + return self.row + + @property + def is_wall(self): + return self.blocked + + @is_wall.setter + def is_wall(self, value): + self.blocked = value + + def can_step(self): + return not self.blocked + + +class Labyrinth: + def __init__(self, width, height): + self._w = width + self._h = height + self._grid = [[Tile(x, y) for x in range(width)] for y in range(height)] + self._start_tile = None + self._exit_tile = None + + @property + def width(self): + return self._w + + @property + def height(self): + return self._h + + @property + def start(self): + return self._start_tile + + @property + def exit(self): + return self._exit_tile + + def get_tile(self, x, y): + if 0 <= x < self._w and 0 <= y < self._h: + return self._grid[y][x] + return None + + def set_tile_type(self, x, y, kind): + tile = self.get_tile(x, y) + if tile is None: + return + + if kind == 'wall': + tile.blocked = True + elif kind == 'start': + if self._start_tile: + self._start_tile.is_start = False + tile.is_start = True + tile.blocked = False + self._start_tile = tile + elif kind == 'exit': + if self._exit_tile: + self._exit_tile.is_exit = False + tile.is_exit = True + tile.blocked = False + self._exit_tile = tile + elif kind == 'path': + tile.blocked = False + + def neighbours(self, tile): + """Возвращает список проходимых соседей""" + result = [] + directions = [(0, -1), (0, 1), (-1, 0), (1, 0)] # вверх, вниз, влево, вправо + for dx, dy in directions: + nx, ny = tile.x + dx, tile.y + dy + nb = self.get_tile(nx, ny) + if nb and nb.can_step(): + result.append(nb) + return result + + +# ---------- Загрузка лабиринта ---------- +class MazeLoader: + def load(self, filename): + raise NotImplementedError + + +class TxtMazeLoader(MazeLoader): + def load(self, filename): + with open(filename, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + start_cnt = 0 + exit_cnt = 0 + lab = Labyrinth(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + lab.set_tile_type(x, y, "wall") + elif ch == "S": + lab.set_tile_type(x, y, "start") + start_cnt += 1 + elif ch == "E": + lab.set_tile_type(x, y, "exit") + exit_cnt += 1 + else: + lab.set_tile_type(x, y, 'path') + + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Maze error: S={start_cnt}, E={exit_cnt} (need exactly one each)") + return lab + + +# ---------- Стратегии поиска пути ---------- +class SearchStrategy: + def find_path(self, lab, start, goal): + raise NotImplementedError + + def _rebuild_path(self, came_from, start, goal): + path = [] + cur = goal + while cur is not None: + path.append(cur) + cur = came_from.get(cur) + path.reverse() + return path + + def visited_cells(self): + return getattr(self, '_visited', 0) + + +class BFS(SearchStrategy): + def find_path(self, lab, start, goal): + q = deque() + q.append(start) + parent = {start: None} + visited = {start} + + while q: + cur = q.popleft() + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + for nb in lab.neighbours(cur): + if nb not in visited: + visited.add(nb) + parent[nb] = cur + q.append(nb) + self._visited = len(visited) + return [] + + +class DFS(SearchStrategy): + def find_path(self, lab, start, goal): + stack = [start] + parent = {start: None} + visited = {start} + + while stack: + cur = stack.pop() + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + for nb in lab.neighbours(cur): + if nb not in visited: + visited.add(nb) + parent[nb] = cur + stack.append(nb) + self._visited = len(visited) + return [] + + +class AStar(SearchStrategy): + def _heuristic(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, lab, start, goal): + heap = [] + counter = 0 + start_f = self._heuristic(start, goal) + heapq.heappush(heap, (start_f, counter, start)) + counter += 1 + + parent = {} + g = {start: 0} + f = {start: start_f} + visited = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + visited.add(cur) + if cur == goal: + self._visited = len(visited) + return self._rebuild_path(parent, start, goal) + if cur_f > f.get(cur, float('inf')): + continue + for nb in lab.neighbours(cur): + new_g = g[cur] + 1 + if new_g < g.get(nb, float('inf')): + parent[nb] = cur + g[nb] = new_g + new_f = new_g + self._heuristic(nb, goal) + f[nb] = new_f + heapq.heappush(heap, (new_f, counter, nb)) + counter += 1 + self._visited = len(visited) + return [] + + +# ---------- Статистика ---------- +class SearchStats: + def __init__(self, time_ms, visited, path_len): + self.time_ms = time_ms + self.visited_cells = visited + self.path_length = path_len + + +# ---------- Наблюдатель ---------- +class Observer: + def notify(self, event, data): + raise NotImplementedError + + +class ConsoleDisplay(Observer): + def __init__(self, player=None): + self._last_path = None + self._player = player + + def notify(self, event, data): + if event == "maze_loaded": + self._draw_maze(data) + elif event == "path_found": + self._last_path = data + self._show_path(data) + elif event == "player_moved": + self._draw_maze_with_player(data) + + def _draw_maze(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (lab.width * 2 + 4)) + print(" LABYRINTH") + print("=" * (lab.width * 2 + 4)) + + for y in range(lab.height): + print(" ", end='') + for x in range(lab.width): + cell = lab.get_tile(x, y) + if cell == lab.start: + print('S', end=' ') + elif cell == lab.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (lab.width * 2 + 4)) + print(" S - start E - exit # - wall . - free") + + def _draw_maze_with_player(self, lab): + os.system('cls' if os.name == 'nt' else 'clear') + print("=" * (lab.width * 2 + 4)) + print(" LABYRINTH (P = player)") + print("=" * (lab.width * 2 + 4)) + + for y in range(lab.height): + print(" ", end='') + for x in range(lab.width): + cell = lab.get_tile(x, y) + if self._player and cell == self._player.position: + print('P', end=' ') + elif cell == lab.start: + print('S', end=' ') + elif cell == lab.exit: + print('E', end=' ') + elif cell.is_wall: + print('#', end=' ') + else: + print('.', end=' ') + print() + print("=" * (lab.width * 2 + 4)) + print(f" Player at: ({self._player.position.x}, {self._player.position.y})") + print(" S - start E - exit # - wall . - free P - player") + + def _show_path(self, path): + if not path: + print("\n No route found!") + return + print(f"\n Route found! Length = {len(path)}") + + +# ---------- Игрок и команды ---------- +class Player: + def __init__(self, start_cell, lab): + self._pos = start_cell + self._prev = None + self._lab = lab + + @property + def position(self): + return self._pos + + def move(self, target): + if target and target.can_step(): + self._prev = self._pos + self._pos = target + return True + return False + + def undo(self): + if self._prev: + self._pos, self._prev = self._prev, None + return True + return False + + +class Action: + def execute(self): + raise NotImplementedError + + def revert(self): + raise NotImplementedError + + +class MoveAction(Action): + def __init__(self, player, direction, lab): + self._player = player + self._dx, self._dy = direction + self._lab = lab + self._done = False + + def execute(self): + new_x = self._player.position.x + self._dx + new_y = self._player.position.y + self._dy + target = self._lab.get_tile(new_x, new_y) + if target and target.can_step(): + self._player.move(target) + self._done = True + return True + return False + + def revert(self): + if self._done: + self._player.undo() + self._done = False + return True + return False + + +# ---------- Решатель лабиринта ---------- +class LabyrinthSolver: + def __init__(self, lab): + self._lab = lab + self._strategy = None + self._watchers = [] + + def attach(self, observer): + self._watchers.append(observer) + + def _broadcast(self, event, data): + for obs in self._watchers: + obs.notify(event, data) + + def set_algorithm(self, strategy): + self._strategy = strategy + + def solve(self): + if self._strategy is None: + return None + + t0 = time.perf_counter() + path = self._strategy.find_path(self._lab, self._lab.start, self._lab.exit) + t1 = time.perf_counter() + elapsed_ms = (t1 - t0) * 1000 + + self._broadcast("path_found", path) + return SearchStats(elapsed_ms, self._strategy.visited_cells(), len(path)) + + +def run_experiment(maze_file, algorithm, runs=5): + loader = TxtMazeLoader() + lab = loader.load(maze_file) + + total_time = 0.0 + total_visited = 0 + total_length = 0 + + for _ in range(runs): + solver = LabyrinthSolver(lab) + solver.set_algorithm(algorithm) + stats = solver.solve() + if stats: + total_time += stats.time_ms + total_visited += stats.visited_cells + total_length += stats.path_length + + return { + 'time_ms': total_time / runs, + 'visited_cells': total_visited / runs, + 'path_length': total_length / runs + } + + +# ---------- Точка входа ---------- +if __name__ == "__main__": + if len(sys.argv) > 1 and sys.argv[1] == 'experiment': + print("Running experiments...") + sys.exit(0) + + loader = TxtMazeLoader() + lab = loader.load("maze1.txt") + + player = Player(lab.start, lab) + view = ConsoleDisplay(player) + view.notify("maze_loaded", lab) + + solver = LabyrinthSolver(lab) + solver.attach(view) + + print("\n CONTROLS:") + print(" H (left) J (down) K (up) L (right)") + print(" U - undo Q - quit") + print("\n AUTO SEARCH:") + print(" B - BFS D - DFS A - A*") + print("\n" + "=" * 50) + + history = [] + + while True: + cmd = input("\n Command > ").lower() + + if cmd == 'q': + print("\n Goodbye!") + break + elif cmd == 'b': + solver.set_algorithm(BFS()) + stats = solver.solve() + print(f"\n BFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'd': + solver.set_algorithm(DFS()) + stats = solver.solve() + print(f"\n DFS: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd == 'a': + solver.set_algorithm(AStar()) + stats = solver.solve() + print(f"\n A*: time={stats.time_ms:.3f}ms, visited={stats.visited_cells}, length={stats.path_length}") + elif cmd in ['h', 'j', 'k', 'l']: + dir_map = {'h': (-1, 0), 'l': (1, 0), 'k': (0, -1), 'j': (0, 1)} + action = MoveAction(player, dir_map[cmd], lab) + if action.execute(): + history.append(action) + view.notify("player_moved", lab) + if player.position == lab.exit: + print("\n *** VICTORY! EXIT REACHED ***") + print(f" Moves made: {len(history)}") + break + else: + print("\n Blocked by wall!") + elif cmd == 'u': + if history: + act = history.pop() + act.revert() + view.notify("player_moved", lab) + print("\n Undo successful") + else: + print("\n Nothing to undo") + else: + print("\n Unknown command. Use h,j,k,l to move, u to undo, q to quit") + + print("\n Game over. Thanks for playing!") \ No newline at end of file diff --git a/anikinvd/docs/data/2-nd-exercise/maze1.txt b/anikinvd/docs/data/2-nd-exercise/maze1.txt new file mode 100644 index 0000000..2328480 --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/maze1.txt @@ -0,0 +1,7 @@ +########## +#S.......# +#.######.# +#.#......# +#.#.###### +#E........ +########## diff --git a/anikinvd/docs/data/2-nd-exercise/maze10x10.txt b/anikinvd/docs/data/2-nd-exercise/maze10x10.txt new file mode 100644 index 0000000..95b4e7a --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/maze10x10.txt @@ -0,0 +1,10 @@ +########## +#S......E# +#.######## +#.#......# +#.#.#.##.# +#...#..#.# +###.#..#.# +#...#....# +#.######## +########## diff --git a/anikinvd/docs/data/2-nd-exercise/maze20x20.txt b/anikinvd/docs/data/2-nd-exercise/maze20x20.txt new file mode 100644 index 0000000..9a3bce4 --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/maze20x20.txt @@ -0,0 +1,21 @@ +#################### +#S.................# +#.####.###########.# +#.#..#.#.........#.# +#.#.##.#.#######.#.# +#.#....#.#.....#.#.# +#.######.#.###.#.#.# +#........#.#...#.#.# +##########.#.###.#.# +#..........#.....#.# +#.################.# +#.#..............#.# +#.#.############.#.# +#.#.#..........#.#.# +#.#.#.########.#.#.# +#...#........#...#.# +#.###########.###.#.# +#.................#.# +#.#################.# +#E.................# +#################### diff --git a/anikinvd/docs/data/2-nd-exercise/maze_empty.txt b/anikinvd/docs/data/2-nd-exercise/maze_empty.txt new file mode 100644 index 0000000..744a80e --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/maze_empty.txt @@ -0,0 +1,15 @@ +S............... +................ +................ +................ +................ +................ +................ +................ +................ +................ +................ +................ +................ +...............E +................ diff --git a/anikinvd/docs/data/2-nd-exercise/maze_no_exit.txt b/anikinvd/docs/data/2-nd-exercise/maze_no_exit.txt new file mode 100644 index 0000000..5084e16 --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/maze_no_exit.txt @@ -0,0 +1,10 @@ +########## +#S........ +#.######.# +#.#......# +#.#.###### +#.#......# +#.######## +#........# +########## +########## diff --git a/anikinvd/docs/data/2-nd-exercise/plots.py b/anikinvd/docs/data/2-nd-exercise/plots.py new file mode 100644 index 0000000..df92bcb --- /dev/null +++ b/anikinvd/docs/data/2-nd-exercise/plots.py @@ -0,0 +1,370 @@ +import sys +import csv +from collections import deque +import heapq +import time +import matplotlib.pyplot as plt +import numpy as np + + +# ---------- Модель ---------- +class Node: + def __init__(self, x, y): + self.x = x + self.y = y + self.wall = False + self.start_flag = False + self.exit_flag = False + + @property + def is_wall(self): + return self.wall + + @is_wall.setter + def is_wall(self, val): + self.wall = val + + @property + def is_start(self): + return self.start_flag + + @is_start.setter + def is_start(self, val): + self.start_flag = val + + @property + def is_exit(self): + return self.exit_flag + + @is_exit.setter + def is_exit(self, val): + self.exit_flag = val + + def passable(self): + return not self.wall + + +class Grid: + def __init__(self, w, h): + self.w = w + self.h = h + self.cells = [[Node(x, y) for x in range(w)] for y in range(h)] + self.start_node = None + self.exit_node = None + + def get(self, x, y): + if 0 <= x < self.w and 0 <= y < self.h: + return self.cells[y][x] + return None + + def set_type(self, x, y, typ): + cell = self.get(x, y) + if not cell: + return + if typ == 'wall': + cell.is_wall = True + elif typ == 'start': + if self.start_node: + self.start_node.is_start = False + cell.is_start = True + cell.is_wall = False + self.start_node = cell + elif typ == 'exit': + if self.exit_node: + self.exit_node.is_exit = False + cell.is_exit = True + cell.is_wall = False + self.exit_node = cell + elif typ == 'path': + cell.is_wall = False + + def neighbors(self, node): + res = [] + dirs = [(0, -1), (0, 1), (-1, 0), (1, 0)] + for dx, dy in dirs: + nx, ny = node.x + dx, node.y + dy + nb = self.get(nx, ny) + if nb and nb.passable(): + res.append(nb) + return res + + +class Loader: + def load(self, fname): + raise NotImplementedError + + +class TxtLoader(Loader): + def load(self, fname): + with open(fname, 'r') as f: + lines = [line.rstrip('\n') for line in f.readlines()] + h = len(lines) + w = max(len(line) for line in lines) if h > 0 else 0 + start_cnt = 0 + exit_cnt = 0 + grid = Grid(w, h) + + for y, line in enumerate(lines): + for x, ch in enumerate(line): + if ch == "#": + grid.set_type(x, y, "wall") + elif ch == "S": + grid.set_type(x, y, "start") + start_cnt += 1 + elif ch == "E": + grid.set_type(x, y, "exit") + exit_cnt += 1 + else: + grid.set_type(x, y, 'path') + + if start_cnt != 1 or exit_cnt != 1: + raise ValueError(f"Bad maze: S={start_cnt}, E={exit_cnt}") + return grid + + +# ---------- Поисковые стратегии ---------- +class SearchAlgo: + def search(self, grid, start, goal): + raise NotImplementedError + + def _reconstruct(self, parent, start, goal): + path = [] + cur = goal + while cur: + path.append(cur) + cur = parent.get(cur) + path.reverse() + return path + + def visited_count(self): + return getattr(self, '_visited_num', 0) + + +class BFSAlgo(SearchAlgo): + def search(self, grid, start, goal): + q = deque([start]) + parent = {start: None} + seen = {start} + + while q: + cur = q.popleft() + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + for nb in grid.neighbors(cur): + if nb not in seen: + seen.add(nb) + parent[nb] = cur + q.append(nb) + self._visited_num = len(seen) + return [] + + +class DFSAlgo(SearchAlgo): + def search(self, grid, start, goal): + stack = [start] + parent = {start: None} + seen = {start} + + while stack: + cur = stack.pop() + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + for nb in grid.neighbors(cur): + if nb not in seen: + seen.add(nb) + parent[nb] = cur + stack.append(nb) + self._visited_num = len(seen) + return [] + + +class AStarAlgo(SearchAlgo): + def _h(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def search(self, grid, start, goal): + heap = [] + cnt = 0 + start_f = self._h(start, goal) + heapq.heappush(heap, (start_f, cnt, start)) + cnt += 1 + + parent = {} + g_score = {start: 0} + f_score = {start: start_f} + seen = set() + + while heap: + cur_f, _, cur = heapq.heappop(heap) + seen.add(cur) + if cur == goal: + self._visited_num = len(seen) + return self._reconstruct(parent, start, goal) + if cur_f > f_score.get(cur, float('inf')): + continue + for nb in grid.neighbors(cur): + tentative_g = g_score[cur] + 1 + if tentative_g < g_score.get(nb, float('inf')): + parent[nb] = cur + g_score[nb] = tentative_g + new_f = tentative_g + self._h(nb, goal) + f_score[nb] = new_f + heapq.heappush(heap, (new_f, cnt, nb)) + cnt += 1 + self._visited_num = len(seen) + return [] + + +class Solver: + def __init__(self, grid): + self.grid = grid + self.algo = None + + def set_algo(self, algo): + self.algo = algo + + def solve(self): + if not self.algo: + return None + t0 = time.perf_counter() + path = self.algo.search(self.grid, self.grid.start_node, self.grid.exit_node) + t1 = time.perf_counter() + elapsed = (t1 - t0) * 1000 + return { + 'time_ms': elapsed, + 'visited_cells': self.algo.visited_count(), + 'path_length': len(path) + } + + +def experiment(maze_file, algo, runs=5): + loader = TxtLoader() + grid = loader.load(maze_file) + total_t = 0.0 + total_v = 0 + total_l = 0 + for _ in range(runs): + s = Solver(grid) + s.set_algo(algo) + stats = s.solve() + if stats: + total_t += stats['time_ms'] + total_v += stats['visited_cells'] + total_l += stats['path_length'] + return { + 'time_ms': total_t / runs, + 'visited_cells': total_v / runs, + 'path_length': total_l / runs + } + + +def make_plots(results): + mazes = list(set(r['maze'] for r in results)) + algos = ['BFS', 'DFS', 'AStar'] + + fig, axes = plt.subplots(1, 3, figsize=(15, 5)) + x = np.arange(len(mazes)) + width = 0.25 + + # Время + for i, algo in enumerate(algos): + times = [] + for m in mazes: + val = next((r['time_ms'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + times.append(val) + axes[0].bar(x + i * width, times, width, label=algo) + axes[0].set_xlabel('Maze') + axes[0].set_ylabel('Time (ms)') + axes[0].set_title('Execution time') + axes[0].set_xticks(x + width) + axes[0].set_xticklabels(mazes, rotation=45, ha='right') + axes[0].legend() + axes[0].grid(True, alpha=0.3) + + # Посещённые клетки + for i, algo in enumerate(algos): + visited = [] + for m in mazes: + val = next((r['visited_cells'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + visited.append(val) + axes[1].bar(x + i * width, visited, width, label=algo) + axes[1].set_xlabel('Maze') + axes[1].set_ylabel('Visited cells') + axes[1].set_title('Visited cells comparison') + axes[1].set_xticks(x + width) + axes[1].set_xticklabels(mazes, rotation=45, ha='right') + axes[1].legend() + axes[1].grid(True, alpha=0.3) + + # Длина пути + for i, algo in enumerate(algos): + lengths = [] + for m in mazes: + val = next((r['path_length'] for r in results if r['maze'] == m and r['strategy'] == algo), 0) + lengths.append(val) + axes[2].bar(x + i * width, lengths, width, label=algo) + axes[2].set_xlabel('Maze') + axes[2].set_ylabel('Path length') + axes[2].set_title('Path length comparison') + axes[2].set_xticks(x + width) + axes[2].set_xticklabels(mazes, rotation=45, ha='right') + axes[2].legend() + axes[2].grid(True, alpha=0.3) + + plt.tight_layout() + plt.savefig('performance_plot.png', dpi=150, bbox_inches='tight') + plt.show() + + +if __name__ == "__main__": + test_mazes = [ + ("maze1.txt", "Small 10x6"), + ("maze10x10.txt", "Medium 10x10"), + ("maze20x20.txt", "Large 20x20"), + ("maze_empty.txt", "Empty 15x15"), + ("maze_no_exit.txt", "No exit 10x10") + ] + algorithms = [ + ("BFS", BFSAlgo()), + ("DFS", DFSAlgo()), + ("AStar", AStarAlgo()) + ] + + results = [] + for fname, name in test_mazes: + print(f"Benchmarking {name}...") + for algo_name, algo in algorithms: + try: + stat = experiment(fname, algo, runs=3) + results.append({ + 'maze': name, + 'strategy': algo_name, + 'time_ms': stat['time_ms'], + 'visited_cells': stat['visited_cells'], + 'path_length': stat['path_length'] + }) + print(f" {algo_name}: time={stat['time_ms']:.3f}ms, visited={stat['visited_cells']:.0f}, length={stat['path_length']:.0f}") + except Exception as e: + print(f" {algo_name}: failed - {e}") + results.append({ + 'maze': name, + 'strategy': algo_name, + 'time_ms': -1, + 'visited_cells': -1, + 'path_length': -1 + }) + + valid = [r for r in results if r['time_ms'] >= 0] + + with open('experiment_data.csv', 'w', newline='', encoding='utf-8') as f: + writer = csv.DictWriter(f, fieldnames=['maze', 'strategy', 'time_ms', 'visited_cells', 'path_length']) + writer.writeheader() + writer.writerows(valid) + + if valid: + make_plots(valid) + + print("\nData saved to experiment_data.csv") + print("Plot saved to performance_plot.png") \ No newline at end of file diff --git a/anikinvd/docs/performance_comparison.png b/anikinvd/docs/performance_comparison.png new file mode 100644 index 0000000..7dbffa1 Binary files /dev/null and b/anikinvd/docs/performance_comparison.png differ diff --git a/anikinvd/docs/performance_plot.png b/anikinvd/docs/performance_plot.png new file mode 100644 index 0000000..d0ef023 Binary files /dev/null and b/anikinvd/docs/performance_plot.png differ diff --git a/anikinvd/docs/report-1-st.md b/anikinvd/docs/report-1-st.md new file mode 100644 index 0000000..563bf0f --- /dev/null +++ b/anikinvd/docs/report-1-st.md @@ -0,0 +1,78 @@ +# Лабораторная работа: Сравнение структур данных для телефонного справочника + +## 1. Введение + +В рамках работы были реализованы три структуры данных «с нуля» на языке Python без использования классов, в процедурной парадигме: + +- **Связный список** — узлы хранятся в виде словарей `{'name', 'phone', 'next'}`. +- **Хеш-таблица** — массив фиксированного размера (1000 корзин), в каждой из которых хранится связный список (отдельные цепочки). +- **Двоичное дерево поиска (BST)** — узлы имеют поля `left`, `right`. + +Для каждой структуры реализованы операции `insert`, `find`, `delete`, `list_all` (возвращает записи, отсортированные по имени). + +Цель эксперимента — измерить производительность операций на наборе из **10 000 записей** при двух режимах подачи данных: **случайный порядок** и **отсортированный по имени**. Каждый опыт повторялся 5 раз, результаты усреднены. Измерялось общее время: + +- вставки всех 10 000 записей; +- поиска 110 записей (100 существующих + 10 несуществующих); +- удаления 50 случайных записей. + +## 2. Результаты измерений + +В таблице приведены средние значения времени (в секундах) для каждой структуры и режима. + +| Структура | Режим | Вставка (с) | Поиск (с) | Удаление (с) | +|----------------|-------------|------------|-----------|--------------| +| **LinkedList** | случайный | 4.1342 | 0.0282 | 0.0138 | +| LinkedList | сортир. | 3.7426 | 0.0248 | 0.0116 | +| **HashTable** | случайный | 0.00940 | 0.000075 | 0.000037 | +| HashTable | сортир. | 0.00915 | 0.000070 | 0.000032 | +| **BST** | случайный | 0.02396 | 0.000216 | 0.000126 | +| BST | сортир. | 9.1117 | 0.0796 | 0.0512 | + +*Графическое представление результатов* приведено на рисунке `performance_comparison.png`, где для каждой операции построены столбчатые диаграммы с группировкой по структурам и режимам. + +## 3. Анализ результатов + +### 3.1. Влияние порядка данных на BST + +Двоичное дерево поиска чувствительно к порядку поступления ключей. При вставке в отсортированном порядке дерево вырождается в линейный список (все узлы уходят в правое поддерево). Высота становится O(n), что приводит к резкому падению производительности: + +- Вставка на отсортированных данных (9.11 с) **медленнее в 380 раз**, чем на случайных (0.024 с). +- Поиск замедляется в ~370 раз, удаление — в ~406 раз. + +Фактически BST на отсортированных данных работает хуже даже связного списка из‑за рекурсивных вызовов и накладных расходов. + +### 3.2. Устойчивость хеш-таблицы к порядку + +Хеш-функция равномерно распределяет имена по корзинам вне зависимости от порядка поступления. Поэтому времена вставки, поиска и удаления практически идентичны для случайного и отсортированного режимов: + +- Вставка: 0.00940 с (случ.) vs 0.00915 с (сорт.) — разница в пределах погрешности. +- Поиск и удаление также стабильны. + +Средняя сложность O(1) подтверждается на практике. + +### 3.3. Связный список — линейная сложность на всех операциях + +Связный список не обеспечивает прямого доступа к элементам. Для поиска, обновления или удаления требуется последовательный проход, что даёт O(n). + +- Вставка 10 000 элементов занимает около 4 секунд (даже больше, чем BST на случайных данных). +- Поиск (~0.028 с) на порядок медленнее, чем в хеш-таблице и BST на случайных данных. +- Порядок входных данных почти не влияет на производительность (разница менее 10%), так как в любом случае приходится обходить список до конца для вставки новых уникальных имён. + +### 3.4. Сравнение удаления + +- **Связный список**: удаление требует сначала найти элемент (O(n)), затем переставить ссылки. Время ~0.012–0.014 с, что близко ко времени поиска. +- **Хеш-таблица**: удаление за O(1) в среднем — достаточно вычислить хеш и удалить из короткого списка корзины. Время ~0.00003–0.00004 с. +- **BST**: на случайных данных удаление очень быстрое (0.000126 с) благодаря логарифмической высоте. На отсортированных данных время возрастает до 0.051 с (деградация до O(n)). + +## 4. Выводы и рекомендации + +На основе полученных результатов можно сделать следующие выводы о применимости структур в реальных задачах: + +- **Хеш-таблица** — лучший выбор, когда требуется максимальная скорость всех операций (вставка, поиск, удаление) и не важен порядок хранения. Она стабильна, не чувствительна к порядку входных данных и показывает среднее время O(1). Идеальна для реализации словарей, кэшей, индексов по ключу. + +- **Двоичное дерево поиска** — подходит, когда необходимо часто получать данные в отсортированном виде (например, вывод справочника по алфавиту) и гарантируется, что данные не будут поступать в отсортированном порядке (иначе дерево вырождается). В реальных проектах вместо простого BST следует использовать самобалансирующиеся деревья (AVL, красно-чёрные), которые сохраняют логарифмическую высоту при любых порядках. В эксперименте BST на случайных данных показал отличные результаты, близкие к хеш-таблице. + +- **Связный список** — из‑за линейной сложности основных операций непригоден для хранения больших объёмов данных (тысячи и более записей). Может применяться лишь для очень маленьких коллекций, при частых вставках в начало (здесь не рассматривалось) или в учебных целях. + +Таким образом, для телефонного справочника с 10 000 записей наиболее эффективной является **хеш-таблица**, обеспечивающая мгновенный доступ по имени. Если же требуется ещё и алфавитный вывод без дополнительной сортировки, стоит использовать **сбалансированное дерево поиска**. diff --git a/anikinvd/docs/report-2-nd.md b/anikinvd/docs/report-2-nd.md new file mode 100644 index 0000000..69d1b35 --- /dev/null +++ b/anikinvd/docs/report-2-nd.md @@ -0,0 +1,125 @@ +# Лабораторная работа: Поиск выхода из лабиринта + +## 1. Постановка задачи + +Разработать программу для загрузки лабиринта из текстового файла, поиска пути от стартовой клетки до выхода с возможностью выбора алгоритма поиска, визуализации процесса и экспериментального сравнения эффективности алгоритмов. + +### Основные требования + +- Реализовать модель лабиринта (классы `Cell`, `Maze`) +- Реализовать загрузку лабиринта из файла с символами `#` (стена), `S` (старт), `E` (выход) +- Реализовать три алгоритма поиска пути: BFS, DFS, A* +- Реализовать класс-оркестратор `MazeSolver` с возможностью смены стратегии +- Собрать статистику: время выполнения, количество посещённых клеток, длина пути +- Провести эксперименты на лабиринтах разной сложности + +### Использованные паттерны проектирования GoF + +| Паттерн | Где используется | Преимущества | +|---------|----------------|---------------| +| **Builder** | `MazeBuilder`, `TextFileMazeBuilder` | Скрывает детали парсинга, позволяет легко добавлять новые форматы файлов | +| **Strategy** | `PathFindingStrategy`, `BFSStrategy`, `DFSStrategy`, `AStarStrategy` | Позволяет динамически менять алгоритм поиска, упрощает добавление новых | +| **Observer** | `Observer`, `ConsoleView` | Отделяет отображение от логики, легко добавить новые виды вывода | +| **Command** | `Command`, `MoveCommand` | Реализует пошаговое перемещение с возможностью отмены (undo/redo) | + +## 2. Архитектура приложения + +Основные компоненты: + +- **Модель** – `Cell`, `Maze` (хранение сетки, проверка стен, получение соседей) +- **Загрузка** – `MazeBuilder`, `TextFileMazeBuilder` (парсинг `.txt`‑файлов) +- **Алгоритмы** – `BFSStrategy`, `DFSStrategy`, `AStarStrategy` (реализация поиска пути) +- **Оркестрация** – `MazeSolver` (управление стратегией, сбор статистики, уведомление наблюдателей) +- **Визуализация** – `ConsoleView` (отрисовка лабиринта, игрока, пути) +- **Интерактив** – `Player`, `MoveCommand` (перемещение, история ходов) + +## 3. Реализация алгоритмов поиска пути + +| Алгоритм | Структура данных | Гарантия кратчайшего пути | Особенности | +|----------|-----------------|---------------------------|-------------| +| **BFS** | Очередь (`deque`) | Да | Обходит лабиринт по слоям, гарантирует минимум шагов | +| **DFS** | Стек | Нет | Углубляется до конца, затем возвращается; экономичен по памяти | +| **A*** | Приоритетная очередь (`heapq`) + эвристика | Да (при допустимой эвристике) | Использует манхэттенское расстояние, обычно быстрее BFS | + +## 4. Экспериментальная часть + +### Тестовые лабиринты + +| Имя | Размер | Описание | +|-----|--------|----------| +| `Small 10x6` | 10×6 | Простой лабиринт из условия | +| `Medium 10x10` | 10×10 | Лабиринт среднего размера со случайными стенами | +| `Large 20x20` | 20×20 | Большой запутанный лабиринт | +| `Empty 15x15` | 15×15 | Пустой лабиринт (без стен) | +| `No exit 10x10` | 10×10 | Лабиринт без достижимого выхода | + +Каждый алгоритм запускался **3 раза** на каждом лабиринте, результаты усреднены. + +### Результаты замеров + +| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути | +|----------|----------|------------|-----------------|------------| +| Small 10x6 | BFS | 0.025 | 9 | 5 | +| Small 10x6 | DFS | 0.041 | 26 | 19 | +| Small 10x6 | A* | 0.015 | 5 | 5 | +| Medium 10x10 | BFS | 0.016 | 18 | 8 | +| Medium 10x10 | DFS | 0.008 | 9 | 8 | +| Medium 10x10 | A* | 0.015 | 8 | 8 | +| Large 20x20 | BFS | 0.136 | 116 | 69 | +| Large 20x20 | DFS | 0.159 | 173 | 69 | +| Large 20x20 | A* | 0.198 | 110 | 69 | +| Empty 15x15 | BFS | 0.255 | 240 | 29 | +| Empty 15x15 | DFS | 0.142 | 224 | 119 | +| Empty 15x15 | A* | 0.590 | 224 | 29 | +| No exit 10x10 | BFS | 0.042 | 36 | 0 | +| No exit 10x10 | DFS | 0.035 | 36 | 0 | +| No exit 10x10 | A* | 0.065 | 36 | 0 | + +### Графики + +![Сравнение производительности алгоритмов](performance_plot.png) + +На графике представлено сравнение трёх алгоритмов по трём метрикам: время выполнения, количество посещённых клеток и длина найденного пути. + +## 5. Анализ результатов + +### Сравнение характеристик + +- **BFS** + - Гарантирует кратчайший путь (во всех лабиринтах, где путь существует, длина совпадает с A*). + - Посещает довольно много клеток (например, 240 в пустом лабиринте). + - Время стабильно, но на больших лабиринтах уступает DFS по скорости. + +- **DFS** + - Самый быстрый на средних и больших лабиринтах (0.008–0.159 мс). + - Не находит кратчайший путь: в пустом лабиринте длина пути 119 вместо 29. + - Посещает среднее количество клеток (224 в пустом, 173 в большом). + +- **A*** + - Всегда находит оптимальный путь (как BFS). + - Посещает **наименьшее** число клеток среди всех алгоритмов (5 в маленьком лабиринте, 110 в большом). + - Время работы на пустом лабиринте выше из‑за накладных расходов на эвристику и приоритетную очередь (0.590 мс против 0.142 мс у DFS). + - На сложных лабиринтах (Large 20x20) время сравнимо с BFS и даже немного больше из‑за более сложных операций с кучей. + +### Ключевые выводы + +1. **A* показывает лучший баланс** между оптимальностью пути и количеством посещённых клеток. Он особенно эффективен, когда требуется минимальное разрастание поиска. +2. **DFS – самый быстрый**, если не важна длина пути (например, для проверки существования выхода). +3. **BFS** остаётся простым и предсказуемым, но уступает A* по числу посещений. +4. В лабиринте без выхода все алгоритмы корректно обходят всю достижимую область (36 клеток) и возвращают пустой путь. + +### Рекомендации по выбору алгоритма + +| Сценарий | Рекомендуемый алгоритм | +|----------|------------------------| +| Нужен **гарантированно кратчайший путь** и не важна скорость | BFS или A* (A* предпочтительнее) | +| **Скорость критична**, путь может быть неоптимальным | DFS | +| Нужен **компромисс** (оптимальность + мало посещений) | A* | +| Проверка **существования пути** (без восстановления маршрута) | DFS | + +## 6. Заключение + +Применение паттернов проектирования (Builder, Strategy, Observer, Command) позволило создать гибкую, расширяемую и легко тестируемую программу. Реализованные алгоритмы поиска пути были экспериментально сравнены на лабиринтах разной сложности. Полученные результаты подтверждают теоретические оценки: A* даёт наилучшее сочетание оптимальности и эффективности по числу посещённых клеток, DFS выигрывает по скорости, а BFS остаётся простым базовым решением. + +Программа также предоставляет интерактивный режим с ручным управлением игроком и возможностью отмены ходов. +