1. Введение в графы: терминология и способы представления (матрица смежности, списки смежности)
Введение в графы: терминология и способы представления
Добро пожаловать на курс «Алгоритмы теории графов на C++». Мы начинаем наше путешествие с фундаментальных понятий. Графы — это не просто абстрактная математическая структура, это скелет современного цифрового мира. Социальные сети, навигаторы, интернет-маршрутизация, зависимости программных пакетов — всё это графы.
В этой статье мы разберем, из чего состоит граф, какие виды графов бывают и, самое главное для программиста, как эффективно хранить их в памяти компьютера, используя язык C++.
Что такое граф?
В простейшем понимании граф — это набор точек, соединенных линиями. В математике и информатике мы используем более строгую терминологию.
!Пример простого неориентированного графа с 5 вершинами.
Формально граф определяется как пара множеств:
Где — сам граф, — множество вершин (от англ. vertex), а — множество ребер (от англ. edge), соединяющих эти вершины.
Основные термины
Чтобы говорить на одном языке, определим ключевые понятия:
* Вершина (Vertex/Node) — это фундаментальная единица графа (точка на схеме). В C++ мы часто обозначаем их целыми числами () для удобства индексации. * Ребро (Edge) — это связь между двумя вершинами. Если ребро соединяет вершины и , мы говорим, что они смежны. * Степень вершины — количество ребер, инцидентных (подсоединенных) данной вершине.
Классификация графов
Графы бывают разными, и выбор алгоритма часто зависит от типа графа.
1. Ориентированные и неориентированные
* Неориентированный граф: Ребра не имеют направления. Связь между и работает в обе стороны (как рукопожатие). Если вы можете пройти от к , то можете и от к . * Ориентированный граф (орграф): Ребра имеют направление (стрелки). Связь не означает наличие связи (как подписка в социальной сети: вы подписаны на знаменитость, но она на вас — нет).
!Сравнение неориентированного и ориентированного графов.
2. Взвешенные и невзвешенные
* Невзвешенный граф: Все ребра равнозначны. Нам важно только наличие связи. * Взвешенный граф: Каждому ребру приписано число — вес (стоимость, длина, пропускная способность). Например, в навигаторе весом ребра может быть расстояние между перекрестками или время в пути.
3. Циклические и ациклические
Если, двигаясь по ребрам, можно вернуться в стартовую вершину, не проходя по одному ребру дважды (в обратном направлении для неориентированных), то в графе есть цикл. Графы без циклов часто называют деревьями (если они связны) или лесами.
Способы представления графа в C++
Компьютер не может «видеть» рисунок графа. Нам нужно перевести визуальную структуру в структуру данных. Существует два основных способа хранения графа: матрица смежности и списки смежности. Выбор зависит от плотности графа и ограничений по памяти.
Матрица смежности (Adjacency Matrix)
Матрица смежности — это двумерный массив (таблица) размером , где — количество вершин.
Пусть у нас есть матрица . Значение ячейки определяет наличие ребра между вершиной и вершиной .
* Для невзвешенного графа: (или true), если ребро есть, и (или false), если нет.
* Для взвешенного графа: в ячейке хранится вес ребра. Отсутствие ребра обычно обозначается специальным значением (например, бесконечностью или -1).
Рассмотрим пример реализации на C++:
Анализ сложности:
Потребление памяти составляет:
Где — объем занимаемой памяти, — асимптотическая оценка («О» большое), а — количество вершин в графе.
Плюсы: * Проверка наличия ребра между двумя вершинами происходит мгновенно за , где означает константное время. * Простота реализации.
Минусы: * Требует много памяти. Если у вас 10 000 вершин, матрица займет около 400 МБ, даже если ребер почти нет. * Перебор всех соседей вершины занимает , так как нужно пройти по всей строке матрицы.
Списки смежности (Adjacency Lists)
Это наиболее часто используемый способ представления в алгоритмическом программировании. Для каждой вершины мы храним список вершин, с которыми она соединена.
В C++ для этого идеально подходит массив (или вектор) векторов: std::vector<std::vector<int>>.
!Визуализация структуры списков смежности.
Реализация на C++:
Для взвешенного графа мы можем хранить пары (сосед, вес):
std::vector<std::vector<std::pair<int, int>>> adjList;
Анализ сложности:
Потребление памяти:
Где — память, — количество вершин, а — количество ребер.
Плюсы: * Экономия памяти для разреженных графов (где ребер мало). * Перебор соседей вершины занимает время, пропорциональное их количеству (степени вершины), что намного быстрее в разреженных графах.
Минусы: * Проверка наличия ребра между и не мгновенна, нужно пробежать по списку соседей .
Сравнение и выбор представления
Чтобы выбрать правильную структуру, нужно оценить плотность графа.
Плотность определяется отношением количества ребер к максимально возможному количеству ребер. Максимальное количество ребер в неориентированном графе:
Где — максимальное число ребер, а — число вершин.
| Характеристика | Матрица смежности | Списки смежности | | :--- | :--- | :--- | | Память | | | | Проверка смежности | | | | Перебор всех соседей | | | | Добавление вершины | Сложно ( при пересоздании) | Легко ( амортизировано) | | Удаление ребра | | |
> «Если граф разреженный (ребер мало), используйте списки смежности. Если граф плотный (ребер почти ) или вершин очень мало — матрица смежности может быть удобнее». — Общее правило олимпиадного программирования.
Заключение
Мы познакомились с базовой терминологией теории графов и двумя основными способами их представления в C++. Матрица смежности хороша для быстрых проверок связей в небольших или плотных графах. Списки смежности — это стандарт де-факто для большинства алгоритмов, так как они экономят память и позволяют эффективно обходить граф.
В следующих статьях мы научимся обходить эти структуры, используя поиск в глубину (DFS) и поиск в ширину (BFS).