Создание графа на Python с примерами кода

Как реализовать граф на python

Содержание статьи

Как реализовать граф на python

Графы – это структуры данных, состоящие из узлов (вершин) и рёбер, связывающих их. В Python для работы с графами чаще всего используют библиотеки NetworkX, igraph и graph-tool. NetworkX – самый популярный выбор для задач средней сложности благодаря простоте API и поддержке базовых алгоритмов (поиск в ширину, кратчайший путь, компоненты связности). Для высокопроизводительных вычислений на больших графах (от 100 000 вершин) лучше подходит graph-tool, написанная на C++ с биндингами для Python.

При выборе инструмента учитывайте объём данных и требования к скорости. Например, NetworkX хранит граф в виде словаря словарей, что удобно для небольших графов (до 10 000 вершин), но приводит к замедлению на больших объёмах. Для визуализации NetworkX интегрируется с Matplotlib и PyVis, но для интерактивных графов с тысячами узлов используйте Plotly или специализированные инструменты вроде Gephi.

Типичные задачи при работе с графами: построение сетевых моделей, анализ социальных сетей, оптимизация маршрутов. Например, алгоритм Дейкстры в NetworkX реализуется одной строкой: nx.dijkstra_path(G, source, target). Для взвешенных графов с отрицательными рёбрами используйте алгоритм Беллмана-Форда (nx.bellman_ford_path). Если граф динамический (часто изменяется), рассмотрите Dask для параллельных вычислений или Snap.py для графов с миллионами вершин.

Пример минимального графа на NetworkX:

import networkx as nx
G = nx.Graph()
G.add_edge("A", "B", weight=4)
G.add_edge("B", "C", weight=2)

Для хранения графов в файлах используйте форматы GraphML (XML-подобный, поддерживает атрибуты) или GEXF (расширяемый, совместим с Gephi). При сериализации больших графов выбирайте бинарные форматы вроде GML или Pickle, но помните о проблемах безопасности при загрузке недоверенных данных.

Выбор структуры данных для представления графа в Python

Выбор структуры данных для представления графа в Python

В Python графы чаще всего реализуют через списки смежности, матрицы смежности или словари словарей. Списки смежности (list[list] или dict[list]) оптимальны для разреженных графов: потребляют память пропорционально числу рёбер (O(V + E)), позволяют быстро перебирать соседей вершины. Для плотных графов (где E ≈ V²) эффективнее матрицы смежности (list[list] или numpy.ndarray) – доступ к ребру за O(1), но расход памяти O(V²). Словари словарей (dict[dict]) удобны для взвешенных графов с нечисловыми метками вершин, так как позволяют хранить веса рёбер в виде {вершина: вес}.

Для динамических графов с частыми изменениями структуры (добавление/удаление вершин и рёбер) используйте классы с инкапсуляцией логики. Пример: class Graph: def __init__(self): self.adj = defaultdict(list) – упрощает валидацию данных и скрывает реализацию. Если граф ориентированный и требуется быстрая проверка наличия ребра, добавьте set для хранения рёбер: self.edges = set(). Для алгоритмов, работающих с путями (например, Дейкстры), храните веса в отдельной структуре, например, dict[tuple, float], где ключ – кортеж (u, v).

Реализация ненаправленного графа с помощью списков смежности

Ненаправленный граф в Python удобно представлять через словарь, где ключи – вершины, а значения – списки смежных узлов. Для добавления ребра между вершинами `u` и `v` достаточно добавить `v` в список `u` и наоборот: `graph[u].append(v)` и `graph[v].append(u)`. Это гарантирует симметричность связей, а время доступа к смежным вершинам остаётся O(1). Пример: граф с рёбрами `(1, 2)`, `(2, 3)`, `(3, 1)` реализуется как `{1: [2, 3], 2: [1, 3], 3: [1, 2]}`. Для проверки наличия ребра используйте `v in graph[u]`, что работает за O(k), где `k` – степень вершины `u`.

При динамическом изменении графа (удаление рёбер, добавление вершин) следите за согласованностью данных: удаление ребра требует синхронного обновления обоих списков смежности. Для оптимизации памяти используйте `defaultdict(list)` из модуля `collections` – это избавит от необходимости проверять существование ключа перед добавлением. Если граф взвешенный, храните кортежи `(вершина, вес)` вместо простых вершин, например: `{1: [(2, 5), (3, 1)], 2: [(1, 5), (3, 2)]}`.

Добавление и удаление вершин и рёбер в направленном графе

Добавление и удаление вершин и рёбер в направленном графе

