Что забыли Графы в программировании?

Для начала уточню: граф Монте‑Кристо и прочие персонажи тут ни при чём. Речь пойдёт о математических графах — структуре, которая помогает решать массу задач в программировании, математике и олимпиадной информатике.

Графы — это способ представить объекты и связи между ними. Они идеально подходят для поиска маршрутов, анализа сетей и моделирования любых систем, где важны отношения между элементами.

Впервые я встретил графы примерно в четвёртом классе, но начал активно использовать, только когда начал заниматься олимпиадным программированием.

В этой статье я расскажу вам про:

  • Основные элементы графа

  • Виды графов

  • Представление графов на примере кода C++

Граф и его элементы

Графом в математике называют абстрактную систему, состоящую из двух множеств — множеством вершин и множеством рёбер (связей между ними).

Пример графа
Пример графа

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

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

  1. Степенью данной вершины — это количество входящих/выходящих рёбер из данной вершины.

  2. Путь в графах — это последовательное множество вершин такое, что каждые две соседние вершины, соединены ребром.

  3. Вес — это числовое значение, присваемое ребру. Оно может представлять собой стоимость или расстояние от точки начала до точки конца.

  4. Цикл — это последовательность вершин, в которой каждая соседняя пара соединена ребром, все вершины различны, кроме первой и последней.

  5. Связность графа — это свойство, при котором между любой парой вершин существует путь. Если это выполняется, граф называют связным.

Виды графов

Графы могут различаться по своим свойствам. Эти различия определяют, как с ними работать и какие задачи они могут решать. Ниже — основные типы графов, которые важно знать.

Полный граф

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

Это означает, что в данном графе каждая вершина соединена со всеми остальными вершинами и нет отсутствия ребер или нескольких ребер, соединяющих одинаковые вершины. Представьте, что в из каждого города ведут дороги в каждый другой, но между городом A и городом B есть только одна дорога.

Пример полного графа
Пример полного графа

Количество рёбер в полном графе считается просто.
Пусть есть n вершин. Каждая вершина соединена со всеми остальными, то есть из одной вершины выходит n − 1 рёбер (степень каждой вершины). Если просто перемножить, получим n * (n − 1), но это число учитывает каждое неориентированное ребро дважды — один раз от первой вершины, и второй раз от второй. Получим формулу:

\frac{n(n-1)}{2}

Ориентированный граф

Ориентированным графом называют граф, ребра которого имеют направление от одной вершины к другой.

Неформально, можно сказать, что если вершины A идет ребро в вершину B, то это означает, что из A в B можно добраться по нему, но необязательно, что из B можно добраться в A . В реальной жизни аналогом такого ребра может служить односторонняя дорога.

Пример ориентированного графа
Пример ориентированного графа

В примере выше из вершины A невозможно попасть куда-либо, так как в нее не выходит ни одно ребро. Вершин C и D соединены двумя ребрами, благодаря чему из C можно попасть в D и наоборот.

В таком графе важны понятия:

  • Входящая степень (сколько рёбер входит в вершину);

  • Исходящая степень (сколько выходит);

  • Источник (вершина с нулевой входящей степенью);

  • Сток (вершина с нулевой исходящей степенью).

Циклы в ориентированном графе могут существовать только, если движение по всем рёбрам совпадает с направлением.

Вместо обычной связности, тут появляются термины:

  • Слабо связный граф — если, убрав направления у рёбер, граф становится связным.

  • Сильно связный граф — если из любой вершины можно добраться до любой другой, соблюдая направление рёбер.

Взвешенный граф

Взвешенным графом, называют граф, в котором каждому ребру присвоено вес.

Например, представьте карту Московской области. На ней есть города (вершины) и дороги между ними (рёбра). У каждой дороги указана её длина — это и есть вес ребра. Такая карта как раз и представляет собой взвешенный граф.

Пример взвешенного графа
Пример взвешенного графа

Вес играет ключевую роль, потому что именно от него зависит выбор оптимального пути — будь то минимальное расстояние, наименьшая стоимость или минимальное время.

Дерево

Деревом, называется связанный неориентированный граф без циклов.

Примером из жизни может служить структура папок в файловой системе: из любой папки есть путь к родительской, циклов нет, и из корневой можно добраться до любой вложенной.

Пример дерева (оно необязательно должно выглядеть именно так)
Пример дерева (оно необязательно должно выглядеть именно так)

Важно отметить несколько свойства такого графа:

  • Граф содержит n вершин и ровно n - 1 рёбер;

  • Добавление любого ребра создаёт цикл;

  • Удаление любого ребра нарушает связность;

  • У дерева нет циклов;

  • Между двумя вершинами всегда существует единственный путь.

Деревья обеспечивают быстрый поиск, эффективное хранение данных и хорошую логическую структуру.

Представление графов

Для работы с графами в программе необходимо их как-то хранить. В этом разделе будут представлены три основных способа. Каждый из них имеет свою асимптотику и удобен по-своему.

В программировании буквами u, v  признано записывать вершины, под буквой w - вес.

Список рёбер

Список рёбер — это представление графа, в котором хранятся все рёбра в виде набора пар или троек числе. Каждое ребро записывается в виде (u, v) для невзвешенного графа или (u, v, w) для взвешенного соответственно. Его просто хранить, и оно хорошо обрабатывается глобально.

Такое представление удобно в работе с именно рёбрами, а не со структурой соседей (связанных вершин).

Пример чтения ориентированного взвешенного графа в виде списка рёбер на C++:

int n, m; // количество вершин и рёбер
cin >> n >> m;

vector<tuple<int,int,int>> edges; // рёбра

for (int = 0; i < m; i++){
  int u, v, w;
  cin >> u >> v >> w;
  edges.push_back({u, v, w});
}

Память: O(m)

Список смежностей

Список смежностей — это представление графа, где для каждой вершины хранится список её соседей (иногда с их весами) (u_i: (v_1, w_1),..., (v_j, w_j)). Стандарт для большинства алгоритмов.

Отлично подходит для алгоритмов требующих частых обходов, поисков путей и работы с соседями вершины.

Пример чтения ориентированного взвешенного графа в виде списка смежностей на C++:

int n, m;
cin >> n >> m;

vector<vector<pair<int,int>>> g(n+1); // для 1 .. n вершины (v, w)

for (int i = 0; i < m; i++){
  int u, v, w;
  cin >> u >> v >> w;
  g[u].push_back({v, w}); // u -> (v, w)
  
}

Память: O(n + m)

Матрица смежностей

Матрица смежностей — это представление графа с помощью квадратной таблицы размером n * n, где n — количество вершин. Для элемента матрицы a_{ij} существует два значения: 1 (во взвешенном графе будет равен w), если есть ребро i → j, иначе 0.

Если граф небольшой и много рёбер, данное представление наиболее удобное и эффективное.

Пример чтения ориентированного взвешенного графа в виде матрицы смежностей на C++:

int n, m;
cin >> n >> m;

vector<vector<int>> g(n+1, vector<int>(n+1, INF)); 

for (int i = 1; i <= n; i++) g[i][i] = 0; // отсутсвует ребро из v в v


for (int i = 0; i < m; i++){
  int u, v, w;
  cin >> u >> v >> w;
  g[u][v] = w;
  
}

Память: O(n^2)

В этой статье я постарался максимально понятно объяснить основную теорию по графам для программистов. В следующей статье я поведаю об основных алгоритмах и их примененьям.

Для более точных, с математической стороны, определений всех понятий рекомендую обратиться к википедии и другим более научным статьям.