diff --git a/SmirnovVS/428.md b/SmirnovVS/428.md new file mode 100644 index 0000000..fb8a210 --- /dev/null +++ b/SmirnovVS/428.md @@ -0,0 +1 @@ +428 \ No newline at end of file diff --git a/SmirnovVS/docs/data/1-st-exercize/experiment.py b/SmirnovVS/docs/data/1-st-exercize/experiment.py new file mode 100644 index 0000000..63e0879 --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/experiment.py @@ -0,0 +1,132 @@ +"""Экспериментальное сравнение структур данных.""" + +import argparse +import csv +import random +from pathlib import Path +from statistics import mean +from time import perf_counter + +from phonebook import ( + bst_delete, bst_find, bst_insert, + ht_delete, ht_find, ht_insert, + ll_delete, ll_find, ll_insert, +) + + +def generate_records(size): + return [(f"User_{index:05d}", f"+7{index:010d}") for index in range(size)] + + +def measure_once(structure, records, existing_names, missing_names, deleted_names, bucket_count): + started = perf_counter() + if structure == "LinkedList": + data = None + for name, phone in records: + data = ll_insert(data, name, phone) + insert_time = perf_counter() - started + + started = perf_counter() + for name in existing_names + missing_names: + ll_find(data, name) + find_time = perf_counter() - started + + started = perf_counter() + for name in deleted_names: + data = ll_delete(data, name) + delete_time = perf_counter() - started + elif structure == "HashTable": + data = [None] * bucket_count + for name, phone in records: + ht_insert(data, name, phone) + insert_time = perf_counter() - started + + started = perf_counter() + for name in existing_names + missing_names: + ht_find(data, name) + find_time = perf_counter() - started + + started = perf_counter() + for name in deleted_names: + ht_delete(data, name) + delete_time = perf_counter() - started + else: + data = None + for name, phone in records: + data = bst_insert(data, name, phone) + insert_time = perf_counter() - started + + started = perf_counter() + for name in existing_names + missing_names: + bst_find(data, name) + find_time = perf_counter() - started + + started = perf_counter() + for name in deleted_names: + data = bst_delete(data, name) + delete_time = perf_counter() - started + + return {"insert": insert_time, "find_110": find_time, "delete_50": delete_time} + + +def run_experiment(size=3000, repeats=5, seed=2026, output_dir="docs/data"): + rng = random.Random(seed) + sorted_records = generate_records(size) + shuffled_records = sorted_records.copy() + rng.shuffle(shuffled_records) + modes = {"shuffled": shuffled_records, "sorted": sorted_records} + rows = [] + + for mode, records in modes.items(): + names = [record[0] for record in records] + test_cases = [] + for run in range(1, repeats + 1): + test_cases.append(( + run, + rng.sample(names, min(100, size)), + [f"None_{index}" for index in range(10)], + rng.sample(names, min(50, size)), + )) + for structure in ("LinkedList", "HashTable", "BST"): + for run, existing, missing, deleted in test_cases: + timings = measure_once(structure, records, existing, missing, deleted, max(17, size * 2 + 1)) + for operation, elapsed in timings.items(): + rows.append({ + "structure": structure, + "mode": mode, + "operation": operation, + "run": run, + "time_seconds": elapsed, + }) + + output = Path(output_dir) + output.mkdir(parents=True, exist_ok=True) + raw_path = output / "results_raw.csv" + with raw_path.open("w", newline="", encoding="utf-8-sig") as file: + writer = csv.DictWriter(file, fieldnames=rows[0].keys()) + writer.writeheader() + writer.writerows(rows) + + groups = {} + for row in rows: + key = (row["structure"], row["mode"], row["operation"]) + groups.setdefault(key, []).append(row["time_seconds"]) + summary = [ + {"structure": key[0], "mode": key[1], "operation": key[2], "mean_seconds": mean(values)} + for key, values in groups.items() + ] + summary_path = output / "results_summary.csv" + with summary_path.open("w", newline="", encoding="utf-8-sig") as file: + writer = csv.DictWriter(file, fieldnames=summary[0].keys()) + writer.writeheader() + writer.writerows(summary) + return raw_path, summary_path + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Сравнение структур телефонного справочника") + parser.add_argument("--size", type=int, default=3000) + parser.add_argument("--repeats", type=int, default=5) + args = parser.parse_args() + raw, summary = run_experiment(args.size, args.repeats) + print(f"Полные замеры: {raw}\nСредние значения: {summary}") diff --git a/SmirnovVS/docs/data/1-st-exercize/main.py b/SmirnovVS/docs/data/1-st-exercize/main.py new file mode 100644 index 0000000..0bf21c6 --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/main.py @@ -0,0 +1,25 @@ +from phonebook import ht_delete, ht_find, ht_insert, ht_list_all + + +def main(): + phonebook = [None] * 101 + print("Телефонный справочник. Команды: add, find, delete, list, exit") + while True: + command = input("> ").strip().lower() + if command == "add": + ht_insert(phonebook, input("Имя: ").strip(), input("Телефон: ").strip()) + elif command == "find": + print(ht_find(phonebook, input("Имя: ").strip()) or "Запись не найдена") + elif command == "delete": + ht_delete(phonebook, input("Имя: ").strip()) + elif command == "list": + for name, phone in ht_list_all(phonebook): + print(f"{name}: {phone}") + elif command == "exit": + break + else: + print("Неизвестная команда") + + +if __name__ == "__main__": + main() diff --git a/SmirnovVS/docs/data/1-st-exercize/phonebook.py b/SmirnovVS/docs/data/1-st-exercize/phonebook.py new file mode 100644 index 0000000..5c3ec08 --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/phonebook.py @@ -0,0 +1,162 @@ +"""Телефонный справочник на трёх структурах данных.""" + + +def _node(name, phone, next_node=None): + return {"name": name, "phone": phone, "next": next_node} + + +def ll_insert(head, name, phone): + """Добавить запись в конец списка или обновить существующую.""" + if head is None: + return _node(name, phone) + + current = head + while True: + if current["name"] == name: + current["phone"] = phone + return head + if current["next"] is None: + current["next"] = _node(name, phone) + return head + current = current["next"] + + +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"] + + previous, current = head, head["next"] + while current is not None: + if current["name"] == name: + previous["next"] = current["next"] + break + previous, current = 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"] + return sorted(records, key=lambda record: record[0]) + + +def _require_buckets(buckets): + if not buckets: + raise ValueError("Хеш-таблица должна содержать хотя бы один бакет") + + +def _hash_index(name, bucket_count): + """Хеш-функция.""" + value = 0 + for character in name: + value = (value * 31 + ord(character)) % bucket_count + return value + + +def ht_insert(buckets, name, phone): + _require_buckets(buckets) + index = _hash_index(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + + +def ht_find(buckets, name): + _require_buckets(buckets) + return ll_find(buckets[_hash_index(name, len(buckets))], name) + + +def ht_delete(buckets, name): + _require_buckets(buckets) + index = _hash_index(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + + +def ht_list_all(buckets): + _require_buckets(buckets) + records = [] + for head in buckets: + records.extend(ll_list_all(head)) + return sorted(records, key=lambda record: record[0]) + + +def _bst_node(name, phone): + return {"name": name, "phone": phone, "left": None, "right": None} + + +def bst_insert(root, name, phone): + if root is None: + return _bst_node(name, phone) + + current = root + while True: + if name == current["name"]: + current["phone"] = phone + return root + side = "left" if name < current["name"] else "right" + if current[side] is None: + current[side] = _bst_node(name, phone) + return root + current = current[side] + + +def bst_find(root, name): + current = root + while current is not None: + if name == current["name"]: + return current["phone"] + current = current["left"] if name < current["name"] else current["right"] + return None + + +def bst_delete(root, name): + parent = None + current = root + while current is not None and current["name"] != name: + parent = current + current = current["left"] if name < current["name"] else current["right"] + if current is None: + return root + + if current["left"] is not None and current["right"] is not None: + successor_parent = current + successor = current["right"] + while successor["left"] is not None: + successor_parent = successor + successor = successor["left"] + current["name"], current["phone"] = successor["name"], successor["phone"] + parent, current = successor_parent, successor + + child = current["left"] if current["left"] is not None else current["right"] + if parent is None: + return child + if parent["left"] is current: + parent["left"] = child + else: + parent["right"] = child + 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 diff --git a/SmirnovVS/docs/data/1-st-exercize/plot_results.py b/SmirnovVS/docs/data/1-st-exercize/plot_results.py new file mode 100644 index 0000000..606a834 --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/plot_results.py @@ -0,0 +1,32 @@ +import csv +from pathlib import Path + +import matplotlib.pyplot as plt + + +def create_plot(csv_path="docs/data/results_summary.csv", output_path="docs/data/performance.png"): + with Path(csv_path).open(encoding="utf-8-sig") as file: + rows = list(csv.DictReader(file)) + operations = ["insert", "find_110", "delete_50"] + titles = ["Вставка всех записей", "Поиск 110 имён", "Удаление 50 записей"] + labels = [(structure, mode) for mode in ("shuffled", "sorted") for structure in ("LinkedList", "HashTable", "BST")] + figure, axes = plt.subplots(1, 3, figsize=(15, 5)) + for axis, operation, title in zip(axes, operations, titles): + values = [] + for structure, mode in labels: + row = next(item for item in rows if item["structure"] == structure and item["mode"] == mode and item["operation"] == operation) + values.append(float(row["mean_seconds"]) * 1000) + axis.bar(range(len(labels)), values, color=["#4472C4", "#70AD47", "#ED7D31"] * 2) + axis.set_title(title) + axis.set_ylabel("Время, мс") + axis.set_xticks(range(len(labels)), [f"{s}\n{m}" for s, m in labels], rotation=35, ha="right") + axis.grid(axis="y", alpha=0.25) + figure.suptitle("Производительность структур данных (средние значения)") + figure.tight_layout() + figure.savefig(output_path, dpi=180) + plt.close(figure) + return Path(output_path) + + +if __name__ == "__main__": + print(f"График сохранён: {create_plot()}") diff --git a/SmirnovVS/docs/data/1-st-exercize/results_raw.csv b/SmirnovVS/docs/data/1-st-exercize/results_raw.csv new file mode 100644 index 0000000..9fa86ec --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/results_raw.csv @@ -0,0 +1,91 @@ +structure,mode,operation,run,time_seconds +LinkedList,shuffled,insert,1,0.1947654000005059 +LinkedList,shuffled,find_110,1,0.0063459000002694665 +LinkedList,shuffled,delete_50,1,0.002933899999334244 +LinkedList,shuffled,insert,2,0.19509340000058728 +LinkedList,shuffled,find_110,2,0.006126699999185803 +LinkedList,shuffled,delete_50,2,0.002876999999898544 +LinkedList,shuffled,insert,3,0.19163060000028054 +LinkedList,shuffled,find_110,3,0.00638989999970363 +LinkedList,shuffled,delete_50,3,0.0027322000005369773 +LinkedList,shuffled,insert,4,0.19537710000076913 +LinkedList,shuffled,find_110,4,0.00553089999993972 +LinkedList,shuffled,delete_50,4,0.00274009999975533 +LinkedList,shuffled,insert,5,0.19868119999955525 +LinkedList,shuffled,find_110,5,0.006262500000048021 +LinkedList,shuffled,delete_50,5,0.0034435999996276223 +HashTable,shuffled,insert,1,0.0045235999996293685 +HashTable,shuffled,find_110,1,0.00012590000005729962 +HashTable,shuffled,delete_50,1,6.089999988034833e-05 +HashTable,shuffled,insert,2,0.004393600000184961 +HashTable,shuffled,find_110,2,0.00011859999995067483 +HashTable,shuffled,delete_50,2,5.79999996261904e-05 +HashTable,shuffled,insert,3,0.0038748000006307848 +HashTable,shuffled,find_110,3,0.00011610000001383014 +HashTable,shuffled,delete_50,3,5.719999990105862e-05 +HashTable,shuffled,insert,4,0.003696299999319308 +HashTable,shuffled,find_110,4,0.00011779999931604834 +HashTable,shuffled,delete_50,4,5.729999975301325e-05 +HashTable,shuffled,insert,5,0.0037232000004223664 +HashTable,shuffled,find_110,5,0.0001156999996965169 +HashTable,shuffled,delete_50,5,5.5900000006658956e-05 +BST,shuffled,insert,1,0.004337399999712943 +BST,shuffled,find_110,1,0.0001140000003942987 +BST,shuffled,delete_50,1,7.019999975454994e-05 +BST,shuffled,insert,2,0.004390000000057626 +BST,shuffled,find_110,2,0.00011150000045745401 +BST,shuffled,delete_50,2,6.52999997328152e-05 +BST,shuffled,insert,3,0.004179900000053749 +BST,shuffled,find_110,3,0.00010879999990720535 +BST,shuffled,delete_50,3,6.070000017643906e-05 +BST,shuffled,insert,4,0.004183999999440857 +BST,shuffled,find_110,4,0.00010620000011840602 +BST,shuffled,delete_50,4,9.539999973640079e-05 +BST,shuffled,insert,5,0.004391600000417384 +BST,shuffled,find_110,5,0.00010419999944133451 +BST,shuffled,delete_50,5,6.630000007135095e-05 +LinkedList,sorted,insert,1,0.19158669999978883 +LinkedList,sorted,find_110,1,0.006112799999755225 +LinkedList,sorted,delete_50,1,0.005895900000723486 +LinkedList,sorted,insert,2,0.19256610000047658 +LinkedList,sorted,find_110,2,0.006044900000233611 +LinkedList,sorted,delete_50,2,0.002797499999360298 +LinkedList,sorted,insert,3,0.19292270000005374 +LinkedList,sorted,find_110,3,0.006467600000178209 +LinkedList,sorted,delete_50,3,0.0028355000004012254 +LinkedList,sorted,insert,4,0.19164809999983845 +LinkedList,sorted,find_110,4,0.005796100000225124 +LinkedList,sorted,delete_50,4,0.0030335999999806518 +LinkedList,sorted,insert,5,0.1920518999995693 +LinkedList,sorted,find_110,5,0.006033799999386247 +LinkedList,sorted,delete_50,5,0.0028001999999105465 +HashTable,sorted,insert,1,0.0036538000003929483 +HashTable,sorted,find_110,1,0.0001173999999082298 +HashTable,sorted,delete_50,1,5.810000038763974e-05 +HashTable,sorted,insert,2,0.003586599999835016 +HashTable,sorted,find_110,2,0.00011629999971773941 +HashTable,sorted,delete_50,2,5.540000074688578e-05 +HashTable,sorted,insert,3,0.00354940000033821 +HashTable,sorted,find_110,3,0.00011549999999260763 +HashTable,sorted,delete_50,3,5.520000013348181e-05 +HashTable,sorted,insert,4,0.0036109000002397806 +HashTable,sorted,find_110,4,0.00011329999961162684 +HashTable,sorted,delete_50,4,5.6000000768108293e-05 +HashTable,sorted,insert,5,0.003564399999959278 +HashTable,sorted,find_110,5,0.00011339999946358148 +HashTable,sorted,delete_50,5,5.480000072566327e-05 +BST,sorted,insert,1,0.3111501000003045 +BST,sorted,find_110,1,0.007159199999478005 +BST,sorted,delete_50,1,0.004548900000372669 +BST,sorted,insert,2,0.30066009999973176 +BST,sorted,find_110,2,0.008023899999898276 +BST,sorted,delete_50,2,0.004239900000357011 +BST,sorted,insert,3,0.3020464000001084 +BST,sorted,find_110,3,0.00876090000019758 +BST,sorted,delete_50,3,0.004445499999746971 +BST,sorted,insert,4,0.30183889999989333 +BST,sorted,find_110,4,0.007328300000153831 +BST,sorted,delete_50,4,0.005208799999309122 +BST,sorted,insert,5,0.3034312999998292 +BST,sorted,find_110,5,0.008107399999971676 +BST,sorted,delete_50,5,0.004352900000412774 diff --git a/SmirnovVS/docs/data/1-st-exercize/results_summary.csv b/SmirnovVS/docs/data/1-st-exercize/results_summary.csv new file mode 100644 index 0000000..b0a163e --- /dev/null +++ b/SmirnovVS/docs/data/1-st-exercize/results_summary.csv @@ -0,0 +1,19 @@ +structure,mode,operation,mean_seconds +LinkedList,shuffled,insert,0.19510954000033962 +LinkedList,shuffled,find_110,0.006131179999829328 +LinkedList,shuffled,delete_50,0.0029453599998305437 +HashTable,shuffled,insert,0.004042300000037358 +HashTable,shuffled,find_110,0.00011881999980687397 +HashTable,shuffled,delete_50,5.7859999833453914e-05 +BST,shuffled,insert,0.004296579999936512 +BST,shuffled,find_110,0.00010894000006373971 +BST,shuffled,delete_50,7.157999989431119e-05 +LinkedList,sorted,insert,0.19215509999994537 +LinkedList,sorted,find_110,0.006091039999955683 +LinkedList,sorted,delete_50,0.0034725400000752416 +HashTable,sorted,insert,0.0035930200001530466 +HashTable,sorted,find_110,0.00011517999973875703 +HashTable,sorted,delete_50,5.590000055235578e-05 +BST,sorted,insert,0.30382535999997345 +BST,sorted,find_110,0.007875939999939874 +BST,sorted,delete_50,0.004559200000039709 diff --git a/SmirnovVS/docs/data/2-nd-exercize/experiment.py b/SmirnovVS/docs/data/2-nd-exercize/experiment.py new file mode 100644 index 0000000..4024058 --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/experiment.py @@ -0,0 +1,64 @@ +import argparse +import csv +from pathlib import Path +from statistics import mean + +from generate_mazes import generate_all +from maze_app import AStarStrategy, BFSStrategy, DFSStrategy, MazeSolver, TextFileMazeBuilder + + +STRATEGIES = (BFSStrategy, DFSStrategy, AStarStrategy) + + +def run_experiment(repeats=7, maze_dir="mazes", output_dir="docs/data"): + generate_all(maze_dir) + builder = TextFileMazeBuilder() + rows = [] + for maze_path in sorted(Path(maze_dir).glob("*.txt")): + maze = builder.build_from_file(maze_path) + for strategy_type in STRATEGIES: + for run in range(1, repeats + 1): + stats = MazeSolver(maze, strategy_type()).solve() + rows.append({ + "maze": maze_path.stem, + "strategy": stats.strategy, + "run": run, + "time_ms": stats.time_ms, + "visited_cells": stats.visited_cells, + "path_length": stats.path_length, + "path_found": bool(stats.path), + }) + output = Path(output_dir) + output.mkdir(parents=True, exist_ok=True) + raw_path = output / "maze_results_raw.csv" + with raw_path.open("w", newline="", encoding="utf-8-sig") as file: + writer = csv.DictWriter(file, fieldnames=rows[0].keys()) + writer.writeheader() + writer.writerows(rows) + + groups = {} + for row in rows: + groups.setdefault((row["maze"], row["strategy"]), []).append(row) + summary = [] + for (maze_name, strategy), values in groups.items(): + summary.append({ + "maze": maze_name, + "strategy": strategy, + "mean_time_ms": mean(row["time_ms"] for row in values), + "mean_visited_cells": mean(row["visited_cells"] for row in values), + "path_length": values[0]["path_length"], + "path_found": values[0]["path_found"], + }) + summary_path = output / "maze_results_summary.csv" + with summary_path.open("w", newline="", encoding="utf-8-sig") as file: + writer = csv.DictWriter(file, fieldnames=summary[0].keys()) + writer.writeheader() + writer.writerows(summary) + return raw_path, summary_path + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Сравнение алгоритмов поиска пути") + parser.add_argument("--repeats", type=int, default=7) + args = parser.parse_args() + print("Результаты:", *run_experiment(args.repeats), sep="\n") diff --git a/SmirnovVS/docs/data/2-nd-exercize/generate_mazes.py b/SmirnovVS/docs/data/2-nd-exercize/generate_mazes.py new file mode 100644 index 0000000..b9fb1c3 --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/generate_mazes.py @@ -0,0 +1,52 @@ +"""Генерация воспроизводимых тестовых лабиринтов.""" + +import random +from pathlib import Path + + +def obstacle_maze(width, height, wall_probability, seed): + rng = random.Random(seed) + grid = [["#" if x in (0, width - 1) or y in (0, height - 1) else " " for x in range(width)] for y in range(height)] + for y in range(1, height - 1): + for x in range(1, width - 1): + if rng.random() < wall_probability: + grid[y][x] = "#" + # Оставляем гарантированный путь по верхней и правой внутренним границам. + for x in range(1, width - 1): + grid[1][x] = " " + for y in range(1, height - 1): + grid[y][width - 2] = " " + grid[1][1], grid[height - 2][width - 2] = "S", "E" + return "\n".join("".join(row) for row in grid) + "\n" + + +def empty_maze(width=50, height=50): + return obstacle_maze(width, height, 0, 1) + + +def blocked_maze(width=30, height=30): + lines = empty_maze(width, height).splitlines() + grid = [list(line) for line in lines] + exit_y, exit_x = height - 2, width - 2 + grid[exit_y - 1][exit_x] = "#" + grid[exit_y][exit_x - 1] = "#" + return "\n".join("".join(row) for row in grid) + "\n" + + +def generate_all(output_dir="mazes"): + output = Path(output_dir) + output.mkdir(parents=True, exist_ok=True) + maps = { + "small_10x10.txt": obstacle_maze(10, 10, 0.12, 10), + "medium_50x50.txt": obstacle_maze(50, 50, 0.28, 50), + "large_100x100.txt": obstacle_maze(100, 100, 0.32, 100), + "empty_50x50.txt": empty_maze(), + "no_path_30x30.txt": blocked_maze(), + } + for filename, content in maps.items(): + (output / filename).write_text(content, encoding="utf-8") + return list(maps) + + +if __name__ == "__main__": + print("Созданы файлы:", ", ".join(generate_all())) diff --git a/SmirnovVS/docs/data/2-nd-exercize/main.py b/SmirnovVS/docs/data/2-nd-exercize/main.py new file mode 100644 index 0000000..a118385 --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/main.py @@ -0,0 +1,24 @@ +import argparse + +from maze_app import AStarStrategy, BFSStrategy, ConsoleView, DFSStrategy, MazeSolver, TextFileMazeBuilder + + +STRATEGIES = {"bfs": BFSStrategy, "dfs": DFSStrategy, "astar": AStarStrategy} + + +def main(): + parser = argparse.ArgumentParser(description="Поиск выхода из лабиринта") + parser.add_argument("maze", nargs="?", default="mazes/small_10x10.txt") + parser.add_argument("--algorithm", choices=STRATEGIES, default="bfs") + args = parser.parse_args() + maze = TextFileMazeBuilder().build_from_file(args.maze) + view = ConsoleView() + solver = MazeSolver(maze, STRATEGIES[args.algorithm]()) + solver.attach(view) + stats = solver.solve() + print(view.render(maze, stats.path)) + print(f"Время: {stats.time_ms:.4f} мс; посещено: {stats.visited_cells}; длина пути: {stats.path_length}") + + +if __name__ == "__main__": + main() diff --git a/SmirnovVS/docs/data/2-nd-exercize/maze_app.py b/SmirnovVS/docs/data/2-nd-exercize/maze_app.py new file mode 100644 index 0000000..7335613 --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/maze_app.py @@ -0,0 +1,246 @@ +"""Модель лабиринта и алгоритмы поиска с паттернами Builder, Strategy, Observer.""" + +from abc import ABC, abstractmethod +from collections import deque +from dataclasses import dataclass, field +from heapq import heappop, heappush +from itertools import count +from pathlib import Path +from time import perf_counter + + +@dataclass(frozen=True) +class Cell: + x: int + y: int + is_wall: bool = False + is_start: bool = False + is_exit: bool = False + + def is_passable(self): + return not self.is_wall + + +class Maze: + def __init__(self, cells): + if not cells or not cells[0]: + raise ValueError("Лабиринт не может быть пустым") + self.cells = cells + self.height = len(cells) + self.width = len(cells[0]) + self.start = next((cell for row in cells for cell in row if cell.is_start), None) + self.exit = next((cell for row in cells for cell in row if cell.is_exit), None) + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + return None + + def get_neighbors(self, cell): + neighbors = [] + for dx, dy in ((0, -1), (1, 0), (0, 1), (-1, 0)): + neighbor = self.get_cell(cell.x + dx, cell.y + dy) + if neighbor is not None and neighbor.is_passable(): + neighbors.append(neighbor) + return neighbors + + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename): + pass + + +class TextFileMazeBuilder(MazeBuilder): + SYMBOLS = {"#", " ", "S", "E"} + + def build_from_file(self, filename): + lines = Path(filename).read_text(encoding="utf-8").splitlines() + if not lines or not lines[0] or any(len(line) != len(lines[0]) for line in lines): + raise ValueError("Строки лабиринта должны иметь одинаковую ненулевую длину") + unknown = {character for line in lines for character in line} - self.SYMBOLS + if unknown: + raise ValueError(f"Недопустимые символы: {sorted(unknown)}") + if sum(line.count("S") for line in lines) != 1 or sum(line.count("E") for line in lines) != 1: + raise ValueError("Лабиринт должен содержать ровно один старт S и один выход E") + + cells = [] + for y, line in enumerate(lines): + cells.append([ + Cell(x, y, symbol == "#", symbol == "S", symbol == "E") + for x, symbol in enumerate(line) + ]) + return Maze(cells) + + +class PathFindingStrategy(ABC): + name = "Неизвестно" + + def __init__(self): + self.visited_count = 0 + + @abstractmethod + def find_path(self, maze, start, exit_cell): + pass + + @staticmethod + def restore_path(parent, start, exit_cell): + if exit_cell not in parent: + return [] + path, current = [], exit_cell + while current is not None: + path.append(current) + current = parent[current] + path.reverse() + return path if path and path[0] == start else [] + + +class BFSStrategy(PathFindingStrategy): + name = "BFS" + + def find_path(self, maze, start, exit_cell): + queue = deque([start]) + parent = {start: None} + visited = 0 + while queue: + current = queue.popleft() + visited += 1 + if current == exit_cell: + break + for neighbor in maze.get_neighbors(current): + if neighbor not in parent: + parent[neighbor] = current + queue.append(neighbor) + self.visited_count = visited + return self.restore_path(parent, start, exit_cell) + + +class DFSStrategy(PathFindingStrategy): + name = "DFS" + + def find_path(self, maze, start, exit_cell): + stack = [start] + parent = {start: None} + visited = 0 + while stack: + current = stack.pop() + visited += 1 + if current == exit_cell: + break + for neighbor in maze.get_neighbors(current): + if neighbor not in parent: + parent[neighbor] = current + stack.append(neighbor) + self.visited_count = visited + return self.restore_path(parent, start, exit_cell) + + +class AStarStrategy(PathFindingStrategy): + name = "A*" + + @staticmethod + def heuristic(first, second): + return abs(first.x - second.x) + abs(first.y - second.y) + + def find_path(self, maze, start, exit_cell): + order = count() + queue = [(self.heuristic(start, exit_cell), next(order), start)] + distance = {start: 0} + parent = {start: None} + closed = set() + while queue: + _, _, current = heappop(queue) + if current in closed: + continue + closed.add(current) + if current == exit_cell: + break + for neighbor in maze.get_neighbors(current): + new_distance = distance[current] + 1 + if new_distance < distance.get(neighbor, float("inf")): + distance[neighbor] = new_distance + parent[neighbor] = current + priority = new_distance + self.heuristic(neighbor, exit_cell) + heappush(queue, (priority, next(order), neighbor)) + self.visited_count = len(closed) + return self.restore_path(parent, start, exit_cell) + + +@dataclass +class SearchStats: + strategy: str + time_ms: float + visited_cells: int + path_length: int + path: list = field(repr=False) + + +class Observer(ABC): + @abstractmethod + def update(self, event): + pass + + +class ConsoleView(Observer): + def __init__(self, verbose=True): + self.verbose = verbose + self.events = [] + + def update(self, event): + self.events.append(event) + if self.verbose: + print(event["message"]) + + def render(self, maze, path=None): + path_cells = set(path or []) + rows = [] + for row in maze.cells: + symbols = [] + for cell in row: + if cell.is_start: + symbols.append("S") + elif cell.is_exit: + symbols.append("E") + elif cell.is_wall: + symbols.append("#") + elif cell in path_cells: + symbols.append(".") + else: + symbols.append(" ") + rows.append("".join(symbols)) + return "\n".join(rows) + + +class MazeSolver: + def __init__(self, maze, strategy): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def set_strategy(self, strategy): + self.strategy = strategy + + def attach(self, observer): + if observer not in self.observers: + self.observers.append(observer) + + def notify(self, event): + for observer in self.observers: + observer.update(event) + + def solve(self): + self.notify({"type": "search_started", "message": f"Запущен алгоритм {self.strategy.name}"}) + started = perf_counter() + path = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit) + elapsed_ms = max((perf_counter() - started) * 1000, 1e-9) + stats = SearchStats( + strategy=self.strategy.name, + time_ms=elapsed_ms, + visited_cells=self.strategy.visited_count, + path_length=max(len(path) - 1, 0), + path=path, + ) + event_type = "path_found" if path else "path_not_found" + message = f"{self.strategy.name}: путь длиной {stats.path_length}" if path else f"{self.strategy.name}: путь не найден" + self.notify({"type": event_type, "message": message, "stats": stats}) + return stats diff --git a/SmirnovVS/docs/data/2-nd-exercize/maze_results_raw.csv b/SmirnovVS/docs/data/2-nd-exercize/maze_results_raw.csv new file mode 100644 index 0000000..7bdeffe --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/maze_results_raw.csv @@ -0,0 +1,106 @@ +maze,strategy,run,time_ms,visited_cells,path_length,path_found +empty_50x50,BFS,1,28.560300008393824,2304,94,True +empty_50x50,BFS,2,21.739099989645183,2304,94,True +empty_50x50,BFS,3,28.969400038477033,2304,94,True +empty_50x50,BFS,4,26.839600002858788,2304,94,True +empty_50x50,BFS,5,16.788399952929467,2304,94,True +empty_50x50,BFS,6,14.332599996123463,2304,94,True +empty_50x50,BFS,7,22.064500022679567,2304,94,True +empty_50x50,DFS,1,0.5307000246830285,95,94,True +empty_50x50,DFS,2,0.6820000126026571,95,94,True +empty_50x50,DFS,3,0.5209000082686543,95,94,True +empty_50x50,DFS,4,0.5133000086061656,95,94,True +empty_50x50,DFS,5,0.8089999901130795,95,94,True +empty_50x50,DFS,6,0.5200000014156103,95,94,True +empty_50x50,DFS,7,0.8933999924920499,95,94,True +empty_50x50,A*,1,68.07579996529967,2304,94,True +empty_50x50,A*,2,61.01420003687963,2304,94,True +empty_50x50,A*,3,52.171000046655536,2304,94,True +empty_50x50,A*,4,53.36979997809976,2304,94,True +empty_50x50,A*,5,64.77910000830889,2304,94,True +empty_50x50,A*,6,51.02490005083382,2304,94,True +empty_50x50,A*,7,70.88650000514463,2304,94,True +large_100x100,BFS,1,91.43029997358099,6409,194,True +large_100x100,BFS,2,98.55669998796657,6409,194,True +large_100x100,BFS,3,98.43990002991632,6409,194,True +large_100x100,BFS,4,51.18469998706132,6409,194,True +large_100x100,BFS,5,44.218000024557114,6409,194,True +large_100x100,BFS,6,28.500600019469857,6409,194,True +large_100x100,BFS,7,60.57189998682588,6409,194,True +large_100x100,DFS,1,25.9578000404872,1561,762,True +large_100x100,DFS,2,18.451499985530972,1561,762,True +large_100x100,DFS,3,18.506099993828684,1561,762,True +large_100x100,DFS,4,14.582899981178343,1561,762,True +large_100x100,DFS,5,10.724799998570234,1561,762,True +large_100x100,DFS,6,17.07870000973344,1561,762,True +large_100x100,DFS,7,19.463400007225573,1561,762,True +large_100x100,A*,1,38.1690000067465,1997,194,True +large_100x100,A*,2,30.891000002156943,1997,194,True +large_100x100,A*,3,23.280899971723557,1997,194,True +large_100x100,A*,4,26.04500000597909,1997,194,True +large_100x100,A*,5,27.82869996735826,1997,194,True +large_100x100,A*,6,26.94000001065433,1997,194,True +large_100x100,A*,7,28.832400043029338,1997,194,True +medium_50x50,BFS,1,6.693199975416064,1700,94,True +medium_50x50,BFS,2,6.831499980762601,1700,94,True +medium_50x50,BFS,3,8.055799989961088,1700,94,True +medium_50x50,BFS,4,7.26869999198243,1700,94,True +medium_50x50,BFS,5,11.507100018206984,1700,94,True +medium_50x50,BFS,6,7.060799980536103,1700,94,True +medium_50x50,BFS,7,7.459699991159141,1700,94,True +medium_50x50,DFS,1,3.0005000298842788,408,266,True +medium_50x50,DFS,2,1.8214000156149268,408,266,True +medium_50x50,DFS,3,2.6385000091977417,408,266,True +medium_50x50,DFS,4,1.7798999906517565,408,266,True +medium_50x50,DFS,5,1.7468000296503305,408,266,True +medium_50x50,DFS,6,1.7085999716073275,408,266,True +medium_50x50,DFS,7,1.7067999579012394,408,266,True +medium_50x50,A*,1,8.779299969319254,942,94,True +medium_50x50,A*,2,9.8794000223279,942,94,True +medium_50x50,A*,3,9.167099953629076,942,94,True +medium_50x50,A*,4,16.693999990820885,942,94,True +medium_50x50,A*,5,12.841499992646277,942,94,True +medium_50x50,A*,6,23.23459996841848,942,94,True +medium_50x50,A*,7,18.193199997767806,942,94,True +no_path_30x30,BFS,1,3.5794000141322613,781,0,False +no_path_30x30,BFS,2,3.4125999663956463,781,0,False +no_path_30x30,BFS,3,3.811199974734336,781,0,False +no_path_30x30,BFS,4,3.524600004311651,781,0,False +no_path_30x30,BFS,5,3.535500029101968,781,0,False +no_path_30x30,BFS,6,3.6441999836824834,781,0,False +no_path_30x30,BFS,7,3.4087999956682324,781,0,False +no_path_30x30,DFS,1,3.3659999608062208,781,0,False +no_path_30x30,DFS,2,3.659199981484562,781,0,False +no_path_30x30,DFS,3,3.4770999918691814,781,0,False +no_path_30x30,DFS,4,3.4067999804392457,781,0,False +no_path_30x30,DFS,5,3.334700013510883,781,0,False +no_path_30x30,DFS,6,3.7082999479025602,781,0,False +no_path_30x30,DFS,7,5.642200005240738,781,0,False +no_path_30x30,A*,1,7.9901000135578215,781,0,False +no_path_30x30,A*,2,8.304700022563338,781,0,False +no_path_30x30,A*,3,11.212200042791665,781,0,False +no_path_30x30,A*,4,14.160500024445355,781,0,False +no_path_30x30,A*,5,9.939100011251867,781,0,False +no_path_30x30,A*,6,8.455800008960068,781,0,False +no_path_30x30,A*,7,7.638900016900152,781,0,False +small_10x10,BFS,1,0.18330005696043372,58,14,True +small_10x10,BFS,2,0.14630000805482268,58,14,True +small_10x10,BFS,3,0.18289999570697546,58,14,True +small_10x10,BFS,4,0.14349998673424125,58,14,True +small_10x10,BFS,5,0.14270003885030746,58,14,True +small_10x10,BFS,6,0.1477000187151134,58,14,True +small_10x10,BFS,7,0.14270003885030746,58,14,True +small_10x10,DFS,1,0.042700034100562334,15,14,True +small_10x10,DFS,2,0.04120002267882228,15,14,True +small_10x10,DFS,3,0.041000021155923605,15,14,True +small_10x10,DFS,4,0.04099996294826269,15,14,True +small_10x10,DFS,5,0.04079996142536402,15,14,True +small_10x10,DFS,6,0.04060001811012626,15,14,True +small_10x10,DFS,7,0.04060001811012626,15,14,True +small_10x10,A*,1,0.30999997397884727,58,14,True +small_10x10,A*,2,0.30529999639838934,58,14,True +small_10x10,A*,3,0.325299974065274,58,14,True +small_10x10,A*,4,0.31670002499595284,58,14,True +small_10x10,A*,5,0.32130000181496143,58,14,True +small_10x10,A*,6,0.3467000206001103,58,14,True +small_10x10,A*,7,0.33109996002167463,58,14,True diff --git a/SmirnovVS/docs/data/2-nd-exercize/maze_results_summary.csv b/SmirnovVS/docs/data/2-nd-exercize/maze_results_summary.csv new file mode 100644 index 0000000..15e6f68 --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/maze_results_summary.csv @@ -0,0 +1,16 @@ +maze,strategy,mean_time_ms,mean_visited_cells,path_length,path_found +empty_50x50,BFS,22.75627143015819,2304,94,True +empty_50x50,DFS,0.6384714340258922,95,94,True +empty_50x50,A*,60.188757155888844,2304,94,True +large_100x100,BFS,67.55744285848257,6409,194,True +large_100x100,DFS,17.82360000236492,1561,762,True +large_100x100,A*,28.85528571537829,1997,194,True +medium_50x50,BFS,7.83954284686063,1700,94,True +medium_50x50,DFS,2.0575000006439432,408,266,True +medium_50x50,A*,14.112728556418526,942,94,True +no_path_30x30,BFS,3.559471424003797,781,0,False +no_path_30x30,DFS,3.799185697321913,781,0,False +no_path_30x30,A*,9.671614305781466,781,0,False +small_10x10,BFS,0.1555857348388859,58,14,True +small_10x10,DFS,0.041128576932741065,15,14,True +small_10x10,A*,0.3223428502678871,58,14,True diff --git a/SmirnovVS/docs/data/2-nd-exercize/plot_results.py b/SmirnovVS/docs/data/2-nd-exercize/plot_results.py new file mode 100644 index 0000000..1c262cc --- /dev/null +++ b/SmirnovVS/docs/data/2-nd-exercize/plot_results.py @@ -0,0 +1,35 @@ +import csv +from pathlib import Path + +import matplotlib.pyplot as plt + + +def create_plot(csv_path="docs/data/maze_results_summary.csv", output_path="docs/data/maze_performance.png"): + with Path(csv_path).open(encoding="utf-8-sig") as file: + rows = list(csv.DictReader(file)) + mazes = sorted({row["maze"] for row in rows}) + strategies = ["BFS", "DFS", "A*"] + colors = {"BFS": "#4472C4", "DFS": "#ED7D31", "A*": "#70AD47"} + figure, axes = plt.subplots(2, 1, figsize=(12, 9)) + positions = range(len(mazes)) + width = 0.24 + for index, strategy in enumerate(strategies): + selected = [next(row for row in rows if row["maze"] == maze and row["strategy"] == strategy) for maze in mazes] + offsets = [position + (index - 1) * width for position in positions] + axes[0].bar(offsets, [float(row["mean_time_ms"]) for row in selected], width, label=strategy, color=colors[strategy]) + axes[1].bar(offsets, [float(row["mean_visited_cells"]) for row in selected], width, label=strategy, color=colors[strategy]) + for axis, ylabel, title in zip(axes, ["Время, мс", "Посещено клеток"], ["Среднее время поиска", "Объём исследования лабиринта"]): + axis.set_xticks(list(positions), mazes, rotation=20, ha="right") + axis.set_ylabel(ylabel) + axis.set_title(title) + axis.grid(axis="y", alpha=0.25) + axis.legend() + figure.suptitle("Сравнение стратегий поиска (средние значения)") + figure.tight_layout() + figure.savefig(output_path, dpi=180) + plt.close(figure) + return Path(output_path) + + +if __name__ == "__main__": + print(f"График сохранён: {create_plot()}") diff --git a/SmirnovVS/docs/maze_performance.png b/SmirnovVS/docs/maze_performance.png new file mode 100644 index 0000000..e7da2f8 Binary files /dev/null and b/SmirnovVS/docs/maze_performance.png differ diff --git a/SmirnovVS/docs/performance.png b/SmirnovVS/docs/performance.png new file mode 100644 index 0000000..41836e1 Binary files /dev/null and b/SmirnovVS/docs/performance.png differ diff --git a/SmirnovVS/docs/Поиск пути в лабиринте.docx b/SmirnovVS/docs/Поиск пути в лабиринте.docx new file mode 100644 index 0000000..e9bc258 Binary files /dev/null and b/SmirnovVS/docs/Поиск пути в лабиринте.docx differ diff --git a/SmirnovVS/docs/Структуры данных.docx b/SmirnovVS/docs/Структуры данных.docx new file mode 100644 index 0000000..2527cb7 Binary files /dev/null and b/SmirnovVS/docs/Структуры данных.docx differ