В направленном графе операции с вершинами и рёбрами требуют учёта ориентации. Для реализации используют структуры данных, где вершины хранятся в виде ключей словаря, а рёбра – в виде вложенных словарей или списков смежности. Пример базовой структуры: {'A': {'B': 5, 'C': 3}, 'B': {}}, где ключи – исходящие вершины, а значения – словарь с целевыми вершинами и весами рёбер.

Добавление вершины сводится к созданию нового ключа в словаре графа. Если вершина уже существует, операция игнорируется или перезаписывает данные. Пример кода для добавления вершины 'D':

graph = {'A': {'B': 5}, 'B': {}}
graph['D'] = {}  # Добавляет вершину без рёбер

Удаление вершины требует удаления всех входящих и исходящих рёбер. Сначала перебирают все вершины графа и удаляют ссылки на целевую вершину из их списков смежности. Затем удаляют саму вершину. Пример:

def remove_vertex(graph, vertex):
for v in list(graph.keys()):
if vertex in graph[v]:
del graph[v][vertex]
del graph[vertex]

Добавление направленного ребра включает указание исходной и целевой вершины, а также веса (если граф взвешенный). Если вершины не существуют, их предварительно добавляют. Пример для ребра A → D с весом 2:

graph.setdefault('A', {})['D'] = 2

Удаление ребра требует проверки его существования. Если ребро есть, его удаляют из словаря исходящей вершины. Пример для ребра A → B:

if 'A' in graph and 'B' in graph['A']:
del graph['A']['B']

Для массовых операций используют циклы. Например, добавление нескольких рёбер из списка кортежей [('A', 'C', 4), ('B', 'D', 1)]:

edges = [('A', 'C', 4), ('B', 'D', 1)]
for src, dst, weight in edges:
graph.setdefault(src, {})[dst] = weight

При работе с большими графами оптимизируют доступ к данным. Хранение рёбер в отдельном множестве или словаре ускоряет проверку их существования. Таблица сравнения методов хранения:

Метод Добавление ребра Удаление ребра Поиск ребра
Словарь смежности O(1) O(1) O(1)
Список смежности O(1) O(n) O(n)
Матрица смежности O(1) O(1) O(1)

Визуализация графа с использованием библиотеки NetworkX и Matplotlib

Визуализация графа с использованием библиотеки NetworkX и Matplotlib

NetworkX и Matplotlib – связка для быстрой визуализации графов без сложных зависимостей. NetworkX генерирует структуру данных, а Matplotlib отрисовывает её с минимальными настройками. Для базовой визуализации достаточно двух строк кода:

  • import networkx as nx – импорт библиотеки для работы с графами;
  • import matplotlib.pyplot as plt – импорт модуля для отрисовки;
  • nx.draw(G, with_labels=True) – отображение графа G с подписями узлов.

Этот подход подходит для графов до 100 узлов. При большем количестве элементы накладываются друг на друга, требуя дополнительных параметров.

Настройка внешнего вида графа выполняется через параметры функции nx.draw(). Ключевые аргументы:

  • node_size – размер узлов (по умолчанию 300, для больших графов уменьшайте до 50–100);
  • node_color – цвет узлов (строка или список значений, например, node_color=['red', 'blue']);
  • edge_color – цвет рёбер (поддерживает градиенты через plt.cm.viridis);
  • width – толщина рёбер (можно задать список для разных значений).

Пример настройки для графа с 50 узлами:

nx.draw(G,
node_size=80,
node_color='skyblue',
edge_color='gray',
width=0.5,
alpha=0.7)

Параметр alpha регулирует прозрачность, полезен для плотных графов.

Для сложных графов используйте компоновки (layouts), чтобы избежать наложений. NetworkX предоставляет несколько алгоритмов:

  1. spring_layout – симуляция физической системы (узлы отталкиваются, рёбра притягивают);
  2. circular_layout – узлы располагаются по окружности;
  3. kamada_kawai_layout – минимизация энергии системы (хорош для деревьев);
  4. random_layout – случайное размещение (для тестирования).

Пример применения компоновки:

pos = nx.spring_layout(G, k=0.15, iterations=50)
nx.draw(G, pos, with_labels=True)

Параметр k регулирует расстояние между узлами, iterations – количество итераций симуляции.

Подписи узлов и рёбер настраиваются отдельно. Для узлов используйте labels в nx.draw(), для рёбер – nx.draw_networkx_edge_labels(). Пример с весами рёбер:

edge_labels = nx.get_edge_attributes(G, 'weight')
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels)

Если граф направленный, добавьте arrows=True в nx.draw(). Для выделения подграфов используйте nx.draw_networkx_nodes() и nx.draw_networkx_edges() с разными параметрами.

Сохранение графа в файл выполняется через plt.savefig(). Поддерживаемые форматы: PNG, SVG, PDF. Рекомендации по экспорту:

  • Для векторной графики используйте format='svg' или format='pdf';
  • Разрешение PNG задавайте через dpi=300;
  • Обрезайте поля с помощью bbox_inches='tight';
  • Для прозрачного фона добавьте transparent=True.

Пример сохранения:

plt.savefig('graph.png', dpi=300, bbox_inches='tight')

После сохранения очищайте холст командой plt.clf(), чтобы избежать наложения графиков при повторном вызове.

Поиск кратчайшего пути между узлами алгоритмом Дейкстры

Алгоритм Дейкстры решает задачу поиска кратчайшего пути в графе с неотрицательными весами рёбер. Его временная сложность – O((V + E) log V) при реализации с приоритетной очередью (кучей), где V – количество вершин, E – рёбер. Для графов с плотной структурой (E ≈ V²) эффективнее использовать массив расстояний с линейным поиском минимума, что даёт O(V²).

Ключевая идея: на каждом шаге выбирается вершина с минимальным текущим расстоянием от стартовой, после чего обновляются расстояния до её соседей. Для корректной работы алгоритма веса рёбер должны быть неотрицательными – иначе используйте алгоритм Беллмана-Форда. Пример графа для тестирования: вершины A, B, C, D с рёбрами A→B (4), A→C (2), B→C (1), B→D (5), C→D (8).

Реализация на Python требует структуры данных для хранения графа. Оптимальный выбор – словарь смежности: {'A': {'B': 4, 'C': 2}, 'B': {'C': 1, 'D': 5}, 'C': {'D': 8}, 'D': {}}. Для приоритетной очереди подойдёт heapq из стандартной библиотеки. Инициализируйте массив расстояний бесконечностью, кроме стартовой вершины (0), и очередь пар (расстояние, вершина).

На каждом шаге извлекайте вершину с минимальным расстоянием. Если текущее расстояние до неё больше сохранённого – пропустите шаг. Иначе для каждого соседа проверьте: если новое расстояние (текущее + вес ребра) меньше сохранённого, обновите его и добавьте в очередь. Пример обновления для вершины C из A: distance['C'] = min(distance['C'], distance['A'] + 2).

Для восстановления пути храните предков каждой вершины в отдельном словаре. При обновлении расстояния записывайте текущую вершину как предка соседа. После завершения алгоритма путь восстанавливается обратным проходом от конечной вершины к стартовой. Пример: если предки {'B': 'A', 'C': 'B', 'D': 'B'}, кратчайший путь A→D – A→B→D с длиной 9.

Алгоритм не работает с отрицательными весами, но допускает модификации для динамических графов. Если веса рёбер меняются редко, используйте инкрементальные обновления: пересчитывайте только затронутые пути. Для графов с миллионами вершин оптимизируйте хранение данных – используйте numpy для массивов расстояний или специализированные библиотеки вроде graph-tool.

Сохранение и загрузка графа в файл с помощью JSON и Pickle

Сохранение и загрузка графа в файл с помощью JSON и Pickle

Для сериализации графов в Python используют json и pickle, но их возможности отличаются. JSON подходит для хранения структур с базовыми типами данных (списки, словари, строки, числа), но не поддерживает объекты классов или пользовательские структуры без дополнительной обработки. Например, граф на основе словаря {'A': ['B', 'C']} сохранится без проблем, но если узлы – экземпляры класса Node, потребуется преобразование в примитивы. Pickle же сохраняет объекты Python "как есть", включая методы и пользовательские классы, но файлы получаются нечитаемыми и несовместимыми с другими языками.

Пример сохранения графа в JSON:

  • Импортируйте json и преобразуйте граф в сериализуемый формат:
    import json
    graph = {'A': ['B', 'C'], 'B': ['A', 'D']}
    with open('graph.json', 'w') as f:
    json.dump(graph, f)
  • Для загрузки используйте json.load(). Если граф содержит объекты, реализуйте методы default и object_hook для кастомной сериализации.
  • Ограничения JSON: не сохраняет порядок ключей в Python < 3.7, не поддерживает кортежи (преобразуются в списки).

Pickle эффективен для сложных графов с объектами, но требует осторожности. Пример:

  1. Сохраните граф с помощью pickle.dump():
    import pickle
    class Node:
    def __init__(self, name):
    self.name = name
    self.edges = []
    node_a = Node('A')
    node_b = Node('B')
    node_a.edges.append(node_b)
    with open('graph.pkl', 'wb') as f:
    pickle.dump(node_a, f)
  2. Загрузите данные через pickle.load(). Учтите: pickle уязвим к выполнению произвольного кода при загрузке ненадежных файлов – используйте только проверенные источники.
  3. Для совместимости между версиями Python добавляйте protocol=pickle.HIGHEST_PROTOCOL при сохранении.

Вопрос-ответ:

Ссылка на основную публикацию