diff --git a/lab1/docs/data/graph_delete.png b/lab1/docs/data/graph_delete.png new file mode 100644 index 0000000..542ee28 Binary files /dev/null and b/lab1/docs/data/graph_delete.png differ diff --git a/lab1/docs/data/graph_find.png b/lab1/docs/data/graph_find.png new file mode 100644 index 0000000..accc3e0 Binary files /dev/null and b/lab1/docs/data/graph_find.png differ diff --git a/lab1/docs/data/graph_insert.png b/lab1/docs/data/graph_insert.png new file mode 100644 index 0000000..03e6b56 Binary files /dev/null and b/lab1/docs/data/graph_insert.png differ diff --git a/lab1/docs/data/results.csv b/lab1/docs/data/results.csv new file mode 100644 index 0000000..2a5b713 --- /dev/null +++ b/lab1/docs/data/results.csv @@ -0,0 +1,19 @@ +structure,order,operation,run1,run2,run3,run4,run5,average +LinkedList,random,insert,3.000600399999712,3.022712899999533,2.9421689999999217,2.9075659000000087,3.0319512999994913,2.980999899999733 +LinkedList,random,find,0.031094500000108383,0.02800200000001496,0.034349299999121286,0.029372199999670556,0.03242119999958959,0.031047839999700955 +LinkedList,random,delete,0.017322699999567703,0.0368361000000732,0.04029200000059063,0.03775789999963308,0.03554420000000391,0.033550579999973705 +HashTable,random,insert,0.011551699999472476,0.012756400000398571,0.011765299999751733,0.011679000000185624,0.011983400000644906,0.011947160000090662 +HashTable,random,find,0.00012409999999363208,0.00011009999980160501,0.0001415999995515449,0.00010400000064691994,0.00010089999977935804,0.000116139999954612 +HashTable,random,delete,6.38999999864609e-05,6.779999966965988e-05,6.0600000324484427e-05,6.070000017643906e-05,6.0600000324484427e-05,6.272000009630574e-05 +BST,random,insert,0.014788199999202334,0.014159299999846553,0.013975800000480376,0.014118900000539725,0.013331299999663315,0.01407469999994646 +BST,random,find,0.00013829999988956843,0.00011389999963284936,0.00011369999992894009,0.00011379999978089472,0.00011439999980211724,0.00011881999980687397 +BST,random,delete,8.690000049682567e-05,6.450000000768341e-05,6.2199999774748e-05,6.209999992279336e-05,6.229999962670263e-05,6.759999996575061e-05 +LinkedList,sorted,insert,2.4411346000006233,2.36463619999995,2.2797248999995645,2.2860746000005747,2.2526011999998445,2.3248343000001115 +LinkedList,sorted,find,0.024703000000044995,0.02455259999987902,0.02468479999970441,0.02444869999999355,0.02606350000041857,0.02489052000000811 +LinkedList,sorted,delete,0.012835599999561964,0.027673999999933585,0.027570299999752024,0.02708100000018021,0.02999909999925876,0.02503199999973731 +HashTable,sorted,insert,0.011780100000578386,0.010850699999537028,0.010314100000869075,0.010621500000524975,0.011015500000212342,0.010916380000344362 +HashTable,sorted,find,0.0001464000006308197,0.00017980000029638177,0.00016909999976633117,0.00012620000052265823,0.00023630000032426324,0.0001715600003080908 +HashTable,sorted,delete,0.00016370000048482325,0.00018089999957737746,0.0001443999999537482,7.579999964946182e-05,6.469999971159268e-05,0.0001258999998754007 +BST,sorted,insert,3.5400651999998445,3.5145174999997835,3.5583661999999094,3.5149656000003233,3.481246600000304,3.521832220000033 +BST,sorted,find,0.03275260000009439,0.030442500000390282,0.02994349999971746,0.030269500000031258,0.030329999999594293,0.030747619999965538 +BST,sorted,delete,0.012705400000413647,0.01333390000036161,0.013192000000344706,0.013699000000087835,0.013079800000014075,0.013202020000244374 diff --git a/lab1/docs/report.md b/lab1/docs/report.md new file mode 100644 index 0000000..1e7e3d4 --- /dev/null +++ b/lab1/docs/report.md @@ -0,0 +1,34 @@ +Лабораторная работа 1 + + + +Цель работы + + + +Нужно было сделать три структуры данных и проверить как они работают на телефонном справочнике. + + + +Ход работы + + + +Сделал связный список хеш таблицу и двоичное дерево поиска. Для всех структур сделал добавление поиск удаление и вывод записей. Для проверки создал 10000 записей с именами User\_00000 и т.д. Потом проверил работу со случайным порядком и с отсортированным порядком. Каждый эксперимент повторял 5 раз. + + + +Результаты + + + +Результаты сохранились в results.csv. Также сделал графики для добавления поиска и удаления. По результатам видно что связный список медленно ищет записи потому что нужно идти по элементам. Хеш таблица работает примерно одинаково при разном порядке записей. У двоичного дерева порядок записей влияет намного сильнее. Если добавлять записи по порядку дерево становится похожим на обычный список и работает медленнее. + + + +Вывод + + + +В работе я сделал три структуры данных и проверил их работу. Самой удобной для телефонного справочника получилась хеш таблица. Связный список проще но поиск медленный. Двоичное дерево может работать быстро но сильно зависит от порядка добавления данных. + diff --git a/lab1/experiments.py b/lab1/experiments.py new file mode 100644 index 0000000..fab1645 --- /dev/null +++ b/lab1/experiments.py @@ -0,0 +1,185 @@ +import random +import time +import csv +import os + +from phonebook import * + +N = 10000 +REPEATS = 5 + +def generate_test_data(): + records = [ + (f"User_{i:05d}", f"+7900000{i:04d}") + for i in range(N) + ] + + records_shuffled = records.copy() + random.shuffle(records_shuffled) + + records_sorted = records.copy() + + return records_shuffled, records_sorted + +def measure_experiment(insert_function, find_function, delete_function, records): + insert_times = [] + find_times = [] + delete_times = [] + + for _ in range(REPEATS): + structure = None + + start = time.perf_counter() + + for name, phone in records: + structure = insert_function(structure, name, phone) + + insert_times.append(time.perf_counter() - start) + + structure_for_find = structure + + names = [name for name, phone in records] + search_names = random.sample(names, 100) + [ + "NotFound_001", + "NotFound_002", + "NotFound_003", + "NotFound_004", + "NotFound_005", + "NotFound_006", + "NotFound_007", + "NotFound_008", + "NotFound_009", + "NotFound_010" + ] + + for _ in range(REPEATS): + start = time.perf_counter() + + for name in search_names: + find_function(structure_for_find, name) + + find_times.append(time.perf_counter() - start) + + delete_names = random.sample(names, 50) + + for _ in range(REPEATS): + structure = structure_for_find + + start = time.perf_counter() + + for name in delete_names: + structure = delete_function(structure, name) + + delete_times.append(time.perf_counter() - start) + + return insert_times, find_times, delete_times + +def measure_hash(records): + insert_times = [] + find_times = [] + delete_times = [] + + names = [name for name, phone in records] + search_names = random.sample(names, 100) + [ + f"NotFound_{i:03d}" for i in range(10) + ] + delete_names = random.sample(names, 50) + + for _ in range(REPEATS): + buckets = ht_create() + + start = time.perf_counter() + + for name, phone in records: + ht_insert(buckets, name, phone) + + insert_times.append(time.perf_counter() - start) + + structure_for_find = buckets + + for _ in range(REPEATS): + start = time.perf_counter() + + for name in search_names: + ht_find(structure_for_find, name) + + find_times.append(time.perf_counter() - start) + + for _ in range(REPEATS): + buckets = structure_for_find.copy() + + start = time.perf_counter() + + for name in delete_names: + ht_delete(buckets, name) + + delete_times.append(time.perf_counter() - start) + + return insert_times, find_times, delete_times + +def average(values): + return sum(values) / len(values) + +def run(): + records_shuffled, records_sorted = generate_test_data() + + results = [] + + for order_name, records in [ + ("random", records_shuffled), + ("sorted", records_sorted) + ]: + print("Order:", order_name) + + ll = measure_experiment( + ll_insert, + ll_find, + ll_delete, + records + ) + + results.append(["LinkedList", order_name, "insert", *ll[0]]) + results.append(["LinkedList", order_name, "find", *ll[1]]) + results.append(["LinkedList", order_name, "delete", *ll[2]]) + + ht = measure_hash(records) + + results.append(["HashTable", order_name, "insert", *ht[0]]) + results.append(["HashTable", order_name, "find", *ht[1]]) + results.append(["HashTable", order_name, "delete", *ht[2]]) + + bst = measure_experiment( + bst_insert, + bst_find, + bst_delete, + records + ) + + results.append(["BST", order_name, "insert", *bst[0]]) + results.append(["BST", order_name, "find", *bst[1]]) + results.append(["BST", order_name, "delete", *bst[2]]) + + os.makedirs("docs/data", exist_ok=True) + + with open("docs/data/results.csv", "w", newline="", encoding="utf-8") as file: + writer = csv.writer(file) + + writer.writerow([ + "structure", + "order", + "operation", + "run1", + "run2", + "run3", + "run4", + "run5", + "average" + ]) + + for row in results: + writer.writerow(row + [average(row[3:])]) + + print("Results saved to docs/data/results.csv") + +if __name__ == "__main__": + run() \ No newline at end of file diff --git a/lab1/graphs.py b/lab1/graphs.py new file mode 100644 index 0000000..d659854 --- /dev/null +++ b/lab1/graphs.py @@ -0,0 +1,56 @@ +import csv +import os +import matplotlib.pyplot as plt + +data = [] + +with open("docs/data/results.csv", "r", encoding="utf-8") as file: + reader = csv.DictReader(file) + + for row in reader: + data.append(row) + +def get_average(structure, order, operation): + for row in data: + if ( + row["structure"] == structure + and row["order"] == order + and row["operation"] == operation + ): + return float(row["average"]) + + return 0 + +structures = ["LinkedList", "HashTable", "BST"] +orders = ["random", "sorted"] + +os.makedirs("docs/data", exist_ok=True) + +for operation in ["insert", "find", "delete"]: + random_values = [ + get_average(s, "random", operation) + for s in structures + ] + + sorted_values = [ + get_average(s, "sorted", operation) + for s in structures + ] + + x = range(len(structures)) + + plt.figure() + plt.bar([i - 0.2 for i in x], random_values, width=0.4, label="random") + plt.bar([i + 0.2 for i in x], sorted_values, width=0.4, label="sorted") + + plt.xticks(list(x), structures) + plt.ylabel("Time, seconds") + plt.title(operation.capitalize() + " time") + plt.yscale("log") + plt.legend() + plt.tight_layout() + + plt.savefig("docs/data/graph_" + operation + ".png") + plt.close() + +print("Graphs saved to docs/data/") \ No newline at end of file diff --git a/lab1/phonebook.py b/lab1/phonebook.py new file mode 100644 index 0000000..31163af --- /dev/null +++ b/lab1/phonebook.py @@ -0,0 +1,211 @@ +def ll_insert(head, name, phone): + new_node = { + 'name': name, + 'phone': phone, + 'next': None + } + + if head is None: + return new_node + + current = head + + while current['next'] is not None: + if current['name'] == name: + current['phone'] = phone + return head + current = current['next'] + + if current['name'] == name: + current['phone'] = phone + else: + current['next'] = new_node + + return head + +def ll_find(head, 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): + total = 0 + + for ch in name: + total = (total * 31 + ord(ch)) % table_size + + return total + +def ht_create(size=1000): + return [None] * size + +def ht_insert(buckets, name, phone): + index = hash_function(name, len(buckets)) + buckets[index] = ll_insert(buckets[index], name, phone) + return buckets + +def ht_find(buckets, name): + index = hash_function(name, len(buckets)) + return ll_find(buckets[index], name) + +def ht_delete(buckets, name): + index = hash_function(name, len(buckets)) + buckets[index] = ll_delete(buckets[index], name) + return buckets + +def ht_list_all(buckets): + records = [] + + for bucket in buckets: + current = bucket + + while current is not None: + records.append((current['name'], current['phone'])) + current = current['next'] + + records.sort(key=lambda x: x[0]) + return records + +def bst_insert(root, name, phone): + new_node = { + 'name': name, + 'phone': phone, + 'left': None, + 'right': None + } + + if root is None: + return new_node + + current = root + + while True: + if name < current['name']: + if current['left'] is None: + current['left'] = new_node + break + current = current['left'] + + elif name > current['name']: + if current['right'] is None: + current['right'] = new_node + break + current = current['right'] + + else: + current['phone'] = phone + break + + return root + +def bst_find(root, name): + current = root + + while current is not None: + if name == current['name']: + return current['phone'] + + if name < current['name']: + current = current['left'] + else: + current = current['right'] + + return None + +def bst_delete(root, name): + 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: + child = current['right'] + + elif current['right'] is None: + child = current['left'] + + 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 + + if parent is None: + return child + + if parent['left'] == current: + parent['left'] = child + else: + parent['right'] = child + + return root + +def bst_list_all(root): + records = [] + + def inorder(node): + if node is None: + return + + inorder(node['left']) + records.append((node['name'], node['phone'])) + inorder(node['right']) + + inorder(root) + + return records \ No newline at end of file diff --git a/lab2/docs/data/dead_time.png b/lab2/docs/data/dead_time.png new file mode 100644 index 0000000..d440812 Binary files /dev/null and b/lab2/docs/data/dead_time.png differ diff --git a/lab2/docs/data/empty_time.png b/lab2/docs/data/empty_time.png new file mode 100644 index 0000000..573b627 Binary files /dev/null and b/lab2/docs/data/empty_time.png differ diff --git a/lab2/docs/data/large_time.png b/lab2/docs/data/large_time.png new file mode 100644 index 0000000..6d99e65 Binary files /dev/null and b/lab2/docs/data/large_time.png differ diff --git a/lab2/docs/data/noexit_time.png b/lab2/docs/data/noexit_time.png new file mode 100644 index 0000000..8ac5ef6 Binary files /dev/null and b/lab2/docs/data/noexit_time.png differ diff --git a/lab2/docs/data/results.csv b/lab2/docs/data/results.csv new file mode 100644 index 0000000..dfcc727 --- /dev/null +++ b/lab2/docs/data/results.csv @@ -0,0 +1,16 @@ +maze,strategy,time_ms,visited_cells,path_length +simple.txt,BFS,0.01464000015403144,11.0,6.0 +simple.txt,DFS,0.010180000390391797,9.0,8.0 +simple.txt,A*,0.017740000475896522,9.0,6.0 +dead.txt,BFS,0.3642999996372964,307.0,35.0 +dead.txt,DFS,0.23493999906349927,279.0,151.0 +dead.txt,A*,0.38374000068870373,235.0,35.0 +large.txt,BFS,23.894459999428364,6812.0,2329.0 +large.txt,DFS,84.77875999960816,6796.0,4537.0 +large.txt,A*,28.69542000044021,6791.0,2329.0 +empty.txt,BFS,1.2770400004228577,1176.0,48.0 +empty.txt,DFS,7.602279999264283,2304.0,1176.0 +empty.txt,A*,0.10093999881064519,48.0,48.0 +noexit.txt,BFS,0.003699999797390774,1.0,0.0 +noexit.txt,DFS,0.0032000003557186574,1.0,0.0 +noexit.txt,A*,0.004120000085094944,1.0,0.0 diff --git a/lab2/docs/data/simple_time.png b/lab2/docs/data/simple_time.png new file mode 100644 index 0000000..ecac6ff Binary files /dev/null and b/lab2/docs/data/simple_time.png differ diff --git a/lab2/docs/report.md b/lab2/docs/report.md new file mode 100644 index 0000000..bb0e596 --- /dev/null +++ b/lab2/docs/report.md @@ -0,0 +1,212 @@ +Лабораторная работа 2 + +Поиск выхода из лабиринта + +Цель работы +----------- +Цель работы состоит в реализации программы для поиска выхода из лабиринта с использованием объектно ориентированного подхода и паттернов проектирования + +В программе реализована загрузка лабиринта из файла несколько алгоритмов поиска и сравнение их работы + +Структура программы +------------------- +В программе используются классы Cell для отдельной клетки лабиринта и Maze для самого лабиринта + +Для загрузки используется MazeBuilder и его реализация TextFileMazeBuilder + +Для поиска пути используется общий класс PathFindingStrategy и три алгоритма BFSStrategy DFSStrategy и AStarStrategy + +За хранение результатов отвечает SearchStats а запуск поиска выполняет MazeSolver + +Для вывода информации используются Observer и ConsoleView + +Использованные паттерны +----------------------- +В работе использованы три паттерна Builder Strategy и Observer + +Builder используется для загрузки лабиринта из текстового файла + +TextFileMazeBuilder читает файл и создаёт объект Maze + +В файле символ # обозначает стену пробел обозначает свободную клетку S является началом а E выходом + +Использование Builder позволяет отдельно реализовать загрузку лабиринта и сам класс лабиринта + +Strategy используется для выбора алгоритма поиска + +В программе реализованы BFS DFS и A* + +Все алгоритмы имеют общий интерфейс PathFindingStrategy поэтому в MazeSolver можно менять алгоритм без изменения самого решателя + +Observer используется для вывода информации о поиске + +MazeSolver отправляет события а ConsoleView получает их и выводит информацию в консоль + +Таким образом вывод отделён от основной логики поиска + +Алгоритмы поиска +---------------- +BFS использует очередь и при обычных условиях находит кратчайший путь в лабиринте без весов + +DFS использует стек и может найти путь быстрее но найденный путь не обязательно будет кратчайшим + +A* использует очередь с приоритетом и манхэттенскую эвристику поэтому старается в первую очередь проверять клетки которые находятся ближе к выходу + +Схема классов +------------- +classDiagram + +class Cell { + x + y + is_wall + is_start + is_exit + is_passable() +} + +class Maze { + width + height + cells + start + exit + get_cell() + get_neighbors() +} + +class MazeBuilder { + build_from_file() +} + +class TextFileMazeBuilder { + build_from_file() +} + +class PathFindingStrategy { + find_path() +} + +class BFSStrategy { + find_path() +} + +class DFSStrategy { + find_path() +} + +class AStarStrategy { + find_path() +} + +class SearchStats { + path + time_ms + visited_count + path_length +} + +class MazeSolver { + maze + strategy + set_strategy() + solve() +} + +class Observer { + update() +} + +class ConsoleView { + update() +} + +MazeBuilder <|-- TextFileMazeBuilder +PathFindingStrategy <|-- BFSStrategy +PathFindingStrategy <|-- DFSStrategy +PathFindingStrategy <|-- AStarStrategy +Observer <|-- ConsoleView +MazeSolver --> Maze +MazeSolver --> PathFindingStrategy +MazeSolver --> Observer +Maze --> Cell +Тестирование +------------ +Для проверки использовалось пять разных лабиринтов + +simple.txt представляет простой лабиринт dead.txt содержит тупики large.txt является большим запутанным лабиринтом empty.txt не содержит стен а в noexit.txt выход недостижим + +Каждый алгоритм запускался пять раз + +Во время эксперимента измерялось время поиска количество посещённых клеток и длина найденного пути + +Результаты сохранялись в файл results.csv + +Результаты +---------- +simple.txt +Алгоритм Время мс Посещено Путь +BFS 0.01464 11 6 +DFS 0.01018 9 8 +A* 0.01774 9 6 + +Все алгоритмы работают быстро + +BFS и A* нашли более короткий путь чем DFS + +dead.txt +Алгоритм Время мс Посещено Путь +BFS 0.36430 307 35 +DFS 0.23494 279 151 +A* 0.38374 235 35 + +DFS работал немного быстрее но нашёл более длинный путь + +BFS и A* нашли короткий путь + +large.txt +Алгоритм Время мс Посещено Путь +BFS 23.89446 6812 2329 +DFS 84.77876 6796 4537 +A* 28.69542 6791 2329 + +На большом лабиринте DFS показал худшее время и самый длинный путь + +BFS и A* нашли одинаковый путь + +empty.txt +Алгоритм Время мс Посещено Путь +BFS 1.277 + + +04 1176 48 +DFS 7.60228 2304 1176 +A* 0.10094 48 48 + +В лабиринте без стен лучше всего показал себя A* + +Он посетил меньше всего клеток и работал быстрее + +noexit.txt +Алгоритм Время мс Посещено Путь +BFS 0.00370 1 0 +DFS 0.00320 1 0 +A* 0.00412 1 0 + +В этом лабиринте выход недостижим поэтому все алгоритмы быстро закончили поиск + +Графики +------- +Для сравнения времени работы были построены графики для каждого лабиринта + +Графики находятся в папке docs/data + +simple_time.png dead_time.png large_time.png empty_time.png и noexit_time.png + +Вывод +----- +В работе была создана программа для поиска выхода из лабиринта + +Были реализованы BFS DFS и A* а также использованы паттерны Builder Strategy и Observer + +По результатам эксперимента BFS хорошо подходит для поиска кратчайшего пути DFS может найти путь быстрее но он не всегда получается коротким A* хорошо показывает себя на больших и открытых лабиринтах diff --git a/lab2/experiments.py b/lab2/experiments.py new file mode 100644 index 0000000..f669322 --- /dev/null +++ b/lab2/experiments.py @@ -0,0 +1,93 @@ +import csv +import os + +from maze_solver import ( + TextFileMazeBuilder, + MazeSolver, + BFSStrategy, + DFSStrategy, + AStarStrategy +) + +REPEATS = 5 + +MAZES = [ + "simple.txt", + "dead.txt", + "large.txt", + "empty.txt", + "noexit.txt" +] + +STRATEGIES = [ + ("BFS", BFSStrategy()), + ("DFS", DFSStrategy()), + ("A*", AStarStrategy()) +] + +def average(values): + return sum(values) / len(values) + +def run(): + builder = TextFileMazeBuilder() + results = [] + + for maze_name in MAZES: + filename = os.path.join("lab2", "mazes", maze_name) + + print("Maze:", maze_name) + + for strategy_name, strategy in STRATEGIES: + times = [] + visited = [] + path_lengths = [] + + for _ in range(REPEATS): + maze = builder.build_from_file(filename) + + solver = MazeSolver(maze, strategy) + stats = solver.solve() + + times.append(stats.time_ms) + visited.append(stats.visited_count) + path_lengths.append(stats.path_length) + + results.append([ + maze_name, + strategy_name, + average(times), + average(visited), + average(path_lengths) + ]) + + print( + strategy_name, + "time =", average(times), + "visited =", average(visited), + "path =", average(path_lengths) + ) + + os.makedirs("lab2/docs/data", exist_ok=True) + + with open( + "lab2/docs/data/results.csv", + "w", + newline="", + encoding="utf-8" + ) as file: + writer = csv.writer(file) + + writer.writerow([ + "maze", + "strategy", + "time_ms", + "visited_cells", + "path_length" + ]) + + writer.writerows(results) + + print("Results saved to lab2/docs/data/results.csv") + +if __name__ == "__main__": + run() diff --git a/lab2/graphs.py b/lab2/graphs.py new file mode 100644 index 0000000..b49e2eb --- /dev/null +++ b/lab2/graphs.py @@ -0,0 +1,23 @@ +import csv +import matplotlib.pyplot as plt + +with open("lab2/docs/data/results.csv", encoding="utf-8") as file: + rows = list(csv.DictReader(file)) + +mazes = ["simple.txt", "dead.txt", "large.txt", "empty.txt", "noexit.txt"] +strategies = ["BFS", "DFS", "A*"] + +for maze in mazes: + values = [] + + for strategy in strategies: + for row in rows: + if row["maze"] == maze and row["strategy"] == strategy: + values.append(float(row["time_ms"])) + + plt.bar(strategies, values) + plt.title("Время поиска: " + maze) + plt.xlabel("Стратегия") + plt.ylabel("Время, мс") + plt.savefig("lab2/docs/data/" + maze.replace(".txt", "_time.png")) + plt.close() diff --git a/lab2/make_large.py b/lab2/make_large.py new file mode 100644 index 0000000..715daff --- /dev/null +++ b/lab2/make_large.py @@ -0,0 +1,26 @@ +lines = [] + +for y in range(100): + row = [" "] * 100 + + if y == 0 or y == 99: + row = ["#"] * 100 + else: + row[0] = "#" + row[99] = "#" + + lines.append(row) + +lines[1][1] = "S" +lines[98][98] = "E" + +for x in range(4, 96, 4): + gap = 1 if (x // 4) % 2 == 0 else 98 + + for y in range(1, 99): + if y != gap: + lines[y][x] = "#" + +with open("lab2/mazes/large.txt", "w", encoding="utf-8") as file: + for row in lines: + file.write("".join(row) + "\n") diff --git a/lab2/maze_solver.py b/lab2/maze_solver.py new file mode 100644 index 0000000..f99bc2d --- /dev/null +++ b/lab2/maze_solver.py @@ -0,0 +1,284 @@ +from abc import ABC, abstractmethod +from collections import deque +import heapq +import time + +class Cell: + def __init__(self, x, y): + self.x = x + self.y = y + self.is_wall = False + self.is_start = False + self.is_exit = False + + def is_passable(self): + return not self.is_wall + +class Maze: + def __init__(self, width, height): + self.width = width + self.height = height + self.cells = [] + self.start = None + self.exit = None + + for y in range(height): + row = [] + + for x in range(width): + row.append(Cell(x, y)) + + self.cells.append(row) + + def get_cell(self, x, y): + if 0 <= x < self.width and 0 <= y < self.height: + return self.cells[y][x] + + return None + + def get_neighbors(self, cell): + neighbors = [] + + directions = [ + (0, -1), + (0, 1), + (-1, 0), + (1, 0) + ] + + for dx, dy in directions: + neighbor = self.get_cell( + cell.x + dx, + cell.y + dy + ) + + if neighbor and neighbor.is_passable(): + neighbors.append(neighbor) + + return neighbors + +class MazeBuilder(ABC): + @abstractmethod + def build_from_file(self, filename): + pass + +class TextFileMazeBuilder(MazeBuilder): + def build_from_file(self, filename): + with open(filename, "r", encoding="utf-8") as file: + lines = [line.rstrip("\n") for line in file] + + if not lines: + raise ValueError("Файл лабиринта пустой") + + width = len(lines[0]) + + for line in lines: + if len(line) != width: + raise ValueError("Строки лабиринта имеют разную длину") + + maze = Maze(width, len(lines)) + + for y, line in enumerate(lines): + for x, symbol in enumerate(line): + cell = maze.get_cell(x, y) + + if symbol == "#": + cell.is_wall = True + + elif symbol == "S": + if maze.start is not None: + raise ValueError("В лабиринте несколько стартов") + + maze.start = cell + cell.is_start = True + + elif symbol == "E": + if maze.exit is not None: + raise ValueError("В лабиринте несколько выходов") + + maze.exit = cell + cell.is_exit = True + + elif symbol == " ": + pass + + else: + raise ValueError("Неизвестный символ в лабиринте") + + if maze.start is None: + raise ValueError("В лабиринте нет старта") + + if maze.exit is None: + raise ValueError("В лабиринте нет выхода") + + return maze + +class PathFindingStrategy(ABC): + @abstractmethod + def find_path(self, maze, start, exit): + pass + +class BFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit): + if start is None or exit is None: + return [], 0 + + queue = deque([(start, [start])]) + visited = {start} + + while queue: + current, path = queue.popleft() + + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + queue.append((neighbor, path + [neighbor])) + + return [], len(visited) + +class DFSStrategy(PathFindingStrategy): + def find_path(self, maze, start, exit): + if start is None or exit is None: + return [], 0 + + stack = [(start, [start])] + visited = {start} + + while stack: + current, path = stack.pop() + + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + if neighbor not in visited: + visited.add(neighbor) + stack.append((neighbor, path + [neighbor])) + + return [], len(visited) + +class AStarStrategy(PathFindingStrategy): + def heuristic(self, a, b): + return abs(a.x - b.x) + abs(a.y - b.y) + + def find_path(self, maze, start, exit): + if start is None or exit is None: + return [], 0 + + heap = [] + counter = 0 + + heapq.heappush( + heap, + (self.heuristic(start, exit), counter, start, [start]) + ) + + g_score = {start: 0} + visited = set() + + while heap: + _, _, current, path = heapq.heappop(heap) + + if current in visited: + continue + + visited.add(current) + + if current == exit: + return path, len(visited) + + for neighbor in maze.get_neighbors(current): + new_cost = g_score[current] + 1 + + if neighbor not in g_score or new_cost < g_score[neighbor]: + g_score[neighbor] = new_cost + counter += 1 + + priority = new_cost + self.heuristic( + neighbor, + exit + ) + + heapq.heappush( + heap, + ( + priority, + counter, + neighbor, + path + [neighbor] + ) + ) + + return [], len(visited) + +class SearchStats: + def __init__(self, path, time_ms, visited_count): + self.path = path + self.time_ms = time_ms + self.visited_count = visited_count + self.path_length = len(path) if path else 0 + +class MazeSolver: + def __init__(self, maze, strategy=None): + self.maze = maze + self.strategy = strategy + self.observers = [] + + def attach(self, observer): + self.observers.append(observer) + + def detach(self, observer): + self.observers.remove(observer) + + def notify(self, event, data=None): + for observer in self.observers: + observer.update(event, data) + + def set_strategy(self, strategy): + self.strategy = strategy + + def solve(self): + if self.strategy is None: + raise ValueError("Стратегия не установлена") + + self.notify("search_started") + + start_time = time.perf_counter() + + path, visited_count = self.strategy.find_path( + self.maze, + self.maze.start, + self.maze.exit + ) + + end_time = time.perf_counter() + + time_ms = (end_time - start_time) * 1000 + + self.notify("search_finished", time_ms) + self.notify("path_found", path) + + return SearchStats( + path, + time_ms, + visited_count + ) + +class Observer(ABC): + @abstractmethod + def update(self, event, data=None): + pass + +class ConsoleView(Observer): + def update(self, event, data=None): + if event == "search_started": + print("Поиск начат") + + elif event == "search_finished": + print(f"Поиск завершен за {data:.3f} мс") + + elif event == "path_found": + print(f"Длина пути: {len(data)}") diff --git a/lab2/mazes/dead.txt b/lab2/mazes/dead.txt new file mode 100644 index 0000000..0ed9b48 --- /dev/null +++ b/lab2/mazes/dead.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# ######### # +# # # +# # # +# # # +# # # +# # # +# # # +# # # +# # # +# # +# # +# # +# E# +#################### diff --git a/lab2/mazes/empty.txt b/lab2/mazes/empty.txt new file mode 100644 index 0000000..cf65c33 --- /dev/null +++ b/lab2/mazes/empty.txt @@ -0,0 +1,50 @@ +################################################## +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +#E # +################################################## diff --git a/lab2/mazes/large.txt b/lab2/mazes/large.txt new file mode 100644 index 0000000..79ed92f --- /dev/null +++ b/lab2/mazes/large.txt @@ -0,0 +1,100 @@ +#################################################################################################### +#S # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # # # # # # # # # # # # # # +# # # # # # # # # # # # E# +#################################################################################################### diff --git a/lab2/mazes/noexit.txt b/lab2/mazes/noexit.txt new file mode 100644 index 0000000..0d9e9c5 --- /dev/null +++ b/lab2/mazes/noexit.txt @@ -0,0 +1,10 @@ +########## +#S######## +########## +########## +########## +########## +########## +########## +########E# +########## diff --git a/lab2/mazes/simple.txt b/lab2/mazes/simple.txt new file mode 100644 index 0000000..b7edabb --- /dev/null +++ b/lab2/mazes/simple.txt @@ -0,0 +1,5 @@ +####### +#S # +# ### # +# E # +#######