diff --git a/REPORT_V_PREDFINAL.docx b/REPORT_V_PREDFINAL.docx new file mode 100644 index 0000000..38a377a Binary files /dev/null and b/REPORT_V_PREDFINAL.docx differ diff --git a/REPORT_V_PREDFINAL.md b/REPORT_V_PREDFINAL.md new file mode 100644 index 0000000..a02a3f0 --- /dev/null +++ b/REPORT_V_PREDFINAL.md @@ -0,0 +1,534 @@ +# Введение + +Поиск пути экстремальной длины во взвешенном графе — одна из базовых задач дискретной математики, на решении которой построены навигационные сервисы, протоколы маршрутизации компьютерных сетей и системы логистического планирования. Под экстремальным понимается путь, на котором суммарный вес рёбер достигает минимума или максимума: при прокладке маршрута по дорожной сети минимизируется расстояние или время в пути, при анализе нагруженных и критических цепочек, напротив, требуется путь наибольшей стоимости. Классические алгоритмы решения этой задачи — алгоритм Дейкстры [2], A\* [3], алгоритм Флойда–Уоршелла [4] — опубликованы в 1959–1968 годах и до настоящего времени составляют ядро производственных систем маршрутизации; смежная задача построения минимального остовного дерева решается алгоритмами Краскала [5] и Прима [6]. + +При изучении этих алгоритмов по учебнику [1] студент видит псевдокод и оценки сложности, но не видит динамику: как волновой фронт BFS расходится по слоям, почему алгоритм Дейкстры нельзя применять при отрицательных весах, как эвристика A\* сокращает область поиска по сравнению с равномерным обходом. Существующие библиотеки для работы с графами (NetworkX [7], igraph, graph-tool) выполняют алгоритм целиком и возвращают готовый ответ, не раскрывая промежуточных состояний. Инструмента, который одновременно показывал бы пошаговое исполнение алгоритма на произвольном графе и позволял проверить тот же алгоритм на реальной дорожной сети, среди открытых решений найти не удалось — этим определяется актуальность настоящей работы. + +**Объект исследования** — алгоритмы поиска экстремальных путей и связанных с ними оптимальных структур (минимальных остовных деревьев) на взвешенных графах. + +**Предмет исследования** — программная реализация этих алгоритмов в составе автономного клиентского приложения, обеспечивающего ввод графа, выбор и параметризацию алгоритма, пошаговое исполнение с визуализацией, а также загрузку реальных дорожных сетей из открытых картографических данных. + +**Цель работы** — разработать приложение для определения путей экстремальной длины (минимальной и максимальной) на графах, поддерживающее как синтетические графы, задаваемые пользователем вручную или генераторами, так и реальные дорожные графы, загружаемые из сервиса OpenStreetMap. + +Для достижения цели поставлены следующие задачи: + +1) провести аналитический обзор классических алгоритмов поиска экстремальных путей и построения остовных деревьев: BFS, DFS, IDDFS, алгоритмов Дейкстры, A\*, Флойда–Уоршелла, Прима и Краскала; + +2) выполнить сравнительный анализ программных библиотек для работы с графами (NetworkX, igraph, graph-tool, pydot) и графических фреймворков (PySide6, Tkinter, PyQt5) и обосновать выбор технологического стека; + +3) спроектировать модульную архитектуру приложения с разделением на слои интерфейса, ядра, источников данных и фоновых задач; + +4) реализовать восемь алгоритмов в виде независимых классов с единым программным интерфейсом и пошаговой трассировкой; + +5) реализовать ввод графа через матрицу смежности, генераторы синтетических графов и импорт/экспорт форматов GraphML и JSON; + +6) реализовать загрузку дорожных сетей из OpenStreetMap через сервисы Nominatim и Overpass API, режим карты с растровой подложкой и дисковый кэш загруженных районов; + +7) реализовать интерактивное редактирование графа: перемещение и групповое выделение вершин, отмену и повтор операций, переключаемые темы оформления; + +8) протестировать реализованные алгоритмы модульными тестами и проверить работу приложения на реальных дорожных сетях; + +9) скомпилировать приложение в самодостаточный исполняемый файл для развёртывания на компьютерах без установленной среды Python. + +**Методы исследования:** анализ литературы по теории графов и формализация постановок задач, сравнительный анализ программных библиотек по заданным критериям, асимптотический анализ сложности алгоритмов, модульное тестирование, вычислительные эксперименты на синтетических графах и дорожных сетях, полученных из OpenStreetMap. + +**Практическая значимость.** Разработанное приложение Graphate применимо в учебном процессе по курсам «Дискретная математика» и «Алгоритмы и структуры данных»: каждый алгоритм снабжён теоретической справкой, интерактивным примером и пошаговой трассировкой, а загрузка реальных районов города позволяет демонстрировать поведение алгоритмов на графах в десятки тысяч вершин. Приложение распространяется одним исполняемым файлом и не требует установки интерпретатора Python. + +**Структура работы.** Работа состоит из введения, трёх глав, заключения и списка источников. В первой главе вводятся понятия теории графов, формализуется задача поиска экстремального пути и проводится аналитический обзор восьми реализованных алгоритмов с оценками временной и пространственной сложности. Во второй главе формулируются требования к приложению, обосновывается выбор технологий и описывается архитектура: слои, модель графа, система трассировки. Третья глава посвящена программной реализации: источникам данных, пользовательскому интерфейсу, фоновым задачам, тестированию и сборке исполняемого файла. + +# 1 Теоретические основы поиска экстремальных путей на графах + +## 1.1 Основные понятия теории графов и структуры хранения + +Определения этого подраздела приводятся по учебным пособиям [19, 20]. + +**Граф** — упорядоченная пара $G = (V, E)$, где $V$ — конечное непустое множество вершин, а $E$ — множество рёбер. Если ребро задано неупорядоченной парой $\{u, v\}$, граф называется неориентированным; если упорядоченной парой $(u, v)$ — ориентированным (орграфом). Мощности множеств обозначаются $|V| = n$ и $|E| = m$. + +Если ребру $(u, v) \in E$ поставлено в соответствие число $w(u, v) \in \mathbb{R}$, граф называется взвешенным. Весовая функция $w\colon E \rightarrow \mathbb{R}$ интерпретируется как длина, время в пути, стоимость или иная характеристика связи; в дорожных графах, используемых в приложении, вес ребра равен длине участка улицы в метрах. Граф, в котором $w(u, v) = 1$ для всех рёбер, называется невзвешенным. + +**Путём** от вершины $s$ до вершины $t$ называется конечная последовательность вершин $\langle v_0, v_1, \ldots, v_k \rangle$, в которой $v_0 = s$, $v_k = t$ и $(v_{i-1}, v_i) \in E$ для всех $i$. Путь, в котором все вершины различны, называется простым. Длиной пути в невзвешенном графе считается число рёбер $k$, во взвешенном — суммарный вес $\sum_{i=1}^{k} w(v_{i-1}, v_i)$. Путь с $v_0 = v_k$ называется циклом. Граф называется связным, если для любой пары вершин существует путь из одной в другую; разреженным — если $|E| \approx |V|$, плотным — если $|E| \approx |V|^2$. + +Выбор структуры хранения графа влияет на асимптотику алгоритмов [19, 20]. **Матрица смежности** — двумерный массив $A[n \times n]$, в котором $A[u][v] = w(u, v)$ при наличии ребра и $\infty$ в противном случае; хранение требует $O(n^2)$ памяти, проверка наличия ребра выполняется за $O(1)$, перечисление соседей вершины — за $O(n)$. Матрица удобна для алгоритма Флойда–Уоршелла. **Список смежности** хранит для каждой вершины список её соседей; памяти требуется $O(n + m)$, перечисление соседей вершины пропорционально её степени. Списки смежности используются в BFS, DFS, алгоритмах Дейкстры и A\*. **Список рёбер** — массив троек $(u, v, w)$ объёмом $O(m)$; он удобен для алгоритмов, обрабатывающих все рёбра последовательно, в частности для алгоритма Краскала. + +В разработанном приложении граф хранится в виде объекта `networkx.MultiDiGraph`, что внутренне соответствует списку смежности с произвольными атрибутами вершин и рёбер; обоснование этого выбора дано в подразделе 2.2. + +## 1.2 Постановка задачи поиска экстремального пути + +Пусть $\mathcal{P}_{s,t}$ — множество всех путей из вершины-источника $s$ в вершину-цель $t$ графа $G = (V, E)$ с весовой функцией $w\colon E \rightarrow \mathbb{R}$. Задача поиска экстремального пути состоит в нахождении пути + +$$P^{*} = \arg\min_{P \in \mathcal{P}_{s,t}} L(P) \quad \text{или} \quad P^{*} = \arg\max_{P \in \mathcal{P}_{s,t}} L(P),$$ + +где $L(P) = \sum_{(u,v) \in P} w(u, v)$ — суммарный вес пути. Приложение поддерживает оба направления оптимизации: при маршрутизации на дорожных сетях минимизируется длина пути, при анализе нагрузочных задач ищется максимальный путь. Направление выбирается переключателем в панели алгоритмов. + +По количеству источников и приёмников выделяют четыре канонические постановки [21, 22]: + +- SPSP (Single-Pair Shortest Path) — кратчайший путь между фиксированной парой вершин $(s, t)$; решается BFS, DFS, IDDFS, алгоритмами Дейкстры и A\*; +- SSSP (Single-Source Shortest Path) — кратчайшие пути от источника $s$ до всех остальных вершин; именно эту задачу решает алгоритм Дейкстры; +- SDSP (Single-Destination Shortest Path) — кратчайшие пути от всех вершин до одной целевой; для неориентированного графа симметрично сводится к SSSP; +- APSP (All-Pairs Shortest Path) — кратчайшие пути между всеми парами вершин; решается алгоритмом Флойда–Уоршелла. + +Корректность классических алгоритмов опирается на два свойства [21, 22]. **Принцип оптимальности Беллмана**: любой подпуть кратчайшего пути сам является кратчайшим — если $\langle v_0, \ldots, v_k \rangle$ — кратчайший путь из $v_0$ в $v_k$, то для любых $0 \leq i \leq j \leq k$ подпуть $\langle v_i, \ldots, v_j \rangle$ является кратчайшим путём из $v_i$ в $v_j$. На этом свойстве основано динамическое программирование во всех алгоритмах SSSP и APSP. **Операция релаксации** ребра $(u, v)$: если $d[v]$ — текущая верхняя оценка расстояния от $s$ до $v$, то выполняется проверка + +$$\text{если } d[u] + w(u, v) < d[v]\colon \quad d[v] \leftarrow d[u] + w(u, v), \quad \pi[v] \leftarrow u,$$ + +где $\pi[v]$ — предшественник вершины $v$, по цепочке которых восстанавливается сам путь. + +Кратчайший путь существует и конечен для всех вершин, достижимых из $s$, если граф не содержит достижимых из источника циклов отрицательного веса; при наличии такого цикла понятие кратчайшего пути теряет смысл. В приложении используются веса $w \geq 0$, что обеспечивает корректность алгоритмов Дейкстры и A\*. + +При выборе алгоритма учитываются свойства входного графа: взвешенность (невзвешенные графы обрабатываются BFS, взвешенные требуют алгоритмов Дейкстры, A\* или Флойда–Уоршелла), знак весов (алгоритм Дейкстры применим только при $w \geq 0$; при отрицательных рёбрах без отрицательных циклов корректен алгоритм Беллмана–Форда), плотность (для разреженных графов эффективнее списки смежности с двоичной кучей, для плотных — матрица смежности), наличие геометрической интерпретации (если вершины соответствуют точкам метрического пространства, применим A\* с евклидовой или манхэттенской эвристикой). Для целевых размеров графов в приложении — учебные графы до 500 вершин и районные дорожные сети до 30 000 вершин — производительности реализаций на Python достаточно. + +Восемь реализованных алгоритмов отобраны по трём критериям: применение в производственных системах, наличие фундаментальных публикаций и дидактическая ценность для демонстрации различных стратегий поиска. Вместе они покрывают все канонические классы задач: неинформированный обход (BFS, DFS, IDDFS), SSSP с неотрицательными весами (Дейкстра, A\*), APSP (Флойд–Уоршелл) и MST (Прим, Краскал). + +## 1.3 Неинформированные алгоритмы обхода + +### 1.3.1 Поиск в ширину (BFS) + +Поиск в ширину (Breadth-First Search) изложен в учебнике Кормена и др. [1] и применяется везде, где требуется кратчайший путь по числу рёбер: анализ связности, выделение компонент, проверка двудольности. Алгоритм просматривает граф «послойно»: сначала всех соседей источника, затем соседей соседей, и так далее; благодаря этому первая встреча с вершиной даёт кратчайший по числу рёбер путь до неё на произвольном невзвешенном графе. В приложение BFS включён как базовый неинформированный алгоритм: его трассировка наглядно показывает движение волнового фронта. + +Псевдокод алгоритма: + +``` +BFS(G, s): + for each v ∈ V: d[v] ← ∞; π[v] ← NIL + d[s] ← 0; Q ← {s} + while Q ≠ ∅: + u ← dequeue(Q) + for each v ∈ Adj(u): + if d[v] = ∞: + d[v] ← d[u] + 1; π[v] ← u + enqueue(Q, v) +``` + +Здесь $d[v]$ — расстояние (число рёбер) от $s$ до $v$, $\pi[v]$ — предшественник $v$ в дереве обхода, $Q$ — очередь FIFO, $Adj(u)$ — список соседей вершины $u$. + +Каждая вершина помещается в очередь не более одного раза (это гарантируется проверкой $d[v] = \infty$); для каждой извлечённой вершины перебираются все её соседи, суммарно $\sum_{u \in V} \deg(u) = O(|E|)$ операций. Итоговая временная сложность $O(|V| + |E|)$, пространственная — $O(|V|)$ на очередь и массивы $d$, $\pi$. При хранении графа матрицей смежности перечисление соседей каждой вершины стоит $O(|V|)$, что даёт $O(|V|^2)$ и неприемлемо для разреженных графов — поэтому в реализации используется список смежности. В реализации очередь представлена структурой `collections.deque`, обеспечивающей вставку и извлечение за $O(1)$. + +### 1.3.2 Поиск в глубину (DFS) + +Поиск в глубину (Depth-First Search) [1] лежит в основе выделения компонент связности, топологической сортировки и обнаружения циклов. В отличие от BFS, который удерживает в памяти весь текущий слой, DFS хранит только вершины активного пути, «погружаясь» вглубь графа до тупика и возвращаясь назад. Найденный DFS путь не обязан быть кратчайшим — это свойство специально демонстрируется в приложении при сравнении с BFS на одном и том же графе. + +В реализации выбрана итеративная схема с явным стеком вместо рекурсивной: на графах в тысячи вершин рекурсия в Python упирается в предел глубины стека вызовов (по умолчанию 1000 кадров). На стеке хранятся записи $(u, Adj(u), i)$, где $i$ — индекс следующего непросмотренного соседа: + +``` +DFS(G, s): + for each v ∈ V: visited[v] ← false; π[v] ← NIL + S ← [(s, Adj(s), 0)]; visited[s] ← true + while S ≠ ∅: + (u, N, i) ← top(S) + if i = |N|: pop(S); continue + w ← N[i]; top(S).i ← i + 1 + if not visited[w]: + visited[w] ← true; π[w] ← u + push(S, (w, Adj(w), 0)) +``` + +Каждая вершина помечается посещённой не более одного раза, каждое ребро просматривается не более двух раз, поэтому временная сложность составляет $O(|V| + |E|)$. Стек в худшем случае (граф-цепочка) достигает глубины $|V|$, итого $O(|V|)$ памяти — для «кустообразных» графов это существенно меньше, чем объём очереди BFS, удерживающей целый уровень. + +### 1.3.3 Итеративный поиск с углублением (IDDFS) + +Алгоритм IDDFS (Iterative Deepening Depth-First Search) предложен Корфом в 1985 году [1] и объединяет оптимальность BFS по числу рёбер с линейным расходом памяти DFS. Запускается серия поисков в глубину с ограничением глубины $\ell = 0, 1, 2, \ldots$; в памяти при этом хранится только текущий путь: + +``` +IDDFS(G, s, t): + for depth ← 0, 1, 2, …: + result ← DLS(G, s, t, depth) + if result ≠ CUTOFF: return result + +DLS(G, v, t, limit): + if v = t: return path + if limit = 0: return CUTOFF + found_cutoff ← false + for each w ∈ Adj(v): + if w not in current_path: + result ← DLS(G, w, t, limit − 1) + if result = CUTOFF: found_cutoff ← true + elif result ≠ FAILURE: return result + return CUTOFF if found_cutoff else FAILURE +``` + +Сигнал CUTOFF означает, что хотя бы одна ветвь обрезана по пределу глубины и требуется итерация с большим лимитом; FAILURE — что путь не существует. Множество $current\_path$ защищает от зацикливания, при этом, в отличие от глобального множества посещённых вершин DFS, при откате вершина из него удаляется и может быть достигнута по другому пути. + +Пусть $b$ — максимальное ветвление графа, $\ell$ — глубина решения. На итерации с пределом $d$ просматривается $O(b^d)$ узлов; суммарная работа $b^0 + b^1 + \ldots + b^{\ell} = O(b^{\ell})$, так как узлы последнего уровня доминируют над всеми предыдущими. Повторные обходы добавляют множитель $b/(b-1)$: при $b = 2$ это двукратный проигрыш, при $b = 10$ — лишь 1,11 раза. Память составляет $O(\ell)$ — главное преимущество перед BFS. Вырожденный случай — граф-цепочка ($b = 1$), на которой повторные обходы дают $O(\ell^2)$; этот случай также воспроизводится в приложении на решётчатых графах. IDDFS включён в приложение для демонстрации компромисса «время–память»: цена повторных обходов уровней видна непосредственно в счётчике шагов трассировки. + +## 1.4 Алгоритмы поиска кратчайших путей во взвешенных графах + +### 1.4.1 Алгоритм Дейкстры + +Алгоритм опубликован Э. Дейкстрой в 1959 году [2] и является стандартным решением задачи SSSP при неотрицательных весах рёбер. Алгоритм жадно «фиксирует» вершины в порядке возрастания расстояния от источника: на каждом шаге из приоритетной очереди извлекается вершина $u$ с минимальной оценкой $d[u]$, после чего все исходящие из неё рёбра релаксируются. При $w \geq 0$ извлечённая оценка уже не может быть улучшена, поэтому каждая вершина обрабатывается один раз. + +``` +Dijkstra(G, w, s): + for each v ∈ V: d[v] ← ∞; π[v] ← NIL + d[s] ← 0; H ← {(0, s)} + while H ≠ ∅: + (du, u) ← extract_min(H) + if du > d[u]: continue // устаревшая запись + for each (u, v) ∈ E: + if d[u] + w(u, v) < d[v]: + d[v] ← d[u] + w(u, v); π[v] ← u + insert(H, (d[v], v)) +``` + +В реализации приоритетная очередь построена на двоичной куче (модуль `heapq` стандартной библиотеки Python). Вместо операции уменьшения ключа в кучу добавляется новая запись, а устаревшие записи отбрасываются при извлечении проверкой $du > d[u]$ — такая схема («ленивое удаление») проще и на разреженных графах не уступает по скорости. Всего выполняется $O(|E|)$ вставок и не более $O(|E|)$ извлечений, каждая операция стоит $O(\log |V|)$; итоговая временная сложность $O((|V| + |E|)\log |V|)$, память $O(|V| + |E|)$. На плотных графах ($|E| = O(|V|^2)$) выгоднее реализация с простым массивом за $O(|V|^2)$, однако для разреженных дорожных сетей, являющихся основным сценарием приложения, двоичная куча эффективнее. + +Применимость алгоритма ограничена неотрицательными весами: отрицательное ребро может сделать уже зафиксированную оценку неоптимальной, и жадная стратегия теряет корректность. Этот эффект объясняется в теоретической справке приложения с отсылкой к алгоритму Беллмана–Форда (подраздел 1.4.4). + +### 1.4.2 Алгоритм A\* + +Алгоритм A\* предложен Хартом, Нильссоном и Рафаэлем в 1968 году [3] как информированное обобщение алгоритма Дейкстры для задачи SPSP. Для каждой вершины $v$ поддерживаются две величины: $g(v)$ — фактическая стоимость пути от $s$ до $v$ и $h(v)$ — эвристическая оценка остатка пути от $v$ до цели $t$. Очередь упорядочивается по сумме $f(v) = g(v) + h(v)$, благодаря чему поиск «вытягивается» в сторону цели, а не расходится равномерно во все стороны. + +Эвристика называется допустимой, если она не завышает истинное расстояние: $h(v) \leq h^{*}(v)$ для всех $v$. При допустимой эвристике A\* гарантированно находит оптимальный путь, при этом просматривает не больше вершин, чем алгоритм Дейкстры (который эквивалентен A\* с $h \equiv 0$). В приложении реализованы две эвристики, выбираемые пользователем: евклидово расстояние $h(v) = \sqrt{(x_v - x_t)^2 + (y_v - y_t)^2}$ — для дорожных сетей и графов с произвольной геометрией, и манхэттенское расстояние $h(v) = |x_v - x_t| + |y_v - y_t|$ — для решётчатых графов, где перемещения возможны только по горизонтали и вертикали. Обе эвристики вычисляются по координатам вершин, хранящимся в атрибутах модели графа. + +Худший случай совпадает с алгоритмом Дейкстры — $O(|E| \log |V|)$ по времени; при точной эвристике $h = h^{*}$ алгоритм раскрывает только вершины оптимального пути за $\Omega(\ell)$ операций. На дорожном графе Сормовского района (около 5 000 вершин) A\* с евклидовой эвристикой просматривает в трассировке в 3–5 раз меньше вершин, чем алгоритм Дейкстры между теми же парами перекрёстков, — это различие видно в метрике «посещено вершин» панели результатов. + +### 1.4.3 Алгоритм Флойда–Уоршелла + +Алгоритм опубликован Флойдом в 1962 году [4] и решает задачу APSP — кратчайшие пути между всеми парами вершин — методом динамического программирования по матрице расстояний. Обозначим $D^{(k)}[i][j]$ длину кратчайшего пути из $i$ в $j$, промежуточные вершины которого принадлежат множеству $\{1, \ldots, k\}$. Рекуррентное соотношение: + +$$D^{(k)}[i][j] = \min\left(D^{(k-1)}[i][j],\; D^{(k-1)}[i][k] + D^{(k-1)}[k][j]\right).$$ + +``` +Floyd_Warshall(W): + D ← W; next[i][j] ← j для каждого ребра (i, j) + for k ← 1 to n: + for i ← 1 to n: + for j ← 1 to n: + if D[i][k] + D[k][j] < D[i][j]: + D[i][j] ← D[i][k] + D[k][j] + next[i][j] ← next[i][k] +``` + +Матрица $next$ хранит первую вершину кратчайшего пути из $i$ в $j$ и позволяет восстановить любой путь за время, пропорциональное его длине. Три вложенных цикла дают $\Theta(|V|^3)$ операций независимо от плотности графа; память $O(|V|^2)$ на две матрицы. Алгоритм корректен и при отрицательных весах рёбер, если в графе нет отрицательных циклов; отрицательный цикл диагностируется появлением $D[i][i] < 0$. + +Кубическая сложность делает алгоритм рациональным только для плотных графов либо когда требуются все пары расстояний сразу. В приложении запуск Флойда–Уоршелла не требует задания источника и цели; результат — матрица расстояний — отображается в журнале шагов, а при заданных $S$ и $T$ подсвечивается восстановленный путь между ними. + +### 1.4.4 Алгоритм Беллмана–Форда + +Алгоритм разработан Беллманом и Фордом в 1955–1958 годах [21] и решает задачу SSSP при произвольных вещественных весах рёбер, включая отрицательные, при условии отсутствия достижимых отрицательных циклов. Вместо жадной фиксации вершин выполняется $|V| - 1$ волн релаксации всех рёбер подряд: после $k$-й волны корректны все кратчайшие пути, содержащие не более $k$ рёбер, а простой путь в графе из $|V|$ вершин содержит не более $|V| - 1$ рёбер. + +``` +Bellman_Ford(G, s): + for each v ∈ V: d[v] ← ∞; π[v] ← NIL + d[s] ← 0 + for i ← 1 to |V| − 1: + for each (u, v, w) ∈ E: + if d[u] + w < d[v]: + d[v] ← d[u] + w; π[v] ← u + for each (u, v, w) ∈ E: // детекция отрицательного цикла + if d[u] + w < d[v]: return NEG_CYCLE + return d, π +``` + +Дополнительный $|V|$-й проход обнаруживает отрицательные циклы: если какая-либо оценка продолжает улучшаться, цикл достижим из источника. Временная сложность $O(|V| \cdot |E|)$, память $O(|V|)$. По сравнению с алгоритмом Дейкстры Беллман–Форд медленнее и на разреженных ($O(|V|^2)$ против $O(|V|\log|V|)$), и на плотных графах ($O(|V|^3)$ против $O(|V|^2\log|V|)$), поэтому оправдан только при отрицательных весах. Распределённая версия алгоритма лежит в основе дистанционно-векторного протокола маршрутизации RIP; детекция отрицательных циклов применяется при поиске арбитражных возможностей на валютных рынках. В приложении алгоритм Беллмана–Форда реализован с поддержкой рёбер отрицательного веса и детектированием отрицательных циклов, дополняя алгоритм Дейкстры на графах, где условие $w \geq 0$ нарушено. + +## 1.5 Алгоритмы построения минимального остовного дерева + +Минимальное остовное дерево (MST) связного взвешенного неориентированного графа — подмножество рёбер, соединяющее все вершины без циклов и имеющее минимальный суммарный вес. Корректность обоих классических алгоритмов следует из свойства разреза: для любого разбиения вершин на два непустых множества ребро минимального веса, пересекающее разрез, принадлежит некоторому MST [1]. + +### 1.5.1 Алгоритм Прима + +Алгоритм опубликован Примом в 1957 году [6]. Дерево выращивается из стартовой вершины: на каждом шаге к построенному фрагменту присоединяется ребро минимального веса, ведущее к ещё не охваченной вершине. Реализация использует ту же двоичную кучу `heapq`, что и алгоритм Дейкстры, но с ключом, равным весу одного ребра, а не суммарному расстоянию от источника: + +``` +Prim(G, w, r): + for each v ∈ V: key[v] ← ∞; π[v] ← NIL + key[r] ← 0; H ← все вершины V + while H ≠ ∅: + u ← extract_min(H) + for each v ∈ Adj(u): + if v ∈ H and w(u, v) < key[v]: + key[v] ← w(u, v); π[v] ← u +``` + +Временная сложность с двоичной кучей $O((|V| + |E|)\log |V|)$, память $O(|V| + |E|)$. Алгоритм предпочтителен на плотных графах и в случаях, когда дерево требуется строить от заданной вершины. + +### 1.5.2 Алгоритм Краскала + +Алгоритм опубликован Краскалом в 1956 году [5] и обрабатывает рёбра в порядке возрастания веса: ребро добавляется в лес, если оно соединяет две разные компоненты, и отбрасывается, если образует цикл. Проверка принадлежности компонентам выполняется структурой непересекающихся множеств (DSU) с эвристиками сжатия путей и объединения по рангу: + +``` +Kruskal(G, w): + A ← ∅ + for each v ∈ V: make_set(v) + отсортировать E по неубыванию w + for each (u, v) ∈ E: + if find_set(u) ≠ find_set(v): + A ← A ∪ {(u, v)}; union(u, v) + return A +``` + +Сортировка рёбер доминирует в оценке: $O(|E| \log |E|)$; операции DSU при указанных эвристиках выполняются за почти константное амортизированное время $O(\alpha(|V|))$, где $\alpha$ — обратная функция Аккермана. На разреженных графах Краскал выигрывает у Прима за счёт меньших констант: сортировка списка рёбер и операции DSU дешевле поддержания приоритетной очереди. В приложении оба алгоритма MST не требуют задания источника и цели (для Прима стартовая вершина берётся произвольной при незаданном $S$), а построенное дерево подсвечивается на сцене зелёным цветом — тем же, что и найденный путь у алгоритмов SPSP. + +## 1.6 Сравнительный анализ реализованных алгоритмов + +Сводные характеристики девяти рассмотренных алгоритмов приведены в таблице 1.1. + +Таблица 1.1 — Сравнительные характеристики алгоритмов + +| Алгоритм | Тип задачи | Структура данных | Время (худший случай) | Память | Отриц. веса | Требует $S$/$T$ | +|---------------|-------------|----------------|---------------------|----------|------------|------------| +| BFS | SSSP (по рёбрам) | `deque` + `dict` | $O(V + E)$ | $O(V)$ | нет | да / опц. | +| DFS | обход, путь | стек + `dict` | $O(V + E)$ | $O(V)$ | нет | да / опц. | +| IDDFS | SPSP (по рёбрам) | стек + путь-множество | $O(b^{\ell})$ | $O(\ell)$ | нет | да / да | +| Дейкстра | SSSP (веса) | `heapq` + `dict` | $O((V + E)\log V)$ | $O(V + E)$ | нет ($w \geq 0$) | да / опц. | +| A\* | SPSP (веса) | `heapq` + `dict` | $O(E \log V)$ | $O(V)$ | нет ($w \geq 0$) | да / да | +| Флойд–Уоршелл | APSP | матрица $V \times V$ | $\Theta(V^3)$ | $O(V^2)$ | да (без отриц. цикла) | нет / нет | +| Беллман–Форд | SSSP + детекция цикла | список рёбер + `dict` | $O(V \cdot E)$ | $O(V)$ | да | да / опц. | +| Прим | MST | `heapq` + массив флагов | $O((V + E)\log V)$ | $O(V + E)$ | $w \geq 0$ | опц. / нет | +| Краскал | MST | сортировка + DSU | $O(E \log E)$ | $O(V + E)$ | $w \geq 0$ | нет / нет | + +Сопоставление асимптотик при характерных плотностях графа — разреженный ($|E| = O(|V|)$), средней плотности ($|E| = O(|V|\log|V|)$) и плотный ($|E| = O(|V|^2)$) — даёт следующие практические выводы. BFS и DFS линейны по размеру входа и нечувствительны к плотности. Алгоритмы Дейкстры и Прима с двоичной кучей эффективны на разреженных и средних графах ($O(|V|\log|V|)$ и $O(|V|\log^2|V|)$ соответственно), но на плотных деградируют до $O(|V|^2\log|V|)$ — хуже, чем реализация с простым массивом за $O(|V|^2)$. Флойд–Уоршелл стоит $\Theta(|V|^3)$ при любой плотности, поэтому рационален только для плотных графов или задач APSP. Краскал на разреженных графах сопоставим с Примом по асимптотике, но выигрывает по константам за счёт простоты операций DSU. Беллман–Форд — самый медленный на плотных графах ($O(|V|^3)$), однако единственный из SSSP-алгоритмов применим при отрицательных весах. + +Для основного сценария приложения — дорожные сети, у которых степень перекрёстка редко превышает четыре и $|E| \approx 2{,}5 |V|$, — оптимальны алгоритмы Дейкстры и A\* с двоичной кучей; этим объясняется выбор `heapq` как базовой структуры в их реализации. + +По результатам главы можно заключить: восемь отобранных алгоритмов покрывают все канонические постановки задачи поиска экстремального пути и взаимно дополняют друг друга по требованиям к памяти, ограничениям на веса и информированности поиска, что делает их набор достаточным и для практической маршрутизации на дорожных графах, и для учебной демонстрации стратегий обхода. + +# 2 Проектирование приложения Graphate + +## 2.1 Требования к приложению + +Разрабатываемому приложению присвоено рабочее название Graphate. По результатам анализа предметной области и аналитического обзора главы 1 сформулированы следующие функциональные требования. + +ФТ-1. Построение и редактирование графа. Поддерживаются ориентированные и неориентированные графы, мульти-рёбра, числовой атрибут «вес» и произвольные пользовательские атрибуты вершин и рёбер; вершины и рёбра добавляются и удаляются через контекстное меню сцены. + +ФТ-2. Ввод графа через матрицу смежности: элементы $A[i][j]$ интерпретируются как веса рёбер, нулевое значение — отсутствие ребра. + +ФТ-3, ФТ-4. Импорт и экспорт графа в форматах GraphML (XML-ориентированный формат, фактический стандарт обмена графовыми данными) и в собственном JSON-формате проекта. + +ФТ-5. Генерация синтетических графов по классическим вероятностным моделям: Эрдёша–Реньи, Уоттса–Строгаца, Барабаши–Альберта, прямоугольная решётка, полный граф; для каждой модели — взвешенный вариант со случайными целочисленными весами в заданном диапазоне. + +ФТ-6. Загрузка реальной дорожной сети района из OpenStreetMap: по текстовому запросу (название района или города), геокодируемому в полигон, либо по ограничивающему прямоугольнику (bounding box) в географических координатах. + +ФТ-7. Дисковый кэш загруженных OSM-районов: автоматическое сохранение результата запроса в GraphML с JSON-файлом метаданных; повторное открытие без обращения к сети. + +ФТ-8. Три режима отображения: «Граф» — абстрактная интерактивная сцена; «Граф + дерево» — сцена и синхронное дерево обхода (при $|V| \leq 40$); «Карта» — граф поверх растровой картографической подложки. + +ФТ-9. Запуск алгоритмов с пошаговой трассировкой: каждый шаг формирует снимок состояния; доступны полный прогон и пошаговое исполнение с управляемым таймлайном. + +ФТ-10. Реализованные алгоритмы: BFS, DFS, IDDFS, Дейкстра, A\* (евклидова и манхэттенская эвристики), Флойд–Уоршелл, Прим, Краскал. + +ФТ-11. Поиск как минимального, так и максимального пути: направление оптимизации выбирается в интерфейсе. + +ФТ-12, ФТ-13. Метрики алгоритма (статус, число шагов, релаксаций, посещённых вершин, время выполнения, длина и стоимость пути) и текстовый журнал шагов в правой колонке окна. + +ФТ-14. Вспомогательные диалоги: «О программе», «Подробнее об алгоритме» (теоретическая справка с псевдокодом и оценками сложности), «Пример работы алгоритма» (встроенная мини-визуализация на учебном графе из 8 вершин), «Кэш OSM», «Конфигурация» (ключ Yandex Tiles API). + +ФТ-15. Масштабирование и навигация по сцене: перетаскивание, масштабирование колесом мыши, сброс масштаба, режим «по размеру». + +ФТ-16. Мониторинг сетевых сервисов: цветовой индикатор доступности Nominatim, Overpass API, CartoDB и Yandex Tiles API в строке состояния. + +ФТ-17. Механизм отмены и повтора (Ctrl+Z / Ctrl+Y) для операций перемещения и удаления вершин; история до 50 операций. + +ФТ-18. Групповое выделение вершин прямоугольной рамкой с подсветкой пунктирным контуром, групповым перемещением и удалением по клавише Delete. + +ФТ-19. Две темы оформления — тёмная (по умолчанию) и светлая — с сохранением выбора в конфигурационном файле и адаптацией всей цветовой палитры. + +Нефункциональные требования определяют качество исполнения. НФТ-1: отзывчивость интерфейса — загрузка OSM-района, прогон алгоритма на большом графе и скачивание растровой подложки выполняются вне GUI-потока. НФТ-2: воспроизводимость — все рандомизированные генераторы принимают параметр `seed`, одинаковые параметры дают идентичные графы. НФТ-3: автономный режим — закэшированные районы открываются без сети. НФТ-4: читаемость на больших графах — подписи вершин и веса рёбер скрываются при сильном отдалении, применяются уровни детализации и кэширование пиксмапов. НФТ-5: целевая платформа Windows 10/11; единственный ОС-специфичный вызов — окраска заголовка окна. НФТ-6: диагностика — длительные операции пишут структурированные сообщения уровня INFO в консоль и журнал шагов. НФТ-7: конфигурируемость — порог слияния близких OSM-узлов и тип сети по умолчанию задаются переменными окружения без перекомпиляции. + +## 2.2 Выбор языка программирования и библиотек + +Язык программирования оценивался по пяти критериям: доступность экосистемы (готовые библиотеки для графов, геоданных, GUI и научных вычислений), кроссплатформенность и простота установки на Windows без компиляторов, скорость разработки, читаемость кода (алгоритмы должны быть понятны при чтении исходников — приложение учебное) и производительность, достаточная для графов в тысячи вершин. Выбран Python 3.11: все необходимые библиотеки (NetworkX, PySide6, OSMnx, contextily, rasterio) доступны готовыми пакетами под Windows; интерпретатор версии 3.11 на 10–25 % быстрее предыдущих, что заметно при прогоне алгоритмов на дорожных графах; современный синтаксис аннотаций типов (`list[int]`, `X | None`) делает код самодокументируемым. + +К библиотеке хранения и обработки графов предъявлялись требования, продиктованные предметной областью. OSM-узлы идентифицируются 64-битными целыми (`osmid`), учебные графы — буквенными метками, поэтому библиотека обязана принимать любой хешируемый тип как ключ вершины. Каждой вершине назначаются координаты, метка и состояние трассировки, каждому ребру — вес, название улицы и категория дороги, поэтому нужны произвольные бессхемные атрибуты. Дополнительно требуются: установка на Windows одной командой, совместимость с OSMnx (библиотека возвращает граф в формате конкретного графового пакета), чтение и запись GraphML, встроенные генераторы синтетических графов и лицензия, не обязывающая публиковать исходный код приложения. Результаты сопоставления четырёх библиотек приведены в таблице 2.1. + +Таблица 2.1 — Сравнение библиотек для работы с графами + +| Критерий | NetworkX | igraph | graph-tool | pydot | +|------------------------|----------|----------|------------|----------| +| Произвольные атрибуты вершин и рёбер | да | частично | частично | нет | +| Произвольные идентификаторы вершин | да | нет | нет | нет | +| Установка на Windows одной командой | да | да | нет (сборка C++) | да | +| Интеграция с OSMnx | да | нет | нет | нет | +| Чтение и запись GraphML | да | да | нет | нет | +| Генераторы ER, WS, BA, решётка, $K_n$ | да | да | да | нет | +| Лицензия | BSD-3 | GPL-2 | GPL-3 | MIT | + +Выбран NetworkX [7]: только он сочетает произвольные хешируемые идентификаторы вершин, бессхемные словарные атрибуты, полный набор генераторов, формат GraphML, нативную интеграцию с OSMnx и свободную лицензию BSD-3. Библиотека graph-tool превосходит NetworkX по скорости на графах свыше $10^5$ вершин, но официально не поддерживается на Windows и требует ручной сборки C++-ядра, что нарушает требование о простоте развёртывания. + +GUI-фреймворк должен отображать интерактивную сцену с тысячами элементов: при загрузке реального OSM-квартала граф содержит 3 000–10 000 вершин и 8 000–30 000 рёбер, и частота кадров при навигации не должна падать ниже 30. Отсюда критерии: встроенная 2D-сцена с пространственным индексом (ручная отрисовка примитивами потребовала бы собственной реализации hit-testing и выборочной перерисовки), нативный вид на Windows 10/11, многопоточность без блокировки GUI, наличие backend для matplotlib (режим карты) и лицензия без обязательной публикации исходников. Сопоставление приведено в таблице 2.2. + +Таблица 2.2 — Сравнение GUI-фреймворков + +| Критерий | PySide6 (Qt 6) | Tkinter | PyQt5 | Electron/JS | +|------------------------|------------|----------|------------|----------| +| 2D-сцена с BSP-индексом | да (Graphics View) | нет | да | нет | +| Свыше 10 000 элементов на сцене | да (LOD, кэш) | нет | да | частично | +| Нативный вид на Windows 10/11 | да | устаревший | да | нет | +| Фоновые потоки (`QThread`) | да | сложно | да | да | +| Backend matplotlib | да (QTAgg) | да | да | нет | +| Лицензия | LGPL | в составе Python | GPL/коммерческая | MIT | + +Выбран PySide6 — официальная привязка Qt 6 от The Qt Company под лицензией LGPL. Решающим аргументом стал фреймворк Graphics View [10]: классы `QGraphicsView` и `QGraphicsScene` предоставляют готовый BSP-индекс сцены, кэширование пиксмапов, уровни детализации и выборочное обновление; реализация собственного движка с этими свойствами была бы сопоставима по трудоёмкости со всем остальным приложением. PyQt5 функционально эквивалентен, но распространяется под GPL либо коммерческой лицензией. + +Вспомогательные библиотеки: OSMnx [8] — конвейер «геокодирование → загрузка → построение графа» для OpenStreetMap; matplotlib [9] — отрисовка дорожного графа в режиме карты; contextily — загрузка растровых тайлов подложки; rasterio [18] — репроекция растра между картографическими проекциями; NumPy — операции над массивами пикселей. + +## 2.3 Выбор внешних картографических сервисов + +Геокодирование текстового запроса («Сормовский район, Нижний Новгород») в полигон выполняет сервис Nominatim [11] — официальный геокодер проекта OpenStreetMap. Альтернативы (Google Geocoding API, Яндекс Геокодер, HERE) платны либо требуют регистрации и ключа API, что усложнило бы развёртывание учебного приложения; Nominatim бесплатен, не требует ключа, покрывает весь мир и нативно интегрирован в OSMnx. + +Векторные данные улично-дорожной сети загружаются через Overpass API [12] — единственный сервис, отдающий сырые данные OpenStreetMap [13] (перекрёстки с координатами, улицы с длиной, категорией и направлением движения) в открытом формате, пригодном для построения взвешенного графа. Проприетарные сети HERE и TomTom распространяют данные по коммерческим лицензиям и не поддерживаются OSMnx. + +Растровая подложка режима карты по умолчанию загружается с CartoDB Dark Matter [14]: тёмный монохромный дизайн не конкурирует визуально с цветными вершинами и маршрутами, нанесёнными поверх, а сервис допускает до 8 параллельных соединений против 2 у публичного тайл-сервера OSM, что ускоряет сборку подложки библиотекой contextily. В качестве альтернативного источника поддерживается Yandex Tiles API; его ключ вводится через диалог «Настройки → Конфигурация» и хранится в локальном файле конфигурации пользователя, не попадая в репозиторий проекта. + +## 2.4 Архитектура приложения + +Приложение реализовано как автономное настольное программное обеспечение для Windows 10/11. Код разделён на четыре пакета-слоя: + +``` +app/ +├── main.py # точка входа, настройка логирования +├── network_reachability.py # проверка сетевых сервисов +├── core/ # ядро: модель графа, трассировка, алгоритмы +│ ├── graph_model.py +│ ├── trace.py +│ └── algorithms/ # bfs, dfs, iddfs, dijkstra, astar, +│ # floyd_warshall, prim, kruskal (+ base) +├── data/ # источники данных +│ ├── generators.py # синтетические графы +│ ├── importers.py # GraphML, JSON, матрица смежности +│ ├── exporters.py # GraphML, JSON +│ └── osm_loader.py # загрузка из OpenStreetMap, кэш +├── gui/ # интерфейс (PySide6): главное окно, сцена, +│ # панели, диалоги, темы, undo-команды +├── config/ +│ └── user_settings.py # локальный JSON-конфиг пользователя +└── workers/ # фоновые потоки QThread +``` + +Зависимости между слоями однонаправленные: `gui` использует `core`, `data` и `workers`; `workers` использует `core` и `data`; `core` зависит только от стандартной библиотеки и NetworkX. Благодаря этому алгоритмы тестируются изолированно, без запуска интерфейса (подраздел 3.6), а замена GUI-слоя не затронула бы ядро. + +Поток данных при типовом сценарии выглядит так. Пользователь запрашивает район — `gui` создаёт фоновый поток `OsmGraphThread`, который через `data/osm_loader` обращается к Nominatim и Overpass API, строит `GraphModel` и возвращает его сигналом в GUI-поток. Пользователь запускает алгоритм — `AlgorithmRunThread` прогоняет выбранный класс алгоритма на копии модели и возвращает результат с трассировкой; сцена и панели обновляются по снимкам трассировки. Все переходы между потоками выполняются только сигналами Qt, что исключает гонки данных. + +## 2.5 Модель графа и система трассировки + +Внутреннее представление графа — класс `GraphModel`, надстройка над `networkx.MultiDiGraph`. Мульти-ориентированный граф выбран по трём причинам: реальные OSM-данные содержат параллельные дороги между одной парой перекрёстков (разделённые шоссе), которые представляются мульти-рёбрами; неориентированный граф воспроизводится парой встречных дуг, что унифицирует реализацию алгоритмов; ядро не придётся переписывать при расширении поддержки ориентированных графов. + +`GraphModel` наследует `QObject` и при изменениях эмитирует сигналы `vertexAdded`, `vertexRemoved`, `edgeAdded`, `edgeRemoved`, `attributesChanged`, `graphReset` — сцена обновляется инкрементально, перерисовывая только затронутые элементы. Канонические атрибуты вершины: `label`, координаты сцены `x`, `y`, географические координаты `lat`, `lon`, состояние `state` (определяет цвет при трассировке), `community_id`. Канонические атрибуты ребра: `weight`, `state`, `label`, OSM-название улицы `name` и категория дороги `highway`. + +Пошаговое исполнение построено на паре классов `Trace` и `Snapshot`. Каждый алгоритм в ходе работы формирует упорядоченную последовательность снимков; снимок — неизменяемый `dataclass` с полями: `description` (человекочитаемое описание шага), `vertex_states` и `edge_states` (изменённые состояния вершин и рёбер), `vertex_labels` (обновлённые подписи — оценки $d[v]$, $f[v]$), `metrics` (шаги, релаксации, посещённые вершины, время) и `payload` (предшественники, расстояния, рёбра MST — данные для дерева обхода). Коллекция `Trace` хранит все снимки и курсор активного шага: таймлайн и кнопки «шаг вперёд/назад» лишь перемещают курсор, алгоритм не перезапускается, поэтому навигация мгновенна даже на трассировках в тысячи шагов. + +Абстрактный базовый класс `AlgorithmBase` задаёт единый контракт: метод `run(model, source, target)` возвращает `AlgorithmResult` со статусом (DONE, NO_PATH, ERROR), трассировкой, найденным путём, его стоимостью и, для алгоритмов MST, списком рёбер дерева. Восемь алгоритмов реализованы независимыми классами-наследниками; добавление нового алгоритма сводится к реализации одного класса и его регистрации в панели алгоритмов. + +Выводы по главе. Сформулированы 19 функциональных и 7 нефункциональных требований; по результатам сравнительного анализа выбран стек Python 3.11 + NetworkX + PySide6 с внешними сервисами Nominatim, Overpass API и CartoDB; спроектирована четырёхслойная архитектура с однонаправленными зависимостями, событийной моделью графа и системой трассировки на неизменяемых снимках, обеспечивающей пошаговую визуализацию без перезапуска алгоритмов. + +# 3 Программная реализация и тестирование + +## 3.1 Источники данных и ввод графа + +Модуль `app/data/generators.py` реализует пять генераторов синтетических графов на основе NetworkX (таблица 3.1). Все генераторы принимают параметр `weighted` (случайные целочисленные веса в заданном диапазоне) и `seed`, обеспечивающий воспроизводимость: одинаковые параметры дают идентичные графы, что используется и в учебных демонстрациях, и в модульных тестах. + +Таблица 3.1 — Генераторы синтетических графов + +| Модель | Параметры | Назначение | +|--------------------|------------|------------------------------| +| Эрдёша–Реньи $G(n, p)$ | $n$, $p$, seed | случайный граф, универсальный учебный пример | +| Уоттса–Строгаца $WS(n, k, p)$ | $n$, $k$, $p$, seed | модель «малого мира», демонстрация кластеризации | +| Барабаши–Альберта $BA(n, m)$ | $n$, $m$, seed | безмасштабный граф с нерегулярными степенями | +| Прямоугольная решётка | rows, cols | регулярная геометрия, естественный полигон для A\* с манхэттенской эвристикой | +| Полный граф $K_n$ | $n$ | поведение алгоритмов на плотных графах | + +Ввод через матрицу смежности (ФТ-2) реализован в импортёре `app/data/importers.py`: пользователь открывает JSON-файл с двумерным массивом весов либо вводит рёбра списком пар `[u, v]` или троек `[u, v, weight]`; импортёр нормализует оба представления к единому виду. Файловый обмен поддерживает два формата: GraphML — файлы, экспортированные из приложения, открываются в Gephi и yEd без конвертации — и собственный JSON-формат с полями `directed`, `nodes`, `edges`, удобный для ручного редактирования и версионирования. + +Загрузка дорожной сети из OpenStreetMap выполняется библиотекой OSMnx по двум сценариям (ФТ-6): по текстовому запросу, который Nominatim геокодирует в полигон, после чего Overpass API возвращает все улицы внутри полигона, — либо по координатам ограничивающего прямоугольника. Полученный мульти-граф конвертируется в `GraphModel`; близкорасположенные перекрёстки сливаются с порогом 12 м (настраивается переменной окружения `DIPLOM_OSM_MERGE_M`) — без слияния развязки с дублированными узлами OSM искажали бы счётчики вершин и картину трассировки. Результат автоматически сохраняется в дисковый кэш `samples/osm_cache/osm_.graphml` с JSON-файлом метаданных; диалог «Кэш OSM» показывает сохранённые записи с исходным запросом, типом сети и датой создания и позволяет открыть запись без сети, переименовать или удалить её. + +## 3.2 Главное окно и режимы отображения + +Главное окно (`MainWindow`) служит диспетчером компонентов. Сверху расположены меню (Файл, Правка, Граф, Вид, Настройки, Справка) и панель инструментов с быстрым созданием типовых графов и переключателем режимов. Центральный виджет работает в одном из трёх режимов: «Граф» — интерактивная сцена `QGraphicsView`; «Граф + дерево» — сцена и синхронное дерево обхода (доступен при $|V| \leq 40$, на больших графах дерево вырождается в нечитаемую структуру); «Карта» — дорожный граф поверх растровой подложки, отрисованный средствами matplotlib. Правая колонка содержит вкладки «Метрики» (числовые показатели и журнал шагов) и «Легенда» (расшифровка цветовых состояний). Нижняя панель — таймлайн: слайдер по истории снимков, кнопки навигации и регулятор скорости воспроизведения. Строка состояния показывает масштаб, маркеры источника и цели, счётчики вершин и рёбер и индикатор сетевых сервисов. + +Источник $S$ и цель $T$ задаются контекстным меню вершины либо двойным кликом (первый — источник, второй — цель) и отображаются жёлтым и красным кольцом соответственно. Для алгоритмов Флойда–Уоршелла, Прима и Краскала задание $S$ и $T$ не обязательно — панель алгоритмов активирует кнопку запуска по фактическим требованиям выбранного алгоритма. + +Каждому алгоритму соответствуют два справочных диалога (ФТ-14): «Подробнее об алгоритме» — HTML-страница с псевдокодом, инвариантом, оценками сложности и условиями применимости — и «Пример работы алгоритма» — модальный диалог со встроенной мини-сценой, на которой алгоритм автоматически прогоняется на учебном графе из 8 вершин, а пользователь управляет просмотром теми же кнопками таймлайна, что и в главном окне. + +## 3.3 Интерактивное редактирование графа + +Перетаскивание вершин мышью построено на штатном механизме Qt Graphics View: каждый `VertexItem` получает флаги `ItemIsMovable`, `ItemIsSelectable` и `ItemSendsGeometryChanges`. Метод `itemChange()` перехватывает два события: при `ItemPositionChange` (позиция ещё не изменилась) на всех инцидентных рёбрах вызывается `prepareGeometryChange()` — без этого при быстром движении на сцене оставались бы артефакты от старых положений рёбер; при `ItemPositionHasChanged` новые координаты записываются в модель через `set_vertex_attrs()`, что сигналом `attributesChanged` обновляет связанные элементы. Нажатие средней кнопки мыши включает панорамирование сцены, не задевающее вершины. + +Групповое выделение (ФТ-18) использует режим `QGraphicsView.RubberBandDrag`: левая кнопка на пустом месте сцены рисует прямоугольную рамку; вершина считается выделенной при пересечении рамкой любой части её формы (`Qt.IntersectsItemShape` — естественнее, чем требование полного охвата). Выделенные вершины подсвечиваются пунктирным голубым контуром (#29B6F6, толщина 3 пикселя) и перемещаются группой; клавиша Delete после диалога подтверждения удаляет их вместе с инцидентными рёбрами. + +Отмена и повтор (ФТ-17) реализованы по паттерну «команда» на классах `QUndoStack` / `QUndoCommand`. Команда `MoveVerticesCommand` хранит словарь перемещений `{vertex_id: (old_x, old_y, new_x, new_y)}` — групповое перемещение отменяется одним Ctrl+Z; первый вызов `redo()` пропускается, поскольку перемещение уже выполнено пользователем. Команда `DeleteVerticesCommand` хранит атрибуты удалённых вершин и рёбер и при отмене восстанавливает их в исходном порядке. Стек ограничен 50 операциями; пункты меню «Отменить»/«Повторить» автоматически дополняются названием последней команды («Отменить Перемещение 3 вершин»). После любого undo/redo трассировка текущего алгоритма сбрасывается, чтобы цветовые состояния не противоречили изменившейся топологии. + +## 3.4 Темы оформления + +Семантическая палитра приложения централизована в модуле `app/gui/styles.py`: функции `vertex_color(state)`, `edge_color(state)`, `marker_color(kind)` и `canvas_scene_background()` возвращают цвета в зависимости от активной темы, и все графические элементы обращаются только к ним — переключение темы не требует перестроения дерева виджетов. + +Тёмная тема (по умолчанию) выдержана в плоском стиле инженерных IDE: фон окна и сцены — тёмно-синий (#0F0F18 / #1A1A2E); палитра состояний — зелёный #06A77D «путь / MST», оранжевый #FF8C42 «открытые вершины», красный #E63946 «тупик», жёлтый #FFD23F «источник». Светлая тема адаптирует схему к белому фону сцены: «источник» отображается тёмно-янтарным #C47A00 вместо плохо различимого на белом жёлтого, «цель» — тёмно-красным #C0392B; метки весов рёбер рисуются белым текстом на тёмной подложке и остаются читаемыми при любом фоне. + +На Windows 10/11 цвет заголовка окна согласуется с темой через Desktop Window Manager: функция `apply_window_titlebar_theme()` вызывает `DwmSetWindowAttribute()` с атрибутом `DWMWA_USE_IMMERSIVE_DARK_MODE` через `ctypes` после события `showEvent`, когда дескриптор окна уже существует; на других ОС вызов пропускается. Выбранная тема сохраняется в пользовательский конфигурационный файл и восстанавливается при следующем запуске. + +## 3.5 Фоновые задачи и мониторинг сетевых сервисов + +Тяжёлые операции вынесены в подклассы `QThread` с переопределённым методом `run()` (таблица 3.2); результат или ошибка возвращаются в GUI-поток сигналами `finished_ok` / `run_failed`, на время работы потока соответствующие элементы интерфейса блокируются. + +Таблица 3.2 — Фоновые потоки приложения + +| Поток | Операция | +|--------------------|--------------------------------------| +| `OsmGraphThread` | геокодирование, загрузка Overpass, конвертация, слияние вершин | +| `AlgorithmRunThread` | прогон алгоритма на копии модели, передача результата в GUI | +| `MapBasemapThread` | загрузка растровых тайлов, репроекция EPSG:3857 → EPSG:4326 | +| `ReachabilityProbeThread` | HTTP-проверка внешних сервисов с таймаутом 5 с | + +Алгоритм прогоняется на копии модели: даже если пользователь редактирует граф во время длительного прогона, фоновое исполнение остаётся согласованным. + +Приложение зависит от четырёх внешних сервисов — Nominatim (геокодирование), Overpass API (загрузка дорожного графа), CartoDB (подложка) и Yandex Tiles API (альтернативная подложка). Чтобы пользователь видел причину сбоя загрузки до её возникновения, в строке состояния реализован индикатор подключения: каждые 2 минуты поток `ReachabilityProbeThread` выполняет лёгкие HTTP-запросы ко всем сервисам и окрашивает индикатор зелёным (доступен), красным (недоступен) или серым (не проверяется — например, Yandex Tiles при отсутствии ключа). Первая проверка запускается через 400 мс после открытия окна, внеочередная — сразу после сохранения ключа API в диалоге «Конфигурация». + +## 3.6 Тестирование + +Однонаправленная архитектура позволила покрыть ядро модульными тестами без запуска интерфейса. Набор `tests/` на pytest включает тесты обходов (`test_bfs_dfs.py`, `test_iddfs.py`), алгоритмов кратчайших путей (`test_dijkstra.py`, `test_floyd_warshall.py`), остовных деревьев (`test_kruskal.py`), выделения сообществ (`test_louvain.py`), импорта JSON (`test_read_json.py`) и OSM-конвейера (`test_osm_loader.py`, `test_osm_merge_vertices.py`). Корректность алгоритмов проверяется на эталонных графах сопоставлением с результатами референсных реализаций NetworkX: найденный путь, его стоимость и множество рёбер MST должны совпадать; отдельные тесты фиксируют поведение в граничных случаях — несвязный граф (статус NO_PATH), совпадение источника и цели, рёбра с бесконечным весом. + +Работа приложения целиком проверялась вручную по сценариям функциональных требований: генерация каждого типа синтетического графа, ввод матрицы смежности, импорт и экспорт GraphML/JSON, загрузка района «Сормовский район, Нижний Новгород» по запросу и по bbox, повторное открытие из кэша без сети, прогон всех восьми алгоритмов в режимах минимального и максимального пути с проверкой метрик и журнала, переключение тем, отмена и повтор операций редактирования, поведение индикатора сервисов при отключённой сети. + +## 3.7 Сборка исполняемого файла + +Приложение компилируется в самодостаточный исполняемый файл `graphate.exe` инструментом PyInstaller 6; конфигурация сборки описана в файле `graphate.spec`, точка входа — `graphate_entry.py`. Сборка выполняется одной командой: + +``` +python -m PyInstaller --noconfirm --clean graphate.spec +``` + +Результат размещается в `dist/graphate/graphate.exe` вместе с необходимыми DLL и данными и запускается на Windows 10/11 без установки Python и дополнительных пакетов, что закрывает требование автономного развёртывания. + +Выводы по главе. Реализованы все источники данных (генераторы, матрица смежности, GraphML/JSON, OpenStreetMap с дисковым кэшем), трёхрежимный интерфейс с пошаговым таймлайном, интерактивное редактирование с undo/redo и групповыми операциями, две темы оформления, фоновое исполнение тяжёлых операций и мониторинг внешних сервисов; ядро покрыто модульными тестами, приложение собрано в автономный исполняемый файл. + +# Заключение + +В ходе выполнения выпускной квалификационной работы разработано настольное приложение Graphate для определения путей экстремальной длины на графах. Получены следующие результаты. + +1. Проведён аналитический обзор девяти алгоритмов поиска экстремальных путей и построения остовных деревьев (BFS, DFS, IDDFS, Дейкстры, A\*, Флойда–Уоршелла, Беллмана–Форда, Прима, Краскала) с оценками временной и пространственной сложности, анализом зависимости от плотности графа и сводной сравнительной таблицей. Показано, что для разреженных дорожных сетей ($|E| \approx 2{,}5|V|$) оптимальны алгоритмы Дейкстры и A\* на двоичной куче. + +2. По результатам сравнительного анализа по явным критериям выбран технологический стек: Python 3.11, NetworkX (единственная библиотека с произвольными идентификаторами и атрибутами вершин, интеграцией с OSMnx и лицензией BSD-3), PySide6 (готовая 2D-сцена с BSP-индексом для графов в десятки тысяч элементов), OSMnx, contextily, rasterio, matplotlib; внешние сервисы — Nominatim, Overpass API, CartoDB Dark Matter с опциональным Yandex Tiles API. + +3. Спроектирована четырёхслойная архитектура (интерфейс, ядро, источники данных, фоновые задачи) с однонаправленными зависимостями; ядро построено на событийной модели `GraphModel` поверх `networkx.MultiDiGraph` и системе трассировки из неизменяемых снимков, обеспечивающей навигацию по шагам алгоритма без его перезапуска. + +4. Реализованы восемь алгоритмов с единым программным интерфейсом и пошаговой трассировкой, поиск как минимального, так и максимального пути, генераторы синтетических графов (Эрдёша–Реньи, Уоттса–Строгаца, Барабаши–Альберта, решётка, полный граф), ввод через матрицу смежности, импорт и экспорт GraphML и JSON, загрузка дорожных сетей из OpenStreetMap с дисковым кэшем, режим карты с растровой подложкой, теоретические справки и интерактивные примеры для каждого алгоритма. + +5. Реализованы механизм отмены и повтора операций редактирования на основе `QUndoStack`, групповое выделение вершин прямоугольной рамкой, переключаемые тёмная и светлая темы оформления с адаптацией палитры и заголовка окна, мониторинг доступности внешних сервисов в строке состояния. + +6. Алгоритмическое ядро покрыто модульными тестами на pytest со сверкой против референсных реализаций NetworkX; функциональные требования проверены ручными сценариями, включая загрузку и маршрутизацию на реальной дорожной сети района Нижнего Новгорода. Приложение скомпилировано в автономный исполняемый файл `graphate.exe` (PyInstaller 6), не требующий установленной среды Python. + +Возможные направления развития: реализация двунаправленного поиска и алгоритма Джонсона для APSP на разреженных графах; ускорение прогона на графах свыше $10^5$ вершин переносом критичных участков на компилируемые расширения; поддержка временных (time-dependent) весов рёбер для маршрутизации с учётом расписаний; экспорт трассировки в видеофайл для использования в лекционных материалах. + +# Список источников и литературы + +1. Cormen T. H., Leiserson C. E., Rivest R. L., Stein C. Introduction to Algorithms. 3rd ed. — Cambridge: MIT Press, 2009. — 1312 p. +2. Dijkstra E. W. A Note on Two Problems in Connexion with Graphs // Numerische Mathematik. — 1959. — Vol. 1. — P. 269–271. +3. Hart P. E., Nilsson N. J., Raphael B. A Formal Basis for the Heuristic Determination of Minimum Cost Paths // IEEE Transactions on Systems Science and Cybernetics. — 1968. — Vol. 4, № 2. — P. 100–107. +4. Floyd R. W. Algorithm 97: Shortest Path // Communications of the ACM. — 1962. — Vol. 5, № 6. — P. 345. +5. Kruskal J. B. On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem // Proceedings of the American Mathematical Society. — 1956. — Vol. 7, № 1. — P. 48–50. +6. Prim R. C. Shortest Connection Networks and Some Generalizations // Bell System Technical Journal. — 1957. — Vol. 36, № 6. — P. 1389–1401. +7. Hagberg A. A., Schult D. A., Swart P. J. Exploring Network Structure, Dynamics, and Function using NetworkX // Proceedings of the 7th Python in Science Conference (SciPy 2008). — 2008. — P. 11–15. +8. Boeing G. OSMnx: New Methods for Acquiring, Constructing, Analyzing, and Visualizing Complex Street Networks // Computers, Environment and Urban Systems. — 2017. — Vol. 65. — P. 126–139. +9. Hunter J. D. Matplotlib: A 2D Graphics Environment // Computing in Science & Engineering. — 2007. — Vol. 9, № 3. — P. 90–95. +10. Graphics View Framework // Qt 6 Documentation. — URL: https://doc.qt.io/qt-6/graphicsview.html (дата обращения: 17.05.2026). +11. Nominatim Documentation. — URL: https://nominatim.org/release-docs/latest/ (дата обращения: 17.05.2026). +12. Overpass API // OpenStreetMap Wiki. — URL: https://wiki.openstreetmap.org/wiki/Overpass_API (дата обращения: 17.05.2026). +13. Haklay M., Weber P. OpenStreetMap: User-Generated Street Maps // IEEE Pervasive Computing. — 2008. — Vol. 7, № 4. — P. 12–18. +14. CARTO Basemaps. — URL: https://carto.com/basemaps/ (дата обращения: 17.05.2026). +15. Pavlopoulos G. A. et al. Empirical Comparison of Visualization Tools for Larger-Scale Network Analysis // Advances in Bioinformatics. — 2017. — Art. 1278932. +16. Святов К. В., Щукарев И. А. Визуализация онтологического графа с использованием библиотеки pydot Python // Вестник кибернетики. — 2026. — Т. 25, № 1. — С. 64–69. +17. Amure R., Agarwal N. A Comparative Evaluation of Social Network Analysis Tools // Social Network Analysis and Mining. — 2025. — Vol. 15, № 1. +18. Gillies S. et al. Rasterio: Geospatial Raster I/O for Python Programmers. — URL: https://rasterio.readthedocs.io/ (дата обращения: 17.05.2026). +19. Алексеев В. Е., Захарова Д. В. Теория графов: учебное пособие. — Нижний Новгород: Изд-во ННГУ, 2017. — 119 с. +20. Бутарев К. В., Павлов Д. И. Введение в динамическое программирование. — М.: МПГУ, 2024. — 144 с. +21. Bellman R. On a Routing Problem // Quarterly of Applied Mathematics. — 1958. — Vol. 16, № 1. — P. 87–90. +22. Левитин А. В. Алгоритмы: введение в разработку и анализ. — М.: Вильямс, 2006. — 576 с.