diff --git a/filippovavm/docs/data/plot_empty.txt.png b/filippovavm/docs/data/plot_empty.txt.png new file mode 100644 index 0000000..a263190 Binary files /dev/null and b/filippovavm/docs/data/plot_empty.txt.png differ diff --git a/filippovavm/docs/data/plot_large.txt.png b/filippovavm/docs/data/plot_large.txt.png new file mode 100644 index 0000000..09b7103 Binary files /dev/null and b/filippovavm/docs/data/plot_large.txt.png differ diff --git a/filippovavm/docs/data/plot_medium.txt.png b/filippovavm/docs/data/plot_medium.txt.png new file mode 100644 index 0000000..6c08710 Binary files /dev/null and b/filippovavm/docs/data/plot_medium.txt.png differ diff --git a/filippovavm/docs/data/plot_no_exit.txt.png b/filippovavm/docs/data/plot_no_exit.txt.png new file mode 100644 index 0000000..37e1f9c Binary files /dev/null and b/filippovavm/docs/data/plot_no_exit.txt.png differ diff --git a/filippovavm/docs/data/plot_tiny.txt.png b/filippovavm/docs/data/plot_tiny.txt.png new file mode 100644 index 0000000..b8b5788 Binary files /dev/null and b/filippovavm/docs/data/plot_tiny.txt.png differ diff --git a/filippovavm/laba1/фулкод1.ipynb b/filippovavm/docs/data/фулкод1.ipynb similarity index 100% rename from filippovavm/laba1/фулкод1.ipynb rename to filippovavm/docs/data/фулкод1.ipynb diff --git a/filippovavm/laba1/МП.ipynb b/filippovavm/docs/laba1/МП.ipynb similarity index 100% rename from filippovavm/laba1/МП.ipynb rename to filippovavm/docs/laba1/МП.ipynb diff --git a/filippovavm/laba1/отчёт1.ipynb b/filippovavm/docs/laba1/отчёт1.ipynb similarity index 100% rename from filippovavm/laba1/отчёт1.ipynb rename to filippovavm/docs/laba1/отчёт1.ipynb diff --git a/filippovavm/docs/laba1/фулкод1.ipynb b/filippovavm/docs/laba1/фулкод1.ipynb new file mode 100644 index 0000000..db48b2b --- /dev/null +++ b/filippovavm/docs/laba1/фулкод1.ipynb @@ -0,0 +1,490 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "id": "65b0def8-04cf-4cfd-b4d4-bc862e120497", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Генерация данных...\n", + "\n", + "Измерение вставки для Связного списка...\n" + ] + } + ], + "source": [ + "import time\n", + "import random\n", + "import csv\n", + "import sys\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "\n", + "# Связный список\n", + "def ll_insert(head, name, phone):\n", + " current = head\n", + " prev = None\n", + " while current is not None:\n", + " if current['name'] == name:\n", + " current['phone'] = phone\n", + " return head\n", + " prev = current\n", + " current = current['next']\n", + " new_node = {'name': name, 'phone': phone, 'next': None}\n", + " if prev is None:\n", + " return new_node\n", + " else:\n", + " prev['next'] = new_node\n", + " return head\n", + "\n", + "def ll_find(head, name):\n", + " current = head\n", + " while current is not None:\n", + " if current['name'] == name:\n", + " return current['phone']\n", + " current = current['next']\n", + " return None\n", + "\n", + "def ll_delete(head, name):\n", + " if head is None:\n", + " return None\n", + " if head['name'] == name:\n", + " return head['next']\n", + " current = head\n", + " while current['next'] is not None:\n", + " if current['next']['name'] == name:\n", + " current['next'] = current['next']['next']\n", + " return head\n", + " current = current['next']\n", + " return head\n", + "\n", + "# Хеш-таблица \n", + "def hash_function(name, size):\n", + " total = 0\n", + " for ch in name:\n", + " total = (total * 31 + ord(ch)) % size\n", + " return total\n", + "\n", + "def ht_create(size=2000):\n", + " return [None] * size\n", + "\n", + "def ht_insert(buckets, name, phone):\n", + " idx = hash_function(name, len(buckets))\n", + " buckets[idx] = ll_insert(buckets[idx], name, phone)\n", + "\n", + "def ht_find(buckets, name):\n", + " idx = hash_function(name, len(buckets))\n", + " return ll_find(buckets[idx], name)\n", + "\n", + "def ht_delete(buckets, name):\n", + " idx = hash_function(name, len(buckets))\n", + " buckets[idx] = ll_delete(buckets[idx], name)\n", + "\n", + "# Двоичное дерево поиска \n", + "def bst_insert(root, name, phone):\n", + " new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n", + " if root is None:\n", + " return new_node\n", + " current = root\n", + " while True:\n", + " if name < current['name']:\n", + " if current['left'] is None:\n", + " current['left'] = new_node\n", + " break\n", + " current = current['left']\n", + " elif name > current['name']:\n", + " if current['right'] is None:\n", + " current['right'] = new_node\n", + " break\n", + " current = current['right']\n", + " else:\n", + " current['phone'] = phone\n", + " break\n", + " return root\n", + "\n", + "def bst_find(root, name):\n", + " current = root\n", + " while current is not None:\n", + " if name < current['name']:\n", + " current = current['left']\n", + " elif name > current['name']:\n", + " current = current['right']\n", + " else:\n", + " return current['phone']\n", + " return None\n", + "\n", + "def bst_find_min(node):\n", + " while node['left'] is not None:\n", + " node = node['left']\n", + " return node\n", + "\n", + "def bst_delete(root, name):\n", + " parent = None\n", + " current = root\n", + " while current is not None and current['name'] != name:\n", + " parent = current\n", + " if name < current['name']:\n", + " current = current['left']\n", + " else:\n", + " current = current['right']\n", + " if current is None:\n", + " return root\n", + " \n", + " if current['left'] is None and current['right'] is None:\n", + " if parent is None:\n", + " return None\n", + " if parent['left'] is current:\n", + " parent['left'] = None\n", + " else:\n", + " parent['right'] = None\n", + " return root\n", + " \n", + " if current['left'] is None:\n", + " if parent is None:\n", + " return current['right']\n", + " if parent['left'] is current:\n", + " parent['left'] = current['right']\n", + " else:\n", + " parent['right'] = current['right']\n", + " return root\n", + " if current['right'] is None:\n", + " if parent is None:\n", + " return current['left']\n", + " if parent['left'] is current:\n", + " parent['left'] = current['left']\n", + " else:\n", + " parent['right'] = current['left']\n", + " return root\n", + " \n", + " succ_parent = current\n", + " succ = current['right']\n", + " while succ['left'] is not None:\n", + " succ_parent = succ\n", + " succ = succ['left']\n", + " current['name'] = succ['name']\n", + " current['phone'] = succ['phone']\n", + " if succ_parent['left'] is succ:\n", + " succ_parent['left'] = succ['right']\n", + " else:\n", + " succ_parent['right'] = succ['right']\n", + " return root\n", + "\n", + "# Генерация данных \n", + "def generate_records(N):\n", + " records = []\n", + " for i in range(N):\n", + " name = f\"User_{i:05d}\"\n", + " phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n", + " records.append((name, phone))\n", + " return records\n", + "\n", + "# Замеры\n", + "REPEATS = 5\n", + "N = 10000\n", + "\n", + "def measure_insert(struct, records, repeats=REPEATS):\n", + " times = []\n", + " for _ in range(repeats):\n", + " if struct == 'll':\n", + " head = None\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " end = time.perf_counter()\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " end = time.perf_counter()\n", + " elif struct == 'bst':\n", + " root = None\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " end = time.perf_counter()\n", + " times.append(end - start)\n", + " return times\n", + "\n", + "def build_structure(struct, records):\n", + " if struct == 'll':\n", + " head = None\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " return head\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " return buckets\n", + " elif struct == 'bst':\n", + " root = None\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " return root\n", + "\n", + "def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n", + " times = []\n", + " N = len(records)\n", + " for _ in range(repeats):\n", + " indices = random.sample(range(N), 100)\n", + " exist = [records[i][0] for i in indices]\n", + " missing = [f\"None_{i}\" for i in range(10)]\n", + " search = exist + missing\n", + " start = time.perf_counter()\n", + " if struct == 'll':\n", + " for name in search:\n", + " ll_find(structure, name)\n", + " elif struct == 'ht':\n", + " for name in search:\n", + " ht_find(structure, name)\n", + " elif struct == 'bst':\n", + " for name in search:\n", + " bst_find(structure, name)\n", + " times.append(time.perf_counter() - start)\n", + " return times\n", + "\n", + "def measure_delete_on_structure(struct, records, repeats=REPEATS):\n", + " times = []\n", + " N = len(records)\n", + " for _ in range(repeats):\n", + " indices = random.sample(range(N), 50)\n", + " del_names = [records[i][0] for i in indices]\n", + " if struct == 'll':\n", + " head = None\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " head = ll_delete(head, name)\n", + " end = time.perf_counter()\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " ht_delete(buckets, name)\n", + " end = time.perf_counter()\n", + " elif struct == 'bst':\n", + " root = None\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " root = bst_delete(root, name)\n", + " end = time.perf_counter()\n", + " times.append(end - start)\n", + " return times\n", + "\n", + "# Основная функция \n", + "def main():\n", + " print(\"Генерация данных...\")\n", + " records = generate_records(N)\n", + " random.shuffle(records) # случайный порядок\n", + " records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n", + "\n", + " results = [] # для CSV\n", + " struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n", + " mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n", + " op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n", + "\n", + " # для графиков\n", + " insert_sh = {} # {struct: [times]}\n", + " insert_so = {}\n", + " find_sh = {}\n", + " find_so = {}\n", + " delete_sh = {}\n", + " delete_so = {}\n", + "\n", + " # Вставка \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n", + " times_sh = measure_insert(struct, records)\n", + " times_so = measure_insert(struct, records_sorted)\n", + " insert_sh[struct] = times_sh\n", + " insert_so[struct] = times_so\n", + " print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n", + " print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n", + "\n", + " # Поиск \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n", + " structure_sh = build_structure(struct, records)\n", + " times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n", + " find_sh[struct] = times_find_sh\n", + " print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n", + "\n", + " print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n", + " structure_so = build_structure(struct, records_sorted)\n", + " times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n", + " find_so[struct] = times_find_so\n", + " print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n", + "\n", + " # Удаление \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n", + " times_del_sh = measure_delete_on_structure(struct, records)\n", + " delete_sh[struct] = times_del_sh\n", + " print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n", + "\n", + " print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n", + " times_del_so = measure_delete_on_structure(struct, records_sorted)\n", + " delete_so[struct] = times_del_so\n", + " print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n", + "\n", + " # Сохраняем CSV\n", + " with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n", + " writer = csv.writer(f)\n", + " writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n", + " writer.writerows(results)\n", + "\n", + " # Построение графиков \n", + " try:\n", + " # График вставки\n", + " fig1, ax1 = plt.subplots(figsize=(10,6))\n", + " x = np.arange(3)\n", + " width = 0.35\n", + " means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n", + " means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax1.set_ylabel('Время (сек)')\n", + " ax1.set_title('Вставка всех записей')\n", + " ax1.set_xticks(x)\n", + " ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax1.legend()\n", + " ax1.set_yscale('log')\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('insert_comparison.png')\n", + " plt.show()\n", + "\n", + " # График поиска\n", + " fig2, ax2 = plt.subplots(figsize=(10,6))\n", + " means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n", + " means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax2.set_ylabel('Время (сек)')\n", + " ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n", + " ax2.set_xticks(x)\n", + " ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax2.legend()\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('find_comparison.png')\n", + " plt.show()\n", + "\n", + " # График удаления \n", + " fig3, ax3 = plt.subplots(figsize=(10,6))\n", + " means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n", + " means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax3.set_ylabel('Время (сек)')\n", + " ax3.set_title('Удаление 50 случайных записей')\n", + " ax3.set_xticks(x)\n", + " ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax3.legend()\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('delete_comparison.png')\n", + " plt.show()\n", + "\n", + " print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n", + " except Exception as e:\n", + " print(f\"Не удалось построить графики: {e}\")\n", + " # Графики замеров\n", + " try:\n", + " def plot_attempts(data_sh, data_so, op_name):\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", + " # случайный порядок\n", + " for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n", + " times = data_sh[struct]\n", + " x = range(1, len(times)+1)\n", + " ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n", + " ax1.scatter(x, times, color=color, s=60, zorder=5)\n", + " ax1.set_xlabel('Номер попытки')\n", + " ax1.set_ylabel('Время (сек)')\n", + " ax1.set_title(f'{op_name} – случайный порядок')\n", + " ax1.legend()\n", + " ax1.grid(True, linestyle=':', alpha=0.7)\n", + " # отсортированный порядок\n", + " for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n", + " times = data_so[struct]\n", + " x = range(1, len(times)+1)\n", + " ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n", + " ax2.scatter(x, times, color=color, s=60, zorder=5)\n", + " ax2.set_xlabel('Номер попытки')\n", + " ax2.set_ylabel('Время (сек)')\n", + " ax2.set_title(f'{op_name} – отсортированный порядок')\n", + " ax2.legend()\n", + " ax2.grid(True, linestyle=':', alpha=0.7)\n", + " plt.tight_layout()\n", + " plt.savefig(f'{op_name}_5attempts.png')\n", + " plt.show()\n", + " \n", + " plot_attempts(insert_sh, insert_so, 'insert')\n", + " plot_attempts(find_sh, find_so, 'find')\n", + " plot_attempts(delete_sh, delete_so, 'delete')\n", + " print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n", + " except Exception as e:\n", + " print(f\"Не удалось построить дополнительные графики: {e}\")\n", + "\n", + "if __name__ == \"__main__\":\n", + " sys.setrecursionlimit(20000)\n", + " main()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cead201d-1150-463f-9ff3-a4bed6f7fc03", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/filippovavm/docs/laba2/all_results.csv b/filippovavm/docs/laba2/all_results.csv new file mode 100644 index 0000000..8378c4c --- /dev/null +++ b/filippovavm/docs/laba2/all_results.csv @@ -0,0 +1,13 @@ +maze,algorithm,avg_time_ms,avg_visited,avg_path_len +tiny.txt,BFSStrategy,0.03461998421698809,6.0,4.0 +tiny.txt,DFSStrategy,0.021119997836649418,5.0,6.0 +tiny.txt,AStarStrategy,0.05429997108876705,6.0,4.0 +medium.txt,BFSStrategy,0.1677999971434474,39.0,16.0 +medium.txt,DFSStrategy,0.18643999937921762,44.0,18.0 +medium.txt,AStarStrategy,0.2677599899470806,39.0,16.0 +large.txt,BFSStrategy,12.41911998949945,2500.0,99.0 +large.txt,DFSStrategy,127.24694001954049,2450.0,2451.0 +large.txt,AStarStrategy,17.408600030466914,2500.0,99.0 +empty.txt,BFSStrategy,0.33364000264555216,100.0,100.0 +empty.txt,DFSStrategy,0.37696000654250383,99.0,100.0 +empty.txt,AStarStrategy,0.4786999896168709,100.0,100.0 diff --git a/filippovavm/docs/laba2/empty.txt b/filippovavm/docs/laba2/empty.txt new file mode 100644 index 0000000..3f96ec2 --- /dev/null +++ b/filippovavm/docs/laba2/empty.txt @@ -0,0 +1,99 @@ +S E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/filippovavm/docs/laba2/large.txt b/filippovavm/docs/laba2/large.txt new file mode 100644 index 0000000..aed9a99 --- /dev/null +++ b/filippovavm/docs/laba2/large.txt @@ -0,0 +1,50 @@ +S E + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/filippovavm/docs/laba2/medium.txt b/filippovavm/docs/laba2/medium.txt new file mode 100644 index 0000000..964a07b --- /dev/null +++ b/filippovavm/docs/laba2/medium.txt @@ -0,0 +1,48 @@ +################################################## +#S # +# ############## ####################### # +# # # # # # +# # ####### # # ################ # # # +# # # # # # # # # # # +# # # ### # # # # ########### # # # # +# # # # # # # # # # # # # # # +# # # # # # # # # # ###### # # # # # +# # # # # # # # # # # # # # # # # +# # # # # # # # # # # ## # # # # # # +# # # # # # # # # # # # # # # # # +# # # # # # # # # # ###### # # # # # +# # # # # # # # # # # # # # # +# # # # # # # # # ########### # # # # +# # # # # # # # # # # # # +# # # # # # # # ################ # # # +# # # # # # # # # # +# # # # # # # ####################### # +# # # # # # # # +# # # # # # ################################### +# # # # # # # +# # # # # ####################################### +# # # # # # +# # # # ######################################### +# # # # # +# # # ########################################### +# # # # +# # ############################################# +# # # +# ################################################ +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +################################################## \ No newline at end of file diff --git a/filippovavm/docs/laba2/no_exit.txt b/filippovavm/docs/laba2/no_exit.txt new file mode 100644 index 0000000..84f1a27 --- /dev/null +++ b/filippovavm/docs/laba2/no_exit.txt @@ -0,0 +1,20 @@ +#################### +#S # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +# # +###################E \ No newline at end of file diff --git a/filippovavm/docs/laba2/plot_empty.txt.png b/filippovavm/docs/laba2/plot_empty.txt.png new file mode 100644 index 0000000..cfaf69a Binary files /dev/null and b/filippovavm/docs/laba2/plot_empty.txt.png differ diff --git a/filippovavm/docs/laba2/plot_large.txt.png b/filippovavm/docs/laba2/plot_large.txt.png new file mode 100644 index 0000000..adf531b Binary files /dev/null and b/filippovavm/docs/laba2/plot_large.txt.png differ diff --git a/filippovavm/docs/laba2/plot_no_exit.txt.png b/filippovavm/docs/laba2/plot_no_exit.txt.png new file mode 100644 index 0000000..f545d59 Binary files /dev/null and b/filippovavm/docs/laba2/plot_no_exit.txt.png differ diff --git a/filippovavm/docs/laba2/plot_tiny.txt.png b/filippovavm/docs/laba2/plot_tiny.txt.png new file mode 100644 index 0000000..5c3136f Binary files /dev/null and b/filippovavm/docs/laba2/plot_tiny.txt.png differ diff --git a/filippovavm/docs/laba2/summary_comparison.png b/filippovavm/docs/laba2/summary_comparison.png new file mode 100644 index 0000000..b9dd2be Binary files /dev/null and b/filippovavm/docs/laba2/summary_comparison.png differ diff --git a/filippovavm/docs/laba2/tiny.txt b/filippovavm/docs/laba2/tiny.txt new file mode 100644 index 0000000..c2d6de4 --- /dev/null +++ b/filippovavm/docs/laba2/tiny.txt @@ -0,0 +1,11 @@ +########## +#S # +# # +# # +# # +# # +# # +# # +# # +# E# +########## \ No newline at end of file diff --git a/filippovavm/docs/laba2/мп2_1.ipynb b/filippovavm/docs/laba2/мп2_1.ipynb new file mode 100644 index 0000000..c17c359 --- /dev/null +++ b/filippovavm/docs/laba2/мп2_1.ipynb @@ -0,0 +1,631 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "332cd3ba-eb85-47e3-85cc-736843c10214", + "metadata": {}, + "source": [ + "# Полная реализация поиска выхода из лабиринта (ООП + паттерны)" + ] + }, + { + "cell_type": "markdown", + "id": "3d027c2d-7827-4b2f-8c52-0632b97fb462", + "metadata": {}, + "source": [ + "## Этап 1. Модель лабиринта (без паттернов)\n", + "\n", + "**Описание:** \n", + "Создаются два класса: `Cell` (клетка) и `Maze` (лабиринт). \n", + "`Cell` хранит координаты `(x, y)`, флаг `is_wall`, флаги `is_start`, `is_exit`, метод `is_passable()`. \n", + "`Maze` содержит двумерный массив клеток, размеры, ссылки на старт и выход. \n", + "Метод `get_neighbors(cell)` возвращает список проходимых соседей (вверх, вниз, влево, вправо). \n", + "\n", + "Этот этап — основа для всех последующих алгоритмов." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c35ca325-3402-4c1b-91d4-32f734d6d599", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import csv\n", + "import heapq\n", + "from collections import deque\n", + "from abc import ABC, abstractmethod\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from dataclasses import dataclass\n", + "\n", + "class Cell:\n", + " def __init__(self, x, y, is_wall=False):\n", + " self.x = x\n", + " self.y = y\n", + " self.is_wall = is_wall\n", + " self.is_start = False\n", + " self.is_exit = False\n", + "\n", + " def is_passable(self):\n", + " return not self.is_wall\n", + "\n", + "\n", + "class Maze:\n", + " def __init__(self, width, height):\n", + " self.width = width\n", + " self.height = height\n", + " self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]\n", + " self.start = None\n", + " self.exit = None\n", + "\n", + " def get_cell(self, x, y):\n", + " if 0 <= x < self.width and 0 <= y < self.height:\n", + " return self.cells[y][x]\n", + " return None\n", + "\n", + " def get_neighbors(self, cell):\n", + " neighbors = []\n", + " for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:\n", + " nx, ny = cell.x + dx, cell.y + dy\n", + " nb = self.get_cell(nx, ny)\n", + " if nb and nb.is_passable():\n", + " neighbors.append(nb)\n", + " return neighbors" + ] + }, + { + "cell_type": "markdown", + "id": "9e10908b-e541-46e4-ad15-99555c9c5de3", + "metadata": {}, + "source": [ + "## Этап 2. Загрузка лабиринта из файла – паттерн Builder\n", + "\n", + "**Описание:** \n", + "Паттерн **Builder** отделяет конструирование сложного объекта (лабиринта) от его представления. \n", + "Интерфейс `MazeBuilder` объявляет метод `build_from_file(filename)`. \n", + "`TextFileMazeBuilder` реализует загрузку из текстового файла, где:\n", + "- `#` – стена\n", + "- пробел (или любой другой символ, кроме `#`, `S`, `E`) – проход\n", + "- `S` – старт\n", + "- `E` – выход\n", + "\n", + "Процесс: чтение строк, определение размеров, создание клеток, установка флагов. \n", + "Builder скрывает детали парсинга и валидации." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "eab1c38a-aef1-4de7-96b3-24df9b6becb7", + "metadata": {}, + "outputs": [], + "source": [ + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename):\n", + " pass\n", + "\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename):\n", + " with open(filename, 'r', encoding='utf-8') as f:\n", + " lines = [line.rstrip('\\n') for line in f.readlines()]\n", + " height = len(lines)\n", + " width = max(len(line) for line in lines)\n", + " maze = Maze(width, height)\n", + "\n", + " for y, line in enumerate(lines):\n", + " for x, ch in enumerate(line):\n", + " cell = maze.get_cell(x, y)\n", + " if ch == '#':\n", + " cell.is_wall = True\n", + " elif ch == 'S':\n", + " cell.is_start = True\n", + " maze.start = cell\n", + " elif ch == 'E':\n", + " cell.is_exit = True\n", + " maze.exit = cell\n", + " else:\n", + " cell.is_wall = False\n", + " return maze" + ] + }, + { + "cell_type": "markdown", + "id": "791f75f0-ea40-496d-ad38-c827e444e6ae", + "metadata": {}, + "source": [ + "## Этап 3. Стратегии поиска пути – паттерн Strategy\n", + "\n", + "**Описание:** \n", + "Паттерн **Strategy** определяет семейство алгоритмов, инкапсулирует каждый и делает их взаимозаменяемыми. \n", + "Интерфейс `PathFindingStrategy` объявляет метод `find_path(maze, start, exit)`, возвращающий `(path, visited_count)`. \n", + "\n", + "Реализованы три стратегии:\n", + "\n", + "1. **BFS (поиск в ширину)** \n", + " - Использует очередь `deque`. \n", + " - Гарантирует нахождение кратчайшего пути по числу шагов. \n", + " - Сложность O(V+E). \n", + " - Подходит для небольших и средних лабиринтов, где важна оптимальность.\n", + "\n", + "2. **DFS (поиск в глубину)** \n", + " - Использует стек (список). \n", + " - Не гарантирует кратчайший путь, но может быть быстрее на определённых конфигурациях. \n", + " - Сложность O(V+E). \n", + " - Полезен, когда нужно быстро найти любой путь.\n", + "\n", + "3. **A\\*** (A-star) \n", + " - Использует приоритетную очередь (heapq) и эвристику. \n", + " - Эвристика – манхэттенское расстояние: \n", + " $[\n", + " h(n) = |x_n - x_{exit}| + |y_n - y_{exit}|\n", + " $] \n", + " - Оценка стоимости пути: \\( f(n) = g(n) + h(n) \\), где \\( g(n) \\) – реальная стоимость от старта. \n", + " - Гарантирует оптимальность при допустимой эвристике. \n", + " - На практике быстрее BFS за счёт целенаправленного поиска." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "fe37e65c-7f33-458f-9ead-37838b60316a", + "metadata": {}, + "outputs": [], + "source": [ + "class PathFindingStrategy(ABC):\n", + " @abstractmethod\n", + " def find_path(self, maze, start, exit):\n", + " pass\n", + "\n", + "\n", + "class BFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit):\n", + " visited = set()\n", + " if start == exit:\n", + " return [start], 1\n", + " queue = deque([start])\n", + " visited.add(start)\n", + " parent = {start: None}\n", + " while queue:\n", + " current = queue.popleft()\n", + " for nb in maze.get_neighbors(current):\n", + " if nb not in visited:\n", + " visited.add(nb)\n", + " parent[nb] = current\n", + " if nb == exit:\n", + " path = []\n", + " node = nb\n", + " while node is not None:\n", + " path.append(node)\n", + " node = parent[node]\n", + " path.reverse()\n", + " return path, len(visited)\n", + " queue.append(nb)\n", + " return [], len(visited)\n", + "\n", + "\n", + "class DFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit):\n", + " visited = set()\n", + " stack = [(start, [start])]\n", + " while stack:\n", + " current, path = stack.pop()\n", + " if current == exit:\n", + " return path, len(visited)\n", + " visited.add(current)\n", + " for nb in maze.get_neighbors(current):\n", + " if nb not in visited:\n", + " stack.append((nb, path + [nb]))\n", + " return [], len(visited)\n", + "\n", + "\n", + "class AStarStrategy(PathFindingStrategy):\n", + " def heuristic(self, cell, exit):\n", + " return abs(cell.x - exit.x) + abs(cell.y - exit.y)\n", + "\n", + " def find_path(self, maze, start, exit):\n", + " open_set = []\n", + " counter = 0\n", + " heapq.heappush(open_set, (0, counter, start))\n", + " counter += 1\n", + " came_from = {}\n", + " g_score = {start: 0}\n", + " f_score = {start: self.heuristic(start, exit)}\n", + " visited = set()\n", + " while open_set:\n", + " _, _, current = heapq.heappop(open_set)\n", + " visited.add(current)\n", + " if current == exit:\n", + " path = []\n", + " node = current\n", + " while node in came_from:\n", + " path.append(node)\n", + " node = came_from[node]\n", + " path.append(start)\n", + " path.reverse()\n", + " return path, len(visited)\n", + " for nb in maze.get_neighbors(current):\n", + " tentative_g = g_score[current] + 1\n", + " if tentative_g < g_score.get(nb, float('inf')):\n", + " came_from[nb] = current\n", + " g_score[nb] = tentative_g\n", + " f = tentative_g + self.heuristic(nb, exit)\n", + " heapq.heappush(open_set, (f, counter, nb))\n", + " counter += 1\n", + " return [], len(visited)" + ] + }, + { + "cell_type": "markdown", + "id": "6eeb7cd4-dade-40a0-b607-c08198b602ea", + "metadata": {}, + "source": [ + "## Этап 4. Класс-оркестратор MazeSolver и статистика\n", + "\n", + "**Описание:** \n", + "`MazeSolver` принимает лабиринт и стратегию. \n", + "Метод `solve()` замеряет время выполнения (`time.perf_counter()`), вызывает стратегию и возвращает статистику `SearchStats`: \n", + "- `time_ms` – время в миллисекундах \n", + "- `visited_cells` – количество посещённых клеток \n", + "- `path_length` – длина пути \n", + "- `algorithm` – имя алгоритма\n", + "\n", + "Класс также поддерживает паттерн **Observer** (будет добавлен в следующем этапе)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "5177c17f-6dd2-42d7-92b0-87471b5c22c1", + "metadata": {}, + "outputs": [], + "source": [ + "@dataclass\n", + "class SearchStats:\n", + " time_ms: float\n", + " visited_cells: int\n", + " path_length: int\n", + " algorithm: str\n", + "\n", + "\n", + "class MazeSolver:\n", + " def __init__(self, maze, strategy, observers=None):\n", + " self.maze = maze\n", + " self.strategy = strategy\n", + " self.observers = observers if observers else []\n", + "\n", + " def attach(self, observer):\n", + " self.observers.append(observer)\n", + "\n", + " def detach(self, observer):\n", + " self.observers.remove(observer)\n", + "\n", + " def notify(self, event_type, data=None):\n", + " for obs in self.observers:\n", + " obs.update(event_type, data)\n", + "\n", + " def set_strategy(self, strategy):\n", + " self.strategy = strategy\n", + "\n", + " def solve(self):\n", + " if self.maze.start is None or self.maze.exit is None:\n", + " raise ValueError(\"Лабиринт не имеет старта или выхода\")\n", + " self.notify(\"search_start\")\n", + " start_time = time.perf_counter()\n", + " path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n", + " end_time = time.perf_counter()\n", + " if path:\n", + " self.notify(\"path_found\", len(path))\n", + " else:\n", + " self.notify(\"no_path\")\n", + " stats = SearchStats(\n", + " time_ms=(end_time - start_time) * 1000,\n", + " visited_cells=visited,\n", + " path_length=len(path),\n", + " algorithm=self.strategy.__class__.__name__\n", + " )\n", + " return path, stats" + ] + }, + { + "cell_type": "markdown", + "id": "87114e56-ac0f-4227-b081-b7f84ebecaa3", + "metadata": {}, + "source": [ + "## Этап 5. Визуализация и пошаговое управление – паттерны Observer и Command\n", + "\n", + "**Описание:** \n", + "- **Observer** (`ConsoleLogger`) подписывается на события `MazeSolver` и выводит сообщения о начале поиска, нахождении пути или его отсутствии. \n", + "- **Command** – интерфейс с методами `execute()` и `undo()`. \n", + " `MoveCommand` реализует перемещение игрока на одну клетку и сохраняет предыдущую позицию для отмены. \n", + " `Player` хранит текущую клетку. \n", + "- Демонстрация: после нахождения пути для `tiny.txt` алгоритм BFS с наблюдателем выводит логи, затем выполняется последовательное перемещение по найденному пути с возможностью отмены последнего шага (undo)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "171e638f-f3ce-4278-902f-37375c33a94b", + "metadata": {}, + "outputs": [], + "source": [ + "class Observer(ABC):\n", + " @abstractmethod\n", + " def update(self, event_type, data=None):\n", + " pass\n", + "\n", + "\n", + "class ConsoleLogger(Observer):\n", + " def update(self, event_type, data=None):\n", + " if event_type == \"search_start\":\n", + " print(f\"[LOG] Поиск пути начат\")\n", + " elif event_type == \"path_found\":\n", + " print(f\"[LOG] Путь найден! Длина: {data}\")\n", + " elif event_type == \"no_path\":\n", + " print(\"[LOG] Путь не найден\")\n", + " elif event_type == \"step\":\n", + " print(f\"[LOG] Шаг: {data}\")\n", + "\n", + "\n", + "class Command(ABC):\n", + " @abstractmethod\n", + " def execute(self):\n", + " pass\n", + "\n", + " @abstractmethod\n", + " def undo(self):\n", + " pass\n", + "\n", + "\n", + "class MoveCommand(Command):\n", + " def __init__(self, player, direction, maze):\n", + " self.player = player\n", + " self.direction = direction\n", + " self.maze = maze\n", + " self.prev_pos = None\n", + "\n", + " def execute(self):\n", + " self.prev_pos = self.player.current_cell\n", + " dx, dy = self.direction\n", + " nx, ny = self.player.current_cell.x + dx, self.player.current_cell.y + dy\n", + " new_cell = self.maze.get_cell(nx, ny)\n", + " if new_cell and new_cell.is_passable():\n", + " self.player.current_cell = new_cell\n", + " return True\n", + " return False\n", + "\n", + " def undo(self):\n", + " if self.prev_pos:\n", + " self.player.current_cell = self.prev_pos\n", + " return True\n", + " return False\n", + "\n", + "\n", + "class Player:\n", + " def __init__(self, start_cell):\n", + " self.current_cell = start_cell\n", + "\n", + "\n", + "def interactive_move_demo(maze, path):\n", + " if not path:\n", + " print(\"Путь не найден, демонстрация движения невозможна.\")\n", + " return\n", + " player = Player(maze.start)\n", + " command_history = []\n", + " print(\"\\n=== Интерактивное движение по найденному пути ===\")\n", + " print(\"Текущая позиция: старт\")\n", + " for step, cell in enumerate(path):\n", + " if cell == maze.start:\n", + " continue\n", + " prev = path[step-1]\n", + " dx = cell.x - prev.x\n", + " dy = cell.y - prev.y\n", + " cmd = MoveCommand(player, (dx, dy), maze)\n", + " cmd.execute()\n", + " command_history.append(cmd)\n", + " print(f\"Шаг {step}: перемещение на ({dx},{dy}), позиция ({player.current_cell.x},{player.current_cell.y})\")\n", + " if cell == maze.exit:\n", + " print(\"Достигнут выход!\")\n", + " break\n", + " if command_history:\n", + " print(\"\\n=== Демонстрация отмены последнего шага (undo) ===\")\n", + " cmd = command_history[-1]\n", + " cmd.undo()\n", + " print(f\"Отменён последний шаг, позиция: ({player.current_cell.x},{player.current_cell.y})\")" + ] + }, + { + "cell_type": "markdown", + "id": "1d67bc06-60f8-4b1d-9018-0b6cb8a8df74", + "metadata": {}, + "source": [ + "## Этап 6. Экспериментальная часть\n", + "\n", + "**Описание:** \n", + "Подготавливаются 5 лабиринтов разной сложности (файлы `tiny.txt`, `medium.txt`, `large.txt`, `empty.txt`, `no_exit.txt`). \n", + "Для каждого лабиринта и каждой стратегии выполняется 5 запусков `solve()`, усредняются: \n", + "- время выполнения (мс) \n", + "- количество посещённых клеток \n", + "- длина найденного пути \n", + "\n", + "Результаты сохраняются в `all_results.csv`. \n", + "Строятся столбчатые диаграммы для каждого лабиринта и общий график сравнения алгоритмов. \n", + "\n", + "Код также демонстрирует паттерны Observer (логирование) и Command (движение) на лабиринте `tiny.txt`." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "996a2948-28ca-4f04-8571-b8ce694fe2a4", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'BFSStrategy' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[1], line 32\u001b[0m\n\u001b[0;32m 24\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;18m__name__\u001b[39m \u001b[38;5;241m==\u001b[39m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m__main__\u001b[39m\u001b[38;5;124m\"\u001b[39m:\n\u001b[0;32m 25\u001b[0m maze_files \u001b[38;5;241m=\u001b[39m [\n\u001b[0;32m 26\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mtiny.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[0;32m 27\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mmedium.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m (...)\u001b[0m\n\u001b[0;32m 30\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mno_exit.txt\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[0;32m 31\u001b[0m ]\n\u001b[1;32m---> 32\u001b[0m strategies \u001b[38;5;241m=\u001b[39m [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n\u001b[0;32m 33\u001b[0m all_results \u001b[38;5;241m=\u001b[39m []\n\u001b[0;32m 34\u001b[0m logger \u001b[38;5;241m=\u001b[39m ConsoleLogger()\n", + "\u001b[1;31mNameError\u001b[0m: name 'BFSStrategy' is not defined" + ] + } + ], + "source": [ + "def test_single_maze(filename, strategies, repeats=5):\n", + " builder = TextFileMazeBuilder()\n", + " maze = builder.build_from_file(filename)\n", + " results = []\n", + " for strategy in strategies:\n", + " solver = MazeSolver(maze, strategy)\n", + " times = []\n", + " visits = []\n", + " lengths = []\n", + " for _ in range(repeats):\n", + " _, stats = solver.solve()\n", + " times.append(stats.time_ms)\n", + " visits.append(stats.visited_cells)\n", + " lengths.append(stats.path_length)\n", + " results.append({\n", + " 'algorithm': strategy.__class__.__name__,\n", + " 'avg_time_ms': sum(times) / repeats,\n", + " 'avg_visited': sum(visits) / repeats,\n", + " 'avg_path_len': sum(lengths) / repeats\n", + " })\n", + " return results\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " maze_files = [\n", + " \"tiny.txt\",\n", + " \"medium.txt\",\n", + " \"large.txt\",\n", + " \"empty.txt\",\n", + " \"no_exit.txt\"\n", + " ]\n", + " strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n", + " all_results = []\n", + " logger = ConsoleLogger()\n", + "\n", + " for maze_file in maze_files:\n", + " print(f\"Загрузка лабиринта из {maze_file}...\")\n", + " try:\n", + " builder = TextFileMazeBuilder()\n", + " maze = builder.build_from_file(maze_file)\n", + " # Демонстрация Observer и Command для tiny.txt\n", + " if maze_file == \"tiny.txt\":\n", + " solver_with_observer = MazeSolver(maze, strategies[0], observers=[logger])\n", + " path, _ = solver_with_observer.solve()\n", + " interactive_move_demo(maze, path)\n", + " results = test_single_maze(maze_file, strategies)\n", + " for r in results:\n", + " r['maze'] = maze_file\n", + " all_results.append(r)\n", + " print(f\"Результаты для {maze_file}:\")\n", + " for r in results:\n", + " print(f\" {r['algorithm']}: время = {r['avg_time_ms']:.3f} мс, \"\n", + " f\"посещено = {r['avg_visited']:.1f}, длина пути = {r['avg_path_len']:.1f}\")\n", + " except Exception as e:\n", + " print(f\"Ошибка при обработке {maze_file}: {e}\")\n", + "\n", + " if all_results:\n", + " with open('all_results.csv', 'w', newline='', encoding='utf-8') as f:\n", + " writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len'])\n", + " writer.writeheader()\n", + " writer.writerows(all_results)\n", + "\n", + " df = pd.DataFrame(all_results)\n", + " for maze in df['maze'].unique():\n", + " subset = df[df['maze'] == maze]\n", + " plt.figure()\n", + " plt.bar(subset['algorithm'], subset['avg_time_ms'])\n", + " plt.title(f'Сравнение алгоритмов на лабиринте {maze}')\n", + " plt.ylabel('Среднее время (мс)')\n", + " plt.savefig(f'plot_{maze}.png')\n", + " plt.close()\n", + "\n", + " plt.figure(figsize=(10, 6))\n", + " for alg in df['algorithm'].unique():\n", + " subset = df[df['algorithm'] == alg]\n", + " plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', label=alg)\n", + " plt.xlabel('Лабиринт')\n", + " plt.ylabel('Среднее время (мс)')\n", + " plt.title('Сравнение эффективности алгоритмов на разных лабиринтах')\n", + " plt.legend()\n", + " plt.grid(True)\n", + " plt.savefig('summary_comparison.png')\n", + " plt.show()\n", + " else:\n", + " print(\"Нет данных для построения графиков. Проверьте файлы лабиринтов.\")\n", + "\n", + " print(\"\\nЭксперимент завершён. Результаты сохранены в all_results.csv и графиках.\")" + ] + }, + { + "cell_type": "markdown", + "id": "937ab5f6-e884-46f6-8d45-b152ca61e7b7", + "metadata": {}, + "source": [ + "## Заключение\n", + "\n", + "В работе реализованы:\n", + "- Классы `Cell` и `Maze` для моделирования лабиринта.\n", + "- Паттерн **Builder** для загрузки лабиринтов из текстовых файлов.\n", + "- Паттерн **Strategy** для трёх алгоритмов поиска: BFS, DFS, A*.\n", + "- Паттерны **Observer** (логирование) и **Command** (управление с отменой) для визуализации и интерактивности.\n", + "- Экспериментальная часть с замером времени, посещённых клеток, длины пути, сохранением результатов в CSV и построением графиков.\n", + "\n", + "Код полностью соответствует заданию и готов к использованию." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1ddcb647-eb50-40aa-bc9a-cb76f8a14f23", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a560e7bf-6b18-4018-9912-ea8da341e8a7", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25793d80-f546-4270-ae7c-86c4898d6c32", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/filippovavm/docs/laba2/отчет2.ipynb b/filippovavm/docs/laba2/отчет2.ipynb new file mode 100644 index 0000000..4ae6777 --- /dev/null +++ b/filippovavm/docs/laba2/отчет2.ipynb @@ -0,0 +1,375 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "204f9862-9a51-47be-bb5b-fcb099f9f707", + "metadata": {}, + "source": [ + "# Отчёт по лабораторной работе: Поиск выхода из лабиринта (ООП + паттерны)\n", + "\n", + "## 1. Описание задачи\n", + "\n", + "Разработать программу для загрузки лабиринта из файла, поиска пути от старта до выхода с возможностью выбора алгоритма и экспериментального сравнения алгоритмов. Требовалось применить минимум 3 паттерна проектирования GoF.\n", + "\n", + "**Исходные данные:** \n", + "- Лабиринты разной сложности (маленький, средний, большой, пустой, без выхода). \n", + "- Формат файла: `#` – стена, ` ` – проход, `S` – старт, `E` – выход. \n", + "- Алгоритмы поиска: BFS, DFS, A* (с манхэттенской эвристикой).\n", + "\n", + "**Цель эксперимента:** сравнить эффективность алгоритмов по времени, количеству посещённых клеток и длине найденного пути.\n", + "\n", + "---\n", + "\n", + "## 2. Выбранные паттерны проектирования\n", + "\n", + "### 2.1. Builder (строитель) – для загрузки лабиринта\n", + "**Проблема:** создание лабиринта из файла требует нескольких шагов (чтение, парсинг, установка флагов). \n", + "**Решение:** интерфейс `MazeBuilder` и конкретная реализация `TextFileMazeBuilder` скрывают детали построения. \n", + "**Преимущество:** легко добавить новый формат (JSON, XML) без изменения остального кода.\n", + "\n", + "### 2.2. Strategy (стратегия) – для алгоритмов поиска\n", + "**Проблема:** алгоритмы поиска (BFS, DFS, A*) взаимозаменяемы, но их реализация отличается. \n", + "**Решение:** интерфейс `PathFindingStrategy` и три класса-стратегии. \n", + "**Преимущество:** можно динамически менять алгоритм во время выполнения (например, через `MazeSolver.setStrategy()`).\n", + "\n", + "### 2.3. Observer (наблюдатель) – для логирования (опционально, но реализован)\n", + "**Проблема:** нужно оповещать внешние компоненты о событиях поиска (начало, найден путь, ошибка). \n", + "**Решение:** интерфейс `Observer` и класс `ConsoleLogger`. \n", + "**Преимущество:** слабая связность – легко добавить другие наблюдатели (GUI, файл лога).\n", + "\n", + "### 2.4. Command (команда) – для пошагового движения (опционально, в демо)\n", + "**Проблема:** требуется поддержка отмены ходов (undo). \n", + "**Решение:** интерфейс `Command`, класс `MoveCommand`, класс `Player`. \n", + "**Преимущество:** инкапсуляция запроса, возможность отмены, ведения истории.\n", + "\n", + "---\n", + "\n", + "## 3. Диаграмма классов (Mermaid)\n", + "\n", + "```mermaid\n", + "classDiagram\n", + " class Cell {\n", + " -int x\n", + " -int y\n", + " -bool isWall\n", + " -bool isStart\n", + " -bool isExit\n", + " +isPassable()\n", + " }\n", + " class Maze {\n", + " -int width\n", + " -int height\n", + " -List[List[Cell]] cells\n", + " -Cell start\n", + " -Cell exit\n", + " +getCell(x,y)\n", + " +getNeighbors(cell)\n", + " }\n", + " class MazeBuilder {\n", + " <>\n", + " +buildFromFile(filename)\n", + " }\n", + " class TextFileMazeBuilder {\n", + " +buildFromFile(filename)\n", + " }\n", + " class PathFindingStrategy {\n", + " <>\n", + " +findPath(maze, start, exit)\n", + " }\n", + " class BFSStrategy\n", + " class DFSStrategy\n", + " class AStarStrategy\n", + " class SearchStats {\n", + " +float time_ms\n", + " +int visited_cells\n", + " +int path_length\n", + " +string algorithm\n", + " }\n", + " class MazeSolver {\n", + " -Maze maze\n", + " -PathFindingStrategy strategy\n", + " +setStrategy(strategy)\n", + " +solve() (path, stats)\n", + " }\n", + " class Observer {\n", + " <>\n", + " +update(event_type, data)\n", + " }\n", + " class ConsoleLogger {\n", + " +update(event_type, data)\n", + " }\n", + " class Command {\n", + " <>\n", + " +execute()\n", + " +undo()\n", + " }\n", + " class MoveCommand {\n", + " -Player player\n", + " -tuple direction\n", + " -Maze maze\n", + " -Cell prev_pos\n", + " +execute()\n", + " +undo()\n", + " }\n", + " class Player {\n", + " -Cell current_cell\n", + " }\n", + "\n", + " MazeBuilder <|.. TextFileMazeBuilder\n", + " PathFindingStrategy <|.. BFSStrategy\n", + " PathFindingStrategy <|.. DFSStrategy\n", + " PathFindingStrategy <|.. AStarStrategy\n", + " MazeSolver --> PathFindingStrategy\n", + " MazeSolver --> Maze\n", + " MazeSolver --> SearchStats\n", + " Observer <|.. ConsoleLogger\n", + " MazeSolver --> Observer\n", + " Command <|.. MoveCommand\n", + " MoveCommand --> Player\n", + " MoveCommand --> Maze" + ] + }, + { + "cell_type": "markdown", + "id": "0e671083-627f-4940-970f-80f8668388fb", + "metadata": {}, + "source": [ + "## 4.1 Builder (загрузка лабиринта)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "bb983238-274f-4f19-b784-73be954a3aae", + "metadata": {}, + "outputs": [ + { + "ename": "NameError", + "evalue": "name 'ABC' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[1;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[1;32mIn[1], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21;01mMazeBuilder\u001b[39;00m(ABC):\n\u001b[0;32m 2\u001b[0m \u001b[38;5;129m@abstractmethod\u001b[39m\n\u001b[0;32m 3\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;21mbuild_from_file\u001b[39m(\u001b[38;5;28mself\u001b[39m, filename):\n\u001b[0;32m 4\u001b[0m \u001b[38;5;28;01mpass\u001b[39;00m\n", + "\u001b[1;31mNameError\u001b[0m: name 'ABC' is not defined" + ] + } + ], + "source": [ + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename):\n", + " pass\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename):\n", + " with open(filename, 'r') as f:\n", + " lines = [line.rstrip('\\n') for line in f]\n", + " height = len(lines)\n", + " width = max(len(line) for line in lines)\n", + " maze = Maze(width, height)\n", + " for y, line in enumerate(lines):\n", + " for x, ch in enumerate(line):\n", + " cell = maze.get_cell(x, y)\n", + " if ch == '#': cell.is_wall = True\n", + " elif ch == 'S': cell.is_start = True; maze.start = cell\n", + " elif ch == 'E': cell.is_exit = True; maze.exit = cell\n", + " else: cell.is_wall = False\n", + " return maze" + ] + }, + { + "cell_type": "markdown", + "id": "f20c327f-b8f7-40f4-a71f-eede64d5dedf", + "metadata": {}, + "source": [ + "## 4.2. Стратегии поиска (BFS, DFS, A*)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b5fb0715-9749-404d-870d-0a83c0025eac", + "metadata": {}, + "outputs": [], + "source": [ + "class BFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit):\n", + " queue = deque([start])\n", + " visited = {start}\n", + " parent = {start: None}\n", + " while queue:\n", + " cur = queue.popleft()\n", + " if cur == exit:\n", + " path = []\n", + " while cur:\n", + " path.append(cur)\n", + " cur = parent[cur]\n", + " return path[::-1], len(visited)\n", + " for nb in maze.get_neighbors(cur):\n", + " if nb not in visited:\n", + " visited.add(nb)\n", + " parent[nb] = cur\n", + " queue.append(nb)\n", + " return [], len(visited)" + ] + }, + { + "cell_type": "markdown", + "id": "bae1222c-07f6-487d-ac05-7d09d7086e67", + "metadata": {}, + "source": [ + "## 4.3. Оркестратор MazeSolver и статистика" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "247f19cf-4811-4dd8-8e35-03af6550d202", + "metadata": {}, + "outputs": [], + "source": [ + "class MazeSolver:\n", + " def solve(self):\n", + " start_time = time.perf_counter()\n", + " path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n", + " end_time = time.perf_counter()\n", + " stats = SearchStats(\n", + " time_ms=(end_time - start_time)*1000,\n", + " visited_cells=visited,\n", + " path_length=len(path),\n", + " algorithm=self.strategy.__class__.__name__\n", + " )\n", + " return path, stats" + ] + }, + { + "cell_type": "markdown", + "id": "8a7167d5-efd8-4b3c-a14e-5b6c1cba83ef", + "metadata": {}, + "source": [ + "## 5. Результаты экспериментов\n", + "\n", + "**Тестовые лабиринты:**\n", + "- `tiny.txt` – 10×10, простой путь\n", + "- `medium.txt` – 50×50, с тупиками\n", + "- `large.txt` – 100×100, запутанный\n", + "- `empty.txt` – 100×100, без стен (старт в левом верхнем углу, выход в правом нижнем)\n", + "- `no_exit.txt` – лабиринт без выхода\n", + "\n", + "> **Примечание:** средний лабиринт (`medium.txt`) не был включён в замеры из-за отсутствия корректного файла. Остальные четыре лабиринта соответствуют заданию.\n", + "\n", + "**Методика:** каждый алгоритм запущен 5 раз на каждом лабиринте, значения усреднены. Данные получены из `all_results.csv`.\n", + "\n", + "### 5.1. Таблица результатов\n", + "\n", + "| Лабиринт | Алгоритм | Время (мс) | Посещено клеток | Длина пути |\n", + "|----------------|----------------|------------|-----------------|------------|\n", + "| tiny.txt | BFSStrategy | 0.2854 | 72.0 | 16.0 |\n", + "| tiny.txt | DFSStrategy | 0.2665 | 71.0 | 72.0 |\n", + "| tiny.txt | AStarStrategy | 0.3941 | 72.0 | 16.0 |\n", + "| large.txt | BFSStrategy | 4.9520 | 1275.0 | 50.0 |\n", + "| large.txt | DFSStrategy | 0.2159 | 49.0 | 50.0 |\n", + "| large.txt | AStarStrategy | 0.3549 | 50.0 | 50.0 |\n", + "| empty.txt | BFSStrategy | 24.3337 | 5049.0 | 100.0 |\n", + "| empty.txt | DFSStrategy | 0.5570 | 99.0 | 100.0 |\n", + "| empty.txt | AStarStrategy | 0.7525 | 100.0 | 100.0 |\n", + "| no_exit.txt | BFSStrategy | 1.2649 | 324.0 | 0.0 |\n", + "| no_exit.txt | DFSStrategy | 3.2304 | 324.0 | 0.0 |\n", + "| no_exit.txt | AStarStrategy | 2.2239 | 324.0 | 0.0 |\n", + "\n", + "### 5.2. Графики\n", + "\n", + "Графики для каждого по отдельности сохранены в файле, тут предоставляю общее сравнение\n", + "![Сводный график](summary_comparison.png)\n", + "\n", + "---\n", + "\n", + "## 6. Анализ эффективности алгоритмов\n", + "\n", + "### 6.1. Время выполнения\n", + "- На **tiny.txt** все алгоритмы показали близкое время (~0.2–0.4 мс); DFS незначительно быстрее.\n", + "- На **large.txt** BFS значительно медленнее (4.95 мс) из-за равномерного обхода, а DFS и A* работают быстро (~0.2–0.35 мс).\n", + "- На **empty.txt** BFS крайне медленен (24.3 мс), поскольку вынужден обойти почти всё поле; DFS и A* справляются за ~0.5–0.75 мс.\n", + "- В лабиринте **no_exit.txt** BFS быстрее (1.26 мс), а DFS и A* медленнее (2.2–3.2 мс) из-за разных порядков обхода.\n", + "\n", + "### 6.2. Количество посещённых клеток\n", + "- **BFS** на `empty.txt` посещает 5049 клеток (почти половину поля), тогда как DFS и A* – всего ~100 клеток.\n", + "- В `large.txt` BFS посещает 1275 клеток, DFS – 49, A* – 50. Это показывает, что A* (как и DFS) находит путь, исследуя лишь узкую область.\n", + "- В `tiny.txt` все алгоритмы посещают около 70 клеток (различия незначительны).\n", + "\n", + "### 6.3. Длина найденного пути\n", + "- **BFS** и **A*** находят кратчайший путь (16 шагов в tiny.txt, 50 в large.txt, 100 в empty.txt).\n", + "- **DFS** в tiny.txt даёт очень длинный путь (72 шага вместо 16), в large.txt – 50 (совпал с оптимальным), в empty.txt – 100 (оптимально).\n", + "- Таким образом, DFS не гарантирует кратчайший путь, хотя в некоторых случаях может его найти.\n", + "\n", + "### 6.4. Лабиринт без выхода\n", + "- Все алгоритмы исследуют всю достижимую область (324 клетки). \n", + "- Время различается: BFS – 1.26 мс, DFS – 3.23 мс, A* – 2.22 мс. Это связано с тем, что DFS «закапывается» в глубину, а A* тратит время на поддержание очереди с приоритетами.\n", + "\n", + "---\n", + "\n", + "## 7. Применимость паттернов и гибкость архитектуры\n", + "\n", + "### 7.1. Паттерн Builder\n", + "- **Без него:** код загрузки был бы прямо в `main` или в конструкторе `Maze`. Пришлось бы переписывать при добавлении нового формата.\n", + "- **С ним:** легко добавить `JSONMazeBuilder`, заменив всего одну строку в клиентском коде.\n", + "\n", + "### 7.2. Паттерн Strategy\n", + "- **Без него:** пришлось бы использовать громоздкие `if-elif` для выбора алгоритма, дублировать код замера времени.\n", + "- **С ним:** алгоритмы полностью независимы, можно динамически менять стратегию (например, на основе размера лабиринта).\n", + "\n", + "### 7.3. Паттерн Observer (опционально)\n", + "- **Без него:** логирование и визуализация были бы вплетены в алгоритмы поиска.\n", + "- **С ним:** наблюдатели подписываются на события, и логирование можно отключить или заменить на другой вывод без изменения `MazeSolver`.\n", + "\n", + "### 7.4. Паттерн Command (для пошагового движения)\n", + "- **Без него:** отмена хода пришлось бы реализовывать вручную, что привело бы к дублированию кода.\n", + "- **С ним:** команды легко складывать в историю, реализовать `undo` и `redo`.\n", + "\n", + "---\n", + "\n", + "## 8. Выводы\n", + "\n", + "- **ООП и паттерны проектирования** позволили создать гибкую, расширяемую и легко тестируемую программу.\n", + "- **Builder** упростил добавление новых форматов лабиринтов.\n", + "- **Strategy** сделал алгоритмы поиска взаимозаменяемыми и позволил проводить честное сравнение.\n", + "- **Observer** и **Command** добавили возможности логирования и отмены действий без «засорения» основной логики.\n", + "- Экспериментально подтверждены теоретические свойства алгоритмов: A* – лучший выбор для большинства случаев (оптимальный путь и высокая скорость), BFS – оптимален по длине пути, но медленен на больших картах, DFS – прост, но не даёт гарантий кратчайшего пути.\n", + "\n", + "**Итог:** использование паттернов повысило качество кода, уменьшило связанность и облегчило поддержку. Без них любое изменение (добавление нового алгоритма или формата файла) потребовало бы правки многих классов.\n", + "\n", + "--- \n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5e634d95-e0c0-4b8a-a330-02463cd085c8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/filippovavm/docs/laba2/фулкод2.ipynb b/filippovavm/docs/laba2/фулкод2.ipynb new file mode 100644 index 0000000..6c05cb2 --- /dev/null +++ b/filippovavm/docs/laba2/фулкод2.ipynb @@ -0,0 +1,352 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "9c9e480a-4ce9-4cfb-b0e0-dfbdc1a2ecc0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "tiny.txt:\n", + " BFSStrategy: 0.285 мс, посещено 72.0, путь 16.0\n", + " DFSStrategy: 0.267 мс, посещено 71.0, путь 72.0\n", + " AStarStrategy: 0.394 мс, посещено 72.0, путь 16.0\n", + "Ошибка при обработке medium.txt: Лабиринт не имеет старта или выхода\n", + "\n", + "large.txt:\n", + " BFSStrategy: 4.952 мс, посещено 1275.0, путь 50.0\n", + " DFSStrategy: 0.216 мс, посещено 49.0, путь 50.0\n", + " AStarStrategy: 0.355 мс, посещено 50.0, путь 50.0\n", + "\n", + "empty.txt:\n", + " BFSStrategy: 24.334 мс, посещено 5049.0, путь 100.0\n", + " DFSStrategy: 0.557 мс, посещено 99.0, путь 100.0\n", + " AStarStrategy: 0.752 мс, посещено 100.0, путь 100.0\n", + "\n", + "no_exit.txt:\n", + " BFSStrategy: 1.265 мс, посещено 324.0, путь 0.0\n", + " DFSStrategy: 3.230 мс, посещено 324.0, путь 0.0\n", + " AStarStrategy: 2.224 мс, посещено 324.0, путь 0.0\n" + ] + }, + { + "data": { + "image/png": "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V4el2tpa7N+/H0uWLHFZPnv2bPz4448e77Ny5UosX77cbfmWLVvg5ydHQ1cFIof8L8acXwe/uouOpdXqUBwedDtM2Qoge6MMdRGRs8j+N2FC2etQAC5vbIj6/+8LuxGmzV/LVB0RtWTr1q1yl0BEbeANx2plZfv68kjCbR5d72E0GjFw4ED88MMPmDy5sWfCiy++iPfffx/Hjh1zu4+nkaWoqCgUFBQgKCioW+r2yGaFNed7HP5pG8ZMmgnlEJ5fTeRtpKMboNzyFKSyxhm2RNBAWGf9CSLuWhkrIyJP6urqsHXrVsyaNQtqddvadRBR9/OmY7W0tBT9+/dHSUlJm7KBV48sNZCaXHsihHBb1kCr1UKr1botV6vVMv9w1MCwabhwrAKJw6bJ/otCRB6MvQEYneo2w5aKb2wQeTX5n+OJqC284Vht7+N79bRM/fv3h1KpRG5ursvyvLw86HQ6maoiol6tYYat0EkQnGGLiIioT/PqsKTRaHD55Ze7nd+4detWl9PyiIiIiIiIOpvXn4a3cOFC/Pa3v8X48eMxadIkvP322zh79iweeOABuUsjIiIiIqJezOvD0q233orCwkI8//zzMJlMGDNmDDZu3Ijo6Gi5SyMiIiIiol7M68MSADz00EN46KGH5C6DiIiIiIj6EK++ZomIiIiIiEguDEtEREREREQeMCwRERERERF5wLBERERERETkAcMSERERERGRBwxLREREREREHjAsERERERERecCwRERERERE5AHDEhERERERkQcquQvoakIIAEBpaanMlQB1dXWorKxEaWkp1Gq13OUQUTN4rBL1DDxWiXoGbzpWGzJBQ0ZoTa8PS2VlZQCAqKgomSshIiIiIiJvUFZWhuDg4Fa3k0RbY1UPZbPZYDQaERgYCEmSZK2ltLQUUVFROHfuHIKCgmSthYiax2OVqGfgsUrUM3jTsSqEQFlZGfR6PRSK1q9I6vUjSwqFAoMGDZK7DBdBQUGy/6IQUet4rBL1DDxWiXoGbzlW2zKi1IATPBAREREREXnAsEREREREROQBw1I30mq1WLp0KbRardylEFELeKwS9Qw8Vol6hp58rPb6CR6IiIiIiIg6giNLREREREREHjAsERERERERecCwRERERERE5AHDUhvt2LEDkiShuLhY7lKI+qTp06fjkUcekbsMIiIi6gSSJOHL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w6OPPor3338fp06dwi+//IK//vWveP/99y9p3+fOncPChQtx7NgxfPzxx3j99dfx8MMPO9bHxMRg586duHDhAgoKChzLQ0JCcOONN+Lxxx/H7NmzMWjQIADAjBkz8MYbb7jcv+lz/fnz5/Hggw9i9erVmDJlCv75z39i5cqVLsHN0zFP1JPMnDkTCQkJuP3223HgwAHs3bsXd955J6ZNm+ZyCntznnvuOXzwwQdYtmwZMjMzkZWVhU8//RTPPPMMALTpOHIWHh4OX19fbN68GWazGSUlJQCAL774AnFxcY7tmnvufPrpp1FSUoK//OUveOKJJzBq1Cjcc889jvvFxMQgJycHGRkZKCgoQE1NTdu/WR260qkPOHbsmJg4caLw9fVtcepwZ1988YXw9C199tlnhVKpdEwr3nARm/N0xGlpaSI2NlaoVCoRHR3tmAbR+eK5uro6kZycLG655RbHsueff15EREQISZI49TD1as4XYn/00UciJiZGaLVaMWnSJJGWluZyYWhzF4B6mjo8OTlZLFmyxLFNbW2teO6550RMTIxQq9UiIiJC3HDDDeLgwYNCCB6/RJ7YbDbx2muviZEjRwq1Wi0GDBgg5syZI7777juPx6On59CmF2BPmzZNPPTQQ+KBBx4QQUFBIiQkRCxZssRlwoeffvpJJCQkCK1W6/b8+8033wgAYv369Y5l0dHRLlMnN32uz87Odpv2XwghHn30UTFs2DDHJDJNj3kib+RpApPrrrvO8XxzqVOHb968WUyePFn4+vqKoKAgkZycLN5++22P7TOEcD+O0GSilH/84x8iKipKKBQKx6x8Da+/nTV97ty+fbtQqVRi165djm3OnDkjgoODxZtvvimEEKK6ulrcdNNNol+/fu2eOlyqL5a60H333Qez2Yy0tDS5SyEiJxUVFRg4cCBeeeUVl3egiEh+06dPR1JSEtauXduh+69btw4PP/wwjEaj49QdIqL24tWKXaikpAT79u3DunXr8N///lfucoj6vF9++QVHjx5FcnIySkpK8PzzzwMArrvuOpkrI6LOUllZiZycHKxcuRL3338/gxIRXRJes9SFrrvuOqSmpuL+++/HrFmz5C6HiGCfWjQxMREzZ85ERUUFdu3ahf79+8tdFhF1kpdeeglJSUnQ6XR48skn5S6HqEcaPXq0y7Tgzh/r1q2Tu7xuxdPwiIiIiIjI4cyZM6irq/O4TqfTITAwsJsrkg/DEhERERERkQc8DY+IiIiIiMgDhiUiIiIiIiIPGJaIiIiIiIg8YFgiIqIey2KxyF0CERH1YgxLRETUY2RnZ+PBBx9EfHw8wsLC4OPjg6NHj8pdFhER9VIMS0RE1C0kSfL40VZZWVm4/PLLYbFY8O6772LPnj04deoU4uLiurBqIiLqyzh1OBERdQtJkvDee+9h7ty5AIDNmzfj7rvvRlufhmbMmIFJkyZhxYoVXVkmERGRA0eWiIioyzVcWxQaGoqIiAhERESgX79+jvWFhYVYsGABBg0aBD8/P4wdOxYff/yxY31FRQW2b9+O2tpaDB8+HD4+Phg7diz++9//OrY5ffo0JElCRkaGY9kzzzwDSZKwdu1axzJJkvDWW29h3rx58PX1xZAhQ/DZZ591aD9ffvmly9c5ffp0PPLII47PmxtNW7ZsWbu/h0RE1P0YloiIqMvV1tYCADQajcf11dXVuPzyy7FhwwYcPnwYv//97/Hb3/4We/bsAWAPU0II/O1vf8Py5ctx8OBB3HTTTbjxxhtdQo2z8+fP47XXXoOvr6/bumeffRY33XQTDAYD7rjjDixYsABZWVnt3k9LPv/8c5hMJphMJkyaNAmLFi1y3H7sscfatS8iIpIHwxIREXW5oqIiAEBAQIDH9QMHDsRjjz2GpKQkDB06FH/84x8xZ84cx4iPzWYDADzxxBO47bbbMGLECCxbtgxXX301Xn75ZY/7fPrpp3HrrbciPDzcbd3NN9+Me++9FyNGjMALL7yA8ePH4/XXX2/3flriPIqm0WgQEBDguN3c94GIiLyLSu4CiIio98vNzQWAZgOH1WrFqlWr8Omnn+LChQuoqalBTU0N/P39XbabOnWqy+0pU6YgLS3NbX8HDhzAF198gWPHjmHbtm1u6ydNmuR229MIVWv7WbBgAZRKpeN2VVUVkpKSPH6NRETU8zAsERFRl8vKyoJarcaQIUM8rn/llVfw6quvYu3atRg7diz8/f3xyCOPOE7fCw0NBQCPs+d5WrZo0SI89thjiIyMbHONHdnPq6++ipkzZzpu33777W1+PCIi8n4MS0RE1OU2btyIiRMnQq1We1y/a9cuXHfddbjjjjsA2E+7O3HiBEaNGgUACAoKQkREBL7//ntcddVVjvt9//33iI+Pd9lXWloajh8/jq+++qrZenbv3o0777zT5fZll13W7v1EREQgNjbWcbu91zUREZF3Y1giIqIuYzQasXbtWqxfv97j6XINYmNj8Z///Ac//vgjQkJCsGbNGuTm5jrCEgA8+uij+NOf/oShQ4di3Lhx+Oijj7B9+3bs37/fZV8vvfQSXn/9dfj5+TX7eJ999hnGjx+PKVOmYN26ddi7dy/eeeeddu+HiIh6N4YlIiLqMh999BH27duHTZs2YdasWc1u9+yzzyInJwdz5syBn58ffv/73+P6669HSUmJY5tFixahrKwMixYtQn5+PuLi4vD555+7XSMUGxuLu+66q8W6li9fjk8++QQPPfQQIiIisG7dOrcRqrbsh4iIejc2pSUioj5FkiR88cUXuP766+UuhYiIvBynDiciIiIiIvKAYYmIiIiIiMgDXrNERER9Cs8+JyKituLIEhERERERkQcMS0RERERERB4wLBEREREREXnAsEREREREROQBwxIREREREZEHDEtEREREREQeMCwRERERERF5wLBERERERETkwf8HY4Qap/tAmbEAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import time\n", + "import csv\n", + "import heapq\n", + "from collections import deque\n", + "from abc import ABC, abstractmethod\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from dataclasses import dataclass\n", + "\n", + "# Этап 1: Модель лабиринта \n", + "class Cell:\n", + " def __init__(self, x, y, is_wall=False):\n", + " self.x = x\n", + " self.y = y\n", + " self.is_wall = is_wall\n", + " self.is_start = False\n", + " self.is_exit = False\n", + "\n", + " def is_passable(self):\n", + " return not self.is_wall\n", + "\n", + "\n", + "class Maze:\n", + " def __init__(self, width, height):\n", + " self.width = width\n", + " self.height = height\n", + " self.cells = [[Cell(x, y) for x in range(width)] for y in range(height)]\n", + " self.start = None\n", + " self.exit = None\n", + "\n", + " def get_cell(self, x, y):\n", + " if 0 <= x < self.width and 0 <= y < self.height:\n", + " return self.cells[y][x]\n", + " return None\n", + "\n", + " def get_neighbors(self, cell):\n", + " neighbors = []\n", + " for dx, dy in [(0, 1), (0, -1), (1, 0), (-1, 0)]:\n", + " nx, ny = cell.x + dx, cell.y + dy\n", + " nb = self.get_cell(nx, ny)\n", + " if nb and nb.is_passable():\n", + " neighbors.append(nb)\n", + " return neighbors\n", + "\n", + "\n", + "# Этап 2: Загрузка из файла (Builder) \n", + "class MazeBuilder(ABC):\n", + " @abstractmethod\n", + " def build_from_file(self, filename):\n", + " pass\n", + "\n", + "\n", + "class TextFileMazeBuilder(MazeBuilder):\n", + " def build_from_file(self, filename):\n", + " with open(filename, 'r', encoding='utf-8') as f:\n", + " lines = [line.rstrip('\\n') for line in f.readlines()]\n", + " height = len(lines)\n", + " width = max(len(line) for line in lines)\n", + " maze = Maze(width, height)\n", + "\n", + " for y, line in enumerate(lines):\n", + " for x, ch in enumerate(line):\n", + " cell = maze.get_cell(x, y)\n", + " if ch == '#':\n", + " cell.is_wall = True\n", + " elif ch == 'S':\n", + " cell.is_start = True\n", + " maze.start = cell\n", + " elif ch == 'E':\n", + " cell.is_exit = True\n", + " maze.exit = cell\n", + " else:\n", + " cell.is_wall = False\n", + " return maze\n", + "\n", + "\n", + "# Этап 3: Стратегии поиска (Strategy) \n", + "class PathFindingStrategy(ABC):\n", + " @abstractmethod\n", + " def find_path(self, maze, start, exit):\n", + " pass\n", + "\n", + "\n", + "class BFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit):\n", + " visited = set()\n", + " if start == exit:\n", + " return [start], 1\n", + " queue = deque([start])\n", + " visited.add(start)\n", + " parent = {start: None}\n", + " while queue:\n", + " current = queue.popleft()\n", + " for nb in maze.get_neighbors(current):\n", + " if nb not in visited:\n", + " visited.add(nb)\n", + " parent[nb] = current\n", + " if nb == exit:\n", + " path = []\n", + " node = nb\n", + " while node is not None:\n", + " path.append(node)\n", + " node = parent[node]\n", + " path.reverse()\n", + " return path, len(visited)\n", + " queue.append(nb)\n", + " return [], len(visited)\n", + "\n", + "\n", + "class DFSStrategy(PathFindingStrategy):\n", + " def find_path(self, maze, start, exit):\n", + " visited = set()\n", + " stack = [(start, [start])]\n", + " while stack:\n", + " current, path = stack.pop()\n", + " if current == exit:\n", + " return path, len(visited)\n", + " visited.add(current)\n", + " for nb in maze.get_neighbors(current):\n", + " if nb not in visited:\n", + " stack.append((nb, path + [nb]))\n", + " return [], len(visited)\n", + "\n", + "\n", + "class AStarStrategy(PathFindingStrategy):\n", + " def heuristic(self, cell, exit):\n", + " return abs(cell.x - exit.x) + abs(cell.y - exit.y)\n", + "\n", + " def find_path(self, maze, start, exit):\n", + " open_set = []\n", + " counter = 0\n", + " heapq.heappush(open_set, (0, counter, start))\n", + " counter += 1\n", + " came_from = {}\n", + " g_score = {start: 0}\n", + " f_score = {start: self.heuristic(start, exit)}\n", + " visited = set()\n", + " while open_set:\n", + " _, _, current = heapq.heappop(open_set)\n", + " visited.add(current)\n", + " if current == exit:\n", + " path = []\n", + " node = current\n", + " while node in came_from:\n", + " path.append(node)\n", + " node = came_from[node]\n", + " path.append(start)\n", + " path.reverse()\n", + " return path, len(visited)\n", + " for nb in maze.get_neighbors(current):\n", + " tentative_g = g_score[current] + 1\n", + " if tentative_g < g_score.get(nb, float('inf')):\n", + " came_from[nb] = current\n", + " g_score[nb] = tentative_g\n", + " f = tentative_g + self.heuristic(nb, exit)\n", + " heapq.heappush(open_set, (f, counter, nb))\n", + " counter += 1\n", + " return [], len(visited)\n", + "\n", + "\n", + "# Этап 4: Оркестратор и статистика \n", + "@dataclass\n", + "class SearchStats:\n", + " time_ms: float\n", + " visited_cells: int\n", + " path_length: int\n", + " algorithm: str\n", + "\n", + "\n", + "class MazeSolver:\n", + " def __init__(self, maze, strategy):\n", + " self.maze = maze\n", + " self.strategy = strategy\n", + "\n", + " def set_strategy(self, strategy):\n", + " self.strategy = strategy\n", + "\n", + " def solve(self):\n", + " if self.maze.start is None or self.maze.exit is None:\n", + " raise ValueError(\"Лабиринт не имеет старта или выхода\")\n", + " start_time = time.perf_counter()\n", + " path, visited = self.strategy.find_path(self.maze, self.maze.start, self.maze.exit)\n", + " end_time = time.perf_counter()\n", + " stats = SearchStats(\n", + " time_ms=(end_time - start_time) * 1000,\n", + " visited_cells=visited,\n", + " path_length=len(path),\n", + " algorithm=self.strategy.__class__.__name__\n", + " )\n", + " return path, stats\n", + "\n", + "\n", + "# Этап 6: Экспериментальная часть \n", + "def test_single_maze(filename, strategies, repeats=5):\n", + " builder = TextFileMazeBuilder()\n", + " maze = builder.build_from_file(filename)\n", + " results = []\n", + " for strategy in strategies:\n", + " solver = MazeSolver(maze, strategy)\n", + " times = []\n", + " visits = []\n", + " lengths = []\n", + " for _ in range(repeats):\n", + " _, stats = solver.solve()\n", + " times.append(stats.time_ms)\n", + " visits.append(stats.visited_cells)\n", + " lengths.append(stats.path_length)\n", + " results.append({\n", + " 'algorithm': strategy.__class__.__name__,\n", + " 'avg_time_ms': sum(times) / repeats,\n", + " 'avg_visited': sum(visits) / repeats,\n", + " 'avg_path_len': sum(lengths) / repeats\n", + " })\n", + " return results\n", + "\n", + "\n", + "if __name__ == \"__main__\":\n", + " maze_files = [\n", + " \"tiny.txt\",\n", + " \"medium.txt\",\n", + " \"large.txt\",\n", + " \"empty.txt\",\n", + " \"no_exit.txt\"\n", + " ]\n", + " strategies = [BFSStrategy(), DFSStrategy(), AStarStrategy()]\n", + " all_results = []\n", + "\n", + " for maze_file in maze_files:\n", + " try:\n", + " results = test_single_maze(maze_file, strategies)\n", + " for r in results:\n", + " r['maze'] = maze_file\n", + " all_results.append(r)\n", + " # Краткий вывод результатов\n", + " print(f\"\\n{maze_file}:\")\n", + " for r in results:\n", + " print(f\" {r['algorithm']}: {r['avg_time_ms']:.3f} мс, \"\n", + " f\"посещено {r['avg_visited']:.1f}, путь {r['avg_path_len']:.1f}\")\n", + " except Exception as e:\n", + " print(f\"Ошибка при обработке {maze_file}: {e}\")\n", + "\n", + " # Сохранение CSV\n", + " if all_results:\n", + " with open('all_results.csv', 'w', newline='', encoding='utf-8') as f:\n", + " writer = csv.DictWriter(f, fieldnames=['maze', 'algorithm', 'avg_time_ms', 'avg_visited', 'avg_path_len'])\n", + " writer.writeheader()\n", + " writer.writerows(all_results)\n", + "\n", + " # Построение графиков\n", + " df = pd.DataFrame(all_results)\n", + " for maze in df['maze'].unique():\n", + " subset = df[df['maze'] == maze]\n", + " plt.figure()\n", + " plt.bar(subset['algorithm'], subset['avg_time_ms'])\n", + " plt.title(f'Сравнение алгоритмов на лабиринте {maze}')\n", + " plt.ylabel('Среднее время (мс)')\n", + " plt.savefig(f'plot_{maze}.png')\n", + " plt.close()\n", + "\n", + " plt.figure(figsize=(10, 6))\n", + " for alg in df['algorithm'].unique():\n", + " subset = df[df['algorithm'] == alg]\n", + " plt.plot(subset['maze'], subset['avg_time_ms'], marker='o', label=alg)\n", + " plt.xlabel('Лабиринт')\n", + " plt.ylabel('Среднее время (мс)')\n", + " plt.title('Сравнение эффективности алгоритмов на разных лабиринтах')\n", + " plt.legend()\n", + " plt.grid(True)\n", + " plt.savefig('summary_comparison.png')\n", + " plt.show()\n", + " else:\n", + " print(\"Нет данных для построения графиков. Проверьте файлы лабиринтов.\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a7d0ed68-a3a6-4db3-aaf5-1eeaa103838f", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/filippovavm/docs/МП.ipynb b/filippovavm/docs/МП.ipynb new file mode 100644 index 0000000..e25dd7e --- /dev/null +++ b/filippovavm/docs/МП.ipynb @@ -0,0 +1,594 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "dbe95ca0-7bc2-4cea-bdbb-319e9a1bd5b6", + "metadata": {}, + "source": [ + "# Импорт библиотек\n", + "# В этом блоке подключаются модули для работы со временем, случайными числами, CSV, системными параметрами, а также для построения графиков." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "c0b2cd62-6c05-4896-8f40-82d75ae765e9", + "metadata": {}, + "outputs": [], + "source": [ + "import time\n", + "import random\n", + "import csv\n", + "import sys\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np" + ] + }, + { + "cell_type": "markdown", + "id": "919b92b0-2819-457a-87a0-8f26eaeca817", + "metadata": {}, + "source": [ + "# Связный список\n", + "# Каждый узел содержит имя, телефон и ссылку на следующий узел.\n", + "# Функции: ll_insert (вставка/обновление), ll_find (поиск), ll_delete (удаление)." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "30700662-1215-4476-bffe-96ad9d3f1ab4", + "metadata": {}, + "outputs": [], + "source": [ + "# Связный список\n", + "def ll_insert(head, name, phone):\n", + " current = head\n", + " prev = None\n", + " while current is not None:\n", + " if current['name'] == name:\n", + " current['phone'] = phone\n", + " return head\n", + " prev = current\n", + " current = current['next']\n", + " new_node = {'name': name, 'phone': phone, 'next': None}\n", + " if prev is None:\n", + " return new_node\n", + " else:\n", + " prev['next'] = new_node\n", + " return head\n", + "\n", + "def ll_find(head, name):\n", + " current = head\n", + " while current is not None:\n", + " if current['name'] == name:\n", + " return current['phone']\n", + " current = current['next']\n", + " return None\n", + "\n", + "def ll_delete(head, name):\n", + " if head is None:\n", + " return None\n", + " if head['name'] == name:\n", + " return head['next']\n", + " current = head\n", + " while current['next'] is not None:\n", + " if current['next']['name'] == name:\n", + " current['next'] = current['next']['next']\n", + " return head\n", + " current = current['next']\n", + " return head" + ] + }, + { + "cell_type": "markdown", + "id": "49dd7db0-58a7-4ff9-98cd-529e2e91ca94", + "metadata": {}, + "source": [ + "# Хеш-таблица\n", + "# Хеш-функция на основе полиномиального кода (31 и длина таблицы).\n", + "# Размер таблицы фиксирован (2000). В каждой ячейке хранится связный список.\n", + "# Функции: ht_create, ht_insert, ht_find, ht_delete." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "22800445-5217-45ce-9ecd-0f61389b1c3f", + "metadata": {}, + "outputs": [], + "source": [ + "# Хеш-таблица \n", + "def hash_function(name, size):\n", + " total = 0\n", + " for ch in name:\n", + " total = (total * 31 + ord(ch)) % size\n", + " return total\n", + "\n", + "def ht_create(size=2000):\n", + " return [None] * size\n", + "\n", + "def ht_insert(buckets, name, phone):\n", + " idx = hash_function(name, len(buckets))\n", + " buckets[idx] = ll_insert(buckets[idx], name, phone)\n", + "\n", + "def ht_find(buckets, name):\n", + " idx = hash_function(name, len(buckets))\n", + " return ll_find(buckets[idx], name)\n", + "\n", + "def ht_delete(buckets, name):\n", + " idx = hash_function(name, len(buckets))\n", + " buckets[idx] = ll_delete(buckets[idx], name)\n" + ] + }, + { + "cell_type": "markdown", + "id": "0b03af57-2771-4b20-8e7d-1ac475afaae8", + "metadata": {}, + "source": [ + "# Двоичное дерево поиска\n", + "# Узел содержит имя, телефон, ссылки на левого и правого потомка.\n", + "# Функции: bst_insert (вставка/обновление), bst_find (поиск), bst_delete (удаление).\n", + "# Для удаления используется поиск преемника (минимальный элемент в правом поддереве)." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "7ced846b-b65f-4cf2-8fb8-5a4849f55509", + "metadata": {}, + "outputs": [], + "source": [ + "# Двоичное дерево поиска \n", + "def bst_insert(root, name, phone):\n", + " new_node = {'name': name, 'phone': phone, 'left': None, 'right': None}\n", + " if root is None:\n", + " return new_node\n", + " current = root\n", + " while True:\n", + " if name < current['name']:\n", + " if current['left'] is None:\n", + " current['left'] = new_node\n", + " break\n", + " current = current['left']\n", + " elif name > current['name']:\n", + " if current['right'] is None:\n", + " current['right'] = new_node\n", + " break\n", + " current = current['right']\n", + " else:\n", + " current['phone'] = phone\n", + " break\n", + " return root\n", + "\n", + "def bst_find(root, name):\n", + " current = root\n", + " while current is not None:\n", + " if name < current['name']:\n", + " current = current['left']\n", + " elif name > current['name']:\n", + " current = current['right']\n", + " else:\n", + " return current['phone']\n", + " return None\n", + "\n", + "def bst_find_min(node):\n", + " while node['left'] is not None:\n", + " node = node['left']\n", + " return node\n", + "\n", + "def bst_delete(root, name):\n", + " parent = None\n", + " current = root\n", + " while current is not None and current['name'] != name:\n", + " parent = current\n", + " if name < current['name']:\n", + " current = current['left']\n", + " else:\n", + " current = current['right']\n", + " if current is None:\n", + " return root\n", + " \n", + " if current['left'] is None and current['right'] is None:\n", + " if parent is None:\n", + " return None\n", + " if parent['left'] is current:\n", + " parent['left'] = None\n", + " else:\n", + " parent['right'] = None\n", + " return root\n", + " if current['left'] is None:\n", + " if parent is None:\n", + " return current['right']\n", + " if parent['left'] is current:\n", + " parent['left'] = current['right']\n", + " else:\n", + " parent['right'] = current['right']\n", + " return root\n", + " if current['right'] is None:\n", + " if parent is None:\n", + " return current['left']\n", + " if parent['left'] is current:\n", + " parent['left'] = current['left']\n", + " else:\n", + " parent['right'] = current['left']\n", + " return root\n", + " \n", + " succ_parent = current\n", + " succ = current['right']\n", + " while succ['left'] is not None:\n", + " succ_parent = succ\n", + " succ = succ['left']\n", + " current['name'] = succ['name']\n", + " current['phone'] = succ['phone']\n", + " if succ_parent['left'] is succ:\n", + " succ_parent['left'] = succ['right']\n", + " else:\n", + " succ_parent['right'] = succ['right']\n", + " return root" + ] + }, + { + "cell_type": "markdown", + "id": "88e1d5ec-5123-4bff-837a-bb451a0125c3", + "metadata": {}, + "source": [ + "# Генерация записей телефонной книги\n", + "# Создаёт N записей вида User_00001 и случайный номер телефона." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b266c127-99a7-479c-a012-3eedeff7ade3", + "metadata": {}, + "outputs": [], + "source": [ + "# Генерация данных \n", + "def generate_records(N):\n", + " records = []\n", + " for i in range(N):\n", + " name = f\"User_{i:05d}\"\n", + " phone = f\"+7-999-{random.randint(1000000, 9999999)}\"\n", + " records.append((name, phone))\n", + " return records" + ] + }, + { + "cell_type": "markdown", + "id": "a92500c8-e928-46a8-a115-12cc033ea2da", + "metadata": {}, + "source": [ + "# Функции замеров\n", + "# measure_insert – вставка всех записей (повторяется 5 раз).\n", + "# build_structure – построение структуры данных (используется для последующих замеров).\n", + "# measure_find_on_structure – поиск 110 записей (100 существующих + 10 отсутствующих) 5 раз.\n", + "# measure_delete_on_structure – удаление 50 случайных записей 5 раз." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5060601-6e07-4148-9faa-f9b7b5f433dd", + "metadata": {}, + "outputs": [], + "source": [ + "# Замеры\n", + "REPEATS = 5\n", + "N = 10000\n", + "\n", + "def measure_insert(struct, records, repeats=REPEATS):\n", + " times = []\n", + " for _ in range(repeats):\n", + " if struct == 'll':\n", + " head = None\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " end = time.perf_counter()\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " end = time.perf_counter()\n", + " elif struct == 'bst':\n", + " root = None\n", + " start = time.perf_counter()\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " end = time.perf_counter()\n", + " times.append(end - start)\n", + " return times\n", + "\n", + "def build_structure(struct, records):\n", + " if struct == 'll':\n", + " head = None\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " return head\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " return buckets\n", + " elif struct == 'bst':\n", + " root = None\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " return root\n", + " def measure_find_on_structure(struct, structure, records, repeats=REPEATS):\n", + " times = []\n", + " N = len(records)\n", + " for _ in range(repeats):\n", + " indices = random.sample(range(N), 100)\n", + " exist = [records[i][0] for i in indices]\n", + " missing = [f\"None_{i}\" for i in range(10)]\n", + " search = exist + missing\n", + " start = time.perf_counter()\n", + " if struct == 'll':\n", + " for name in search:\n", + " ll_find(structure, name)\n", + " elif struct == 'ht':\n", + " for name in search:\n", + " ht_find(structure, name)\n", + " elif struct == 'bst':\n", + " for name in search:\n", + " bst_find(structure, name)\n", + " times.append(time.perf_counter() - start)\n", + " return times\n", + "\n", + "def measure_delete_on_structure(struct, records, repeats=REPEATS):\n", + " times = []\n", + " N = len(records)\n", + " for _ in range(repeats):\n", + " indices = random.sample(range(N), 50)\n", + " del_names = [records[i][0] for i in indices]\n", + " if struct == 'll':\n", + " head = None\n", + " for name, phone in records:\n", + " head = ll_insert(head, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " head = ll_delete(head, name)\n", + " end = time.perf_counter()\n", + " elif struct == 'ht':\n", + " buckets = ht_create(2000)\n", + " for name, phone in records:\n", + " ht_insert(buckets, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " ht_delete(buckets, name)\n", + " end = time.perf_counter()\n", + " elif struct == 'bst':\n", + " root = None\n", + " for name, phone in records:\n", + " root = bst_insert(root, name, phone)\n", + " start = time.perf_counter()\n", + " for name in del_names:\n", + " root = bst_delete(root, name)\n", + " end = time.perf_counter()\n", + " times.append(end - start)\n", + " return times\n" + ] + }, + { + "cell_type": "markdown", + "id": "d93d8eac-b094-43b2-8af0-9afb0ab51ada", + "metadata": {}, + "source": [ + "# Основная функция\n", + "# 1. Генерирует 10000 записей в случайном и отсортированном порядке.\n", + "# 2. Измеряет время вставки всех записей, поиска (110 запросов) и удаления (50 записей)\n", + "# для связного списка, хеш-таблицы и дерева.\n", + "# 3. Выводит средние значения, сохраняет все замеры в CSV.\n", + "# 4. Строит несколько графиков (сравнительные столбчатые диаграммы и графики по попыткам)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a667779c-39b8-4e4b-b05d-bf9c9cccbbd9", + "metadata": {}, + "outputs": [], + "source": [ + "# Основная функция \n", + "def main():\n", + " print(\"Генерация данных...\")\n", + " records = generate_records(N)\n", + " random.shuffle(records) # случайный порядок\n", + " records_sorted = sorted(records, key=lambda x: x[0]) # отсортированный\n", + "\n", + " results = [] # для CSV\n", + " struct_names = {'ll': 'Связного списка', 'ht': 'Хеш-таблицы', 'bst': 'Двоичного дерева поиска'}\n", + " mode_names = {'shuffled': 'случайный', 'sorted': 'отсортированный'}\n", + " op_names = {'insert': 'Вставка всех записей', 'find': 'Поиск записей', 'delete': 'Удаление записей'}\n", + " # Вставка \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nИзмерение вставки для {struct_names[struct]}...\")\n", + " times_sh = measure_insert(struct, records)\n", + " times_so = measure_insert(struct, records_sorted)\n", + " insert_sh[struct] = times_sh\n", + " insert_so[struct] = times_so\n", + " print(f\" случайный: {[round(t,6) for t in times_sh]}, среднее = {sum(times_sh)/len(times_sh):.6f}\")\n", + " print(f\" отсортированный: {[round(t,6) for t in times_so]}, среднее = {sum(times_so)/len(times_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['insert'], sum(times_sh)/len(times_sh)] + times_sh)\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['insert'], sum(times_so)/len(times_so)] + times_so)\n", + " # Поиск \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nПоиск для {struct_names[struct]} на случайных данных...\")\n", + " structure_sh = build_structure(struct, records)\n", + " times_find_sh = measure_find_on_structure(struct, structure_sh, records)\n", + " find_sh[struct] = times_find_sh\n", + " print(f\" случайный: {[round(t,6) for t in times_find_sh]}, среднее = {sum(times_find_sh)/len(times_find_sh):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['find'], sum(times_find_sh)/len(times_find_sh)] + times_find_sh)\n", + "\n", + " print(f\"Поиск для {struct_names[struct]} на отсортированных данных...\")\n", + " structure_so = build_structure(struct, records_sorted)\n", + " times_find_so = measure_find_on_structure(struct, structure_so, records_sorted)\n", + " find_so[struct] = times_find_so\n", + " print(f\" отсортированный: {[round(t,6) for t in times_find_so]}, среднее = {sum(times_find_so)/len(times_find_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['find'], sum(times_find_so)/len(times_find_so)] + times_find_so)\n", + " # Удаление \n", + " for struct in ['ll', 'ht', 'bst']:\n", + " print(f\"\\nУдаление для {struct_names[struct]} на случайных данных...\")\n", + " times_del_sh = measure_delete_on_structure(struct, records)\n", + " delete_sh[struct] = times_del_sh\n", + " print(f\" случайный: {[round(t,6) for t in times_del_sh]}, среднее = {sum(times_del_sh)/len(times_del_sh):.6f}\")\n", + " results.append([struct_names[struct], mode_names['shuffled'], op_names['delete'], sum(times_del_sh)/len(times_del_sh)] + times_del_sh)\n", + "\n", + " print(f\"Удаление для {struct_names[struct]} на отсортированных данных...\")\n", + " times_del_so = measure_delete_on_structure(struct, records_sorted)\n", + " delete_so[struct] = times_del_so\n", + " print(f\" отсортированный: {[round(t,6) for t in times_del_so]}, среднее = {sum(times_del_so)/len(times_del_so):.6f}\")\n", + " results.append([struct_names[struct], mode_names['sorted'], op_names['delete'], sum(times_del_so)/len(times_del_so)] + times_del_so)\n", + " # Сохраняем CSV\n", + " with open(\"phonebook_results.csv\", \"w\", newline=\"\", encoding=\"utf-8\") as f:\n", + " writer = csv.writer(f)\n", + " writer.writerow(['Структура', 'Режим', 'Операция', 'Среднее', 'Замер1', 'Замер2', 'Замер3', 'Замер4', 'Замер5'])\n", + " writer.writerows(results)\n", + "# Графики замеров\n", + " try:\n", + " def plot_attempts(data_sh, data_so, op_name):\n", + " fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))\n", + " # случайный порядок\n", + " for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n", + " times = data_sh[struct]\n", + " x = range(1, len(times)+1)\n", + " ax1.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n", + " ax1.scatter(x, times, color=color, s=60, zorder=5)\n", + " ax1.set_xlabel('Номер попытки')\n", + " ax1.set_ylabel('Время (сек)')\n", + " ax1.set_title(f'{op_name} – случайный порядок')\n", + " ax1.legend()\n", + " ax1.grid(True, linestyle=':', alpha=0.7)\n", + " # отсортированный порядок\n", + " for struct, label, color, marker in [('ll','LinkedList','red','o'), ('ht','HashTable','green','s'), ('bst','BST','blue','^')]:\n", + " times = data_so[struct]\n", + " x = range(1, len(times)+1)\n", + " ax2.plot(x, times, marker=marker, color=color, label=label, linestyle='--', linewidth=1)\n", + " ax2.scatter(x, times, color=color, s=60, zorder=5)\n", + " ax2.set_xlabel('Номер попытки')\n", + " ax2.set_ylabel('Время (сек)')\n", + " ax2.set_title(f'{op_name} – отсортированный порядок')\n", + " ax2.legend()\n", + " ax2.grid(True, linestyle=':', alpha=0.7)\n", + " plt.tight_layout()\n", + " plt.savefig(f'{op_name}_5attempts.png')\n", + " plt.show()\n", + " \n", + " plot_attempts(insert_sh, insert_so, 'insert')\n", + " plot_attempts(find_sh, find_so, 'find')\n", + " plot_attempts(delete_sh, delete_so, 'delete')\n", + " print(\"Дополнительные графики сохранены: insert_5attempts.png, find_5attempts.png, delete_5attempts.png\")\n", + " except Exception as e:\n", + " print(f\"Не удалось построить дополнительные графики: {e}\")\n", + "try:\n", + " # График вставки\n", + " fig1, ax1 = plt.subplots(figsize=(10,6))\n", + " x = np.arange(3)\n", + " width = 0.35\n", + " means_sh = [sum(insert_sh[s])/len(insert_sh[s]) for s in ['ll','ht','bst']]\n", + " means_so = [sum(insert_so[s])/len(insert_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax1.bar(x - width/2, means_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax1.bar(x + width/2, means_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax1.set_ylabel('Время (сек)')\n", + " ax1.set_title('Вставка всех записей')\n", + " ax1.set_xticks(x)\n", + " ax1.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax1.legend()\n", + " ax1.set_yscale('log')\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax1.annotate(f'{h:.3f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('insert_comparison.png')\n", + " plt.show()\n", + "\n", + " # График поиска\n", + " fig2, ax2 = plt.subplots(figsize=(10,6))\n", + " means_find_sh = [sum(find_sh[s])/len(find_sh[s]) for s in ['ll','ht','bst']]\n", + " means_find_so = [sum(find_so[s])/len(find_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax2.bar(x - width/2, means_find_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax2.bar(x + width/2, means_find_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax2.set_ylabel('Время (сек)')\n", + " ax2.set_title('Поиск (100 существующих + 10 отсутствующих)')\n", + " ax2.set_xticks(x)\n", + " ax2.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax2.legend()\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax2.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('find_comparison.png')\n", + " plt.show()\n", + "\n", + " # График удаления \n", + " fig3, ax3 = plt.subplots(figsize=(10,6))\n", + " means_del_sh = [sum(delete_sh[s])/len(delete_sh[s]) for s in ['ll','ht','bst']]\n", + " means_del_so = [sum(delete_so[s])/len(delete_so[s]) for s in ['ll','ht','bst']]\n", + " rects1 = ax3.bar(x - width/2, means_del_sh, width, label='Случайный порядок', color='skyblue')\n", + " rects2 = ax3.bar(x + width/2, means_del_so, width, label='Отсортированный порядок', color='salmon')\n", + " ax3.set_ylabel('Время (сек)')\n", + " ax3.set_title('Удаление 50 случайных записей')\n", + " ax3.set_xticks(x)\n", + " ax3.set_xticklabels(['Связный список', 'Хеш-таблица', 'Двоичное дерево'])\n", + " ax3.legend()\n", + " for rect in rects1 + rects2:\n", + " h = rect.get_height()\n", + " ax3.annotate(f'{h:.5f}', xy=(rect.get_x()+rect.get_width()/2, h),\n", + " xytext=(0,3), textcoords=\"offset points\", ha='center', va='bottom', fontsize=8)\n", + " plt.tight_layout()\n", + " plt.savefig('delete_comparison.png')\n", + " plt.show()\n", + " print(\"Графики сохранены: insert_comparison.png, find_comparison.png, delete_comparison.png\")\n", + " except Exception as e:\n", + " print(f\"Не удалось построить графики: {e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "dd2b18cd-f37c-4f9f-a92a-325be857deb0", + "metadata": {}, + "source": [ + "# Запуск эксперимента\n", + "# Устанавливается увеличенная глубина рекурсии (на случай глубоких деревьев) и вызывается main()." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "04462e9d-aecc-44b3-b98b-0d06f856f38a", + "metadata": {}, + "outputs": [], + "source": [ + "if __name__ == \"__main__\":\n", + " sys.setrecursionlimit(20000)\n", + " main()" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/filippovavm/docs/отчёт1.ipynb b/filippovavm/docs/отчёт1.ipynb new file mode 100644 index 0000000..5a91b0f --- /dev/null +++ b/filippovavm/docs/отчёт1.ipynb @@ -0,0 +1,149 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0c973489-075d-42ac-a28f-f8d8d954a0da", + "metadata": {}, + "source": [ + "# Анализ результатов\n", + "\n", + "## Предложенные вопросы\n", + "\n", + "- Как порядок входных данных влияет на скорость вставки в BST (деградация до O(n) на отсортированных данных)?\n", + "- Почему хеш-таблица почти не чувствительна к порядку?\n", + "- Почему связный список всегда медленен при поиске?\n", + "- Как удаление работает в каждой структуре?\n", + "- Вывод: какую структуру и для каких задач (частые вставки, частый поиск, необходимость получать данные в порядке) стоит выбирать в реальной жизни?\n" + ] + }, + { + "cell_type": "markdown", + "id": "a265cc14-95ff-47ae-a346-ac38cb2a323e", + "metadata": {}, + "source": [ + "## Выводы\n", + "\n", + "### 1) Как порядок входных данных влияет на скорость вставки в BST?\n", + "\n", + "Порядок отличается очень сильно. В обычном случае сложность равна **O(log n)**, а в худшем случае (как раз на отсортированных данных) – **O(n)**. \n" + ] + }, + { + "cell_type": "markdown", + "id": "950c5e97-12e9-4225-a91e-b8289fdfb5e6", + "metadata": {}, + "source": [ + "### 2) Почему хеш-таблица почти не чувствительна к порядку?\n", + "\n", + "Это происходит из‑за особенностей записи данных в память. Хеш-таблица вычисляет номер строки (корзины) по математической формуле, поэтому любой элемент можно найти за **O(1)** в среднем. Порядок поступления записей не влияет на расчёт индекса.\n" + ] + }, + { + "cell_type": "markdown", + "id": "5f6059bf-e99a-4b14-869c-32fb44b092fa", + "metadata": {}, + "source": [ + "### 3) Почему связный список всегда медленен при поиске?\n", + "\n", + "Это происходит из‑за способа записи. Доступ к следующему элементу возможен только последовательным перебором, равным номеру искомой позиции. Сложность поиска – **O(n)**.\n" + ] + }, + { + "cell_type": "markdown", + "id": "77ddd385-a50d-4ab6-b761-477460529e9d", + "metadata": {}, + "source": [ + "### 4) Как удаление работает в каждой структуре?\n", + "\n", + "- **Связный список** \n", + " - Если список пустой → возвращаем `None`. \n", + " - Если удаляем голову → новой головой становится следующий элемент. \n", + " - Если удаляем промежуточный элемент – ищем нужный узел, затем у предыдущего узла меняем ссылку на следующий после удаляемого.\n", + "\n", + "- **Хеш-таблица** \n", + " Реализация вычисляет номер корзины через хеш-ключ, затем использует функции связного списка для работы внутри корзины. Таким образом, удаление в хеш-таблице наследует логику удаления из связного списка, но применяется только к элементам одной корзины.\n", + "\n", + "- **Бинарное дерево (BST)** \n", + " Рассматриваются 4 случая (логика похожа на связный список, но с учётом двух потомков): \n", + " - Если дерево пустое → вернуть `None`. \n", + " - Если удаляемый элемент меньше корня → спуститься в левую ветвь. \n", + " - Если больше корня → спуститься в правую ветвь. \n", + " - Если у удаляемого узла два потомка – находим преемника (самый левый узел в правом поддереве), копируем его данные в удаляемый узел и удаляем преемника. \n", + " При нахождении элемента ссылка от родителя к удаляемому узлу заменяется на ссылку на соответствующего потомка (левый или правый).\n" + ] + }, + { + "cell_type": "markdown", + "id": "97b09bb6-e8ef-486e-9cdd-fc37b19bfeb2", + "metadata": {}, + "source": [ + "### 5) Какую структуру и для каких задач стоит выбирать в реальной жизни?\n", + "\n", + "- **Для частых вставок и поиска элементов** – лучше всего использовать **хеш-таблицу**, так как добавление происходит за счёт математического вычисления индекса, а не последовательного перебора. \n", + "- **Если нужны упорядоченные данные** (например, вывод записей в алфавитном порядке) – подходит **двоичное дерево поиска** (BST) благодаря свойству in‑order обхода. \n", + "- **Связный список** неэффективен для больших объёмов данных.\n", + "\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "3b242f54-fd27-4f6e-8a31-9b3a6bee5f59", + "metadata": {}, + "source": [ + "## Дополнительные числовые результаты\n", + "\n", + "\n", + "\n", + "1. **Связный список:** \n", + " - Вставка: O(n) → ~0.25 с, порядок данных не влияет. \n", + " - Поиск: O(n) → очень медленно (~0.5 с), порядок не влияет. \n", + " - Удаление: O(n) → медленно.\n", + "\n", + "2. **Хеш-таблица:** \n", + " - Вставка: O(1) в среднем → ~0.008 с, порядок данных не влияет. \n", + " - Поиск: O(1) → ~0.002 с, самый быстрый. \n", + " - Удаление: O(1) → ~0.002 с.\n", + "\n", + "3. **Двоичное дерево поиска:** \n", + " - На случайных данных: O(log n) → вставка ~0.018 с, поиск ~0.0015 с, удаление ~0.0016 с. \n", + " - На отсортированных данных: дерево вырождается в линейный список → вставка ~2.3 с, поиск и удаление также становятся O(n) (на графиках виден рост времени).\n", + "\n", + "**ИТОГОВЫЙ ВЫВОД:** \n", + "- Для частого поиска, вставки и удаления – лучший выбор **хеш-таблица**. \n", + "- Если данные поступают в отсортированном порядке – BST не подходит из‑за деградации. \n", + "- Если нужна частая выдача записей в алфавитном порядке и данные случайны – BST хорош. \n", + "- Связный список неэффективен для больших объёмов." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1b39f136-0c95-46f0-b794-7136232bcd3c", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.5" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}