Введение в графы: терминология и способы представления (матрица смежности, списки смежности)
Введение в графы: терминология и способы представления
Добро пожаловать на курс «Алгоритмы теории графов на C++». Мы начинаем наше путешествие с фундаментальных понятий. Графы — это не просто абстрактная математическая структура, это скелет современного цифрового мира. Социальные сети, навигаторы, интернет-маршрутизация, зависимости программных пакетов — всё это графы.
В этой статье мы разберем, из чего состоит граф, какие виды графов бывают и, самое главное для программиста, как эффективно хранить их в памяти компьютера, используя язык C++.
Что такое граф?
В простейшем понимании граф — это набор точек, соединенных линиями. В математике и информатике мы используем более строгую терминологию.
Формально граф определяется как пара множеств:
Где — сам граф, — множество вершин (от англ. vertex), а — множество ребер (от англ. edge), соединяющих эти вершины.
Основные термины
Чтобы говорить на одном языке, определим ключевые понятия:
- Вершина (Vertex/Node) — это фундаментальная единица графа (точка на схеме). В C++ мы часто обозначаем их целыми числами () для удобства индексации.
- Ребро (Edge) — это связь между двумя вершинами. Если ребро соединяет вершины и , мы говорим, что они смежны.
- Степень вершины — количество ребер, инцидентных (подсоединенных) данной вершине.
Классификация графов
Графы бывают разными, и выбор алгоритма часто зависит от типа графа.
1. Ориентированные и неориентированные
- Неориентированный граф: Ребра не имеют направления. Связь между и работает в обе стороны (как рукопожатие). Если вы можете пройти от к , то можете и от к .
- Ориентированный граф (орграф): Ребра имеют направление (стрелки). Связь не означает наличие связи (как подписка в социальной сети: вы подписаны на знаменитость, но она на вас — нет).
2. Взвешенные и невзвешенные
- Невзвешенный граф: Все ребра равнозначны. Нам важно только наличие связи.
- Взвешенный граф: Каждому ребру приписано число — вес (стоимость, длина, пропускная способность). Например, в навигаторе весом ребра может быть расстояние между перекрестками или время в пути.
3. Циклические и ациклические
Если, двигаясь по ребрам, можно вернуться в стартовую вершину, не проходя по одному ребру дважды (в обратном направлении для неориентированных), то в графе есть цикл. Графы без циклов часто называют деревьями (если они связны) или лесами.
Способы представления графа в C++
Компьютер не может «видеть» рисунок графа. Нам нужно перевести визуальную структуру в структуру данных. Существует два основных способа хранения графа: матрица смежности и списки смежности. Выбор зависит от плотности графа и ограничений по памяти.
Матрица смежности (Adjacency Matrix)
Матрица смежности — это двумерный массив (таблица) размером , где — количество вершин.
Пусть у нас есть матрица . Значение ячейки определяет наличие ребра между вершиной и вершиной .
- Для невзвешенного графа: (или
true), если ребро есть, и (илиfalse), если нет. - Для взвешенного графа: в ячейке хранится вес ребра. Отсутствие ребра обычно обозначается специальным значением (например, бесконечностью или -1).
Рассмотрим пример реализации на C++:
#include <iostream>
#include <vector>
// Пример создания матрицы смежности для 5 вершин
int main() {
int n = 5; // Количество вершин
// Создаем матрицу n x n, заполненную нулями
std::vector<std::vector<int>> adjMatrix(n, std::vector<int>(n, 0));
// Добавляем ребро между вершиной 0 и 1 (неориентированное)
adjMatrix[0][1] = 1;
adjMatrix[1][0] = 1;
// Добавляем ребро между 1 и 4
adjMatrix[1][4] = 1;
adjMatrix[4][1] = 1;
// Проверка смежности
if (adjMatrix[0][1] == 1) {
std::cout << "Вершины 0 и 1 смежны" << std::endl;
}
return 0;
}
Анализ сложности:
Потребление памяти составляет:
Где — объем занимаемой памяти, — асимптотическая оценка («О» большое), а — количество вершин в графе.
Плюсы:
- Проверка наличия ребра между двумя вершинами происходит мгновенно за , где означает константное время.
- Простота реализации.
Минусы:
- Требует много памяти. Если у вас 10 000 вершин, матрица займет около 400 МБ, даже если ребер почти нет.
- Перебор всех соседей вершины занимает , так как нужно пройти по всей строке матрицы.
Списки смежности (Adjacency Lists)
Это наиболее часто используемый способ представления в алгоритмическом программировании. Для каждой вершины мы храним список вершин, с которыми она соединена.
В C++ для этого идеально подходит массив (или вектор) векторов: std::vector<std::vector<int>>.
Реализация на C++:
#include <iostream>
#include <vector>
int main() {
int n = 5; // Количество вершин
// Вектор векторов. adjList[i] хранит список соседей вершины i
std::vector<std::vector<int>> adjList(n);
// Добавляем ребро 0-1 (неориентированное)
adjList[0].push_back(1);
adjList[1].push_back(0);
// Добавляем ребро 1-4
adjList[1].push_back(4);
adjList[4].push_back(1);
// Перебор соседей вершины 1
std::cout << "Соседи вершины 1: ";
for (int neighbor : adjList[1]) {
std::cout << neighbor << " ";
}
std::cout << std::endl;
return 0;
}
Для взвешенного графа мы можем хранить пары (сосед, вес):
std::vector<std::vector<std::pair<int, int>>> adjList;
Анализ сложности:
Потребление памяти:
Где — память, — количество вершин, а — количество ребер.
Плюсы:
- Экономия памяти для разреженных графов (где ребер мало).
- Перебор соседей вершины занимает время, пропорциональное их количеству (степени вершины), что намного быстрее в разреженных графах.
Минусы:
- Проверка наличия ребра между и не мгновенна, нужно пробежать по списку соседей .
Сравнение и выбор представления
Чтобы выбрать правильную структуру, нужно оценить плотность графа.
Плотность определяется отношением количества ребер к максимально возможному количеству ребер. Максимальное количество ребер в неориентированном графе:
Где — максимальное число ребер, а — число вершин.
| Характеристика | Матрица смежности | Списки смежности |
|---|---|---|
| Память | ||
| Проверка смежности | ||
| Перебор всех соседей | ||
| Добавление вершины | Сложно ( при пересоздании) | Легко ( амортизировано) |
| Удаление ребра |
«Если граф разреженный (ребер мало), используйте списки смежности. Если граф плотный (ребер почти ) или вершин очень мало — матрица смежности может быть удобнее». — Общее правило олимпиадного программирования.
Заключение
Мы познакомились с базовой терминологией теории графов и двумя основными способами их представления в C++. Матрица смежности хороша для быстрых проверок связей в небольших или плотных графах. Списки смежности — это стандарт де-факто для большинства алгоритмов, так как они экономят память и позволяют эффективно обходить граф.
В следующих статьях мы научимся обходить эти структуры, используя поиск в глубину (DFS) и поиск в ширину (BFS).