1. Введение в графы: терминология и способы представления (матрица смежности, списки смежности)
Введение в графы: терминология и способы представления
Добро пожаловать в курс «Алгоритмы на графах и их реализация в C++». Мы начинаем наше путешествие с фундаментальных понятий. Графы — это не просто абстрактная математическая структура, это скелет современного цифрового мира. Социальные сети, навигаторы, интернет-маршрутизация, нейронные сети и даже структура молекул — всё это описывается языком графов.
В этой статье мы разберем, из чего состоят графы, какими они бывают и, самое главное для программиста, как эффективно хранить их в памяти компьютера, используя язык C++.
Что такое граф?
Если говорить простым языком, граф — это набор точек и линий, соединяющих эти точки. В компьютерных науках мы используем более строгую терминологию:
* Вершины (Vertices/Nodes) — это те самые «точки» или объекты. Например, города на карте или пользователи в социальной сети. * Рёбра (Edges) — это связи между вершинами. Например, дороги между городами или дружба между пользователями.
!Схематичное изображение графа с вершинами и рёбрами.
Формальное определение
С точки зрения математики, граф определяется как пара множеств:
Где — сам граф, — множество вершин, а — множество рёбер, соединяющих эти вершины.
Количество вершин обычно обозначают как , а количество рёбер — как . В контексте алгоритмической сложности мы часто будем опускать модуль и писать просто и .
Основные виды графов
Прежде чем писать код, важно понять, с каким типом графа мы имеем дело. От этого зависит выбор структуры данных.
1. Ориентированные и неориентированные графы
* Неориентированный граф: Связь работает в обе стороны. Если существует ребро между вершиной и вершиной , то можно пройти как от к , так и от к . Пример:* Рукопожатие или дружба в Facebook (если я у тебя в друзьях, то и ты у меня). * Ориентированный граф (орграф): Рёбра имеют направление (стрелки). Ребро позволяет пройти от к , но не обязательно обратно. Пример:* Подписка в Instagram (я подписан на Илона Маска, но он на меня — нет) или одностороннее движение на дороге.
2. Взвешенные и невзвешенные графы
* Невзвешенный граф: Все рёбра равнозначны. Нам важно лишь наличие связи. * Взвешенный граф: Каждое ребро имеет числовое значение — вес. Пример:* Расстояние между городами в километрах, стоимость проезда или пропускная способность интернет-кабеля.
3. Связность
Граф называется связным, если между любой парой вершин существует путь. Если граф распадается на несколько изолированных кусков, эти куски называются компонентами связности.
Способы представления графа в C++
Главный вопрос инженера: «Как запихнуть эту структуру в память компьютера?». Существует два основных способа: Матрица смежности и Списки смежности. Выбор зависит от того, насколько плотным является ваш граф.
Матрица смежности (Adjacency Matrix)
Матрица смежности — это двумерный массив (таблица) размером .
Пусть у нас есть вершины, пронумерованные от до . Элемент матрицы matrix[i][j] хранит информацию о ребре между вершиной и вершиной .
* Для невзвешенного графа: matrix[i][j] = 1, если ребро есть, и 0, если его нет.
* Для взвешенного графа: matrix[i][j] = weight, где weight — вес ребра.
!Иллюстрация соответствия графа и его матрицы смежности.
#### Реализация на C++
#### Анализ сложности
* Память: Где — количество вершин. Мы всегда выделяем память под полный квадрат, даже если рёбер почти нет. * Проверка связи: Мы можем мгновенно узнать, есть ли ребро между и , просто обратившись к ячейке массива. * Перебор соседей: Чтобы найти всех соседей вершины, нужно пройтись по всей строке матрицы.
Списки смежности (Adjacency Lists)
Этот способ чаще используется в реальных задачах. Идея проста: для каждой вершины мы храним список только тех вершин, с которыми она соединена напрямую.
В C++ это обычно реализуется как массив (или вектор) векторов: vector<vector<int>>.
* adj[0] содержит список соседей вершины 0.
* adj[1] содержит список соседей вершины 1.
* И так далее.
!Визуализация списков смежности: массив, где каждый элемент ссылается на список соседей.
#### Реализация на C++
#### Анализ сложности
* Память: Где — количество вершин, а — количество рёбер. Мы храним только существующие связи. Это очень экономно для разреженных графов (где мало рёбер). * Проверка связи: Где — количество соседей у вершины. В худшем случае (полный граф) это , но на практике быстрее. * Перебор соседей: Мы итерируемся только по реальным соседям, не просматривая пустые ячейки.
Сравнение: что выбрать?
Выбор зависит от плотности графа. Плотность определяется отношением количества рёбер к максимально возможному количеству рёбер.
| Характеристика | Матрица смежности | Списки смежности | | :--- | :--- | :--- | | Расход памяти | Высокий (). Плохо для больших графов. | Низкий (). Оптимально для большинства задач. | | Проверка наличия ребра | Мгновенно (). | Медленнее (нужно искать в списке). | | Добавление новой вершины | Дорого () — пересоздание матрицы. | Дешево () — добавление вектора. | | Удаление ребра | Мгновенно (). | Медленнее (поиск и удаление из списка). |
> «Если у вас 10 000 городов и всего 20 000 дорог, матрица смежности займет 100 000 000 ячеек (около 400 МБ int-ов), большинство из которых будут нулями. Списки смежности займут лишь память под 20 000 связей. Разница колоссальна.»
Итог
Мы познакомились с базовой терминологией теории графов и двумя основными способами их реализации в C++.
В следующей статье мы научимся «ходить» по этим структурам, изучив алгоритмы обхода графа в глубину (DFS) и в ширину (BFS).