EduBrick

Графы: определения и хранение

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

4 мин

Граф — это множество вершин и множество рёбер между ними. Проще всего думать о нём как о карте: вершины — города, рёбра — дороги.

Обозначения стандартные: nn — число вершин, mm — число рёбер. Почти все оценки в теме записываются через них.

Термины

Ориентированное ребро проходится в одну сторону, неориентированное — в обе. Дорога с односторонним движением против обычной.

Кратные рёбра — два и более ребра между одной парой вершин. Петля — ребро из вершины в неё же. И то, и другое ломает наивные реализации, поэтому в условии на это смотрят отдельно.

Степень вершины deg(v)\deg(v) — количество её соседей. Сумма степеней всех вершин равна 2m2m: каждое ребро считается дважды.

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

Компонента связности — максимальный по включению связный кусок.

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

Полный граф — граф, где есть все возможные рёбра. Их n(n1)2\frac{n(n-1)}{2} — это и есть максимум для графа без петель и кратных рёбер.

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

vector<vector<char>> g(n, vector<char>(n, 0));
for (int i = 0; i < m; i++) {
    int u, v;
    cin >> u >> v;
    g[u][v] = 1;
    g[v][u] = 1;   // только для неориентированного
}

У неориентированного графа матрица симметрична относительно главной диагонали, у ориентированного — вообще говоря, нет.

Память — O(n2)O(n^2). При n=105n = 10^5 это 101010^{10} ячеек, то есть матрица применима только при nn примерно до пяти тысяч.

Зато проверка «есть ли ребро между uu и vv» стоит константу — единственная операция, где матрица выигрывает.

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

Для каждой вершины храним вектор её соседей.

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

Память — O(n+m)O(n + m). Обход всех соседей вершины — O(degv)O(\deg v), а обход всех соседей всех вершин — O(n+m)O(n + m), потому что каждое ребро встретится дважды.

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

Список рёбер

struct Edge { int u, v, weight; };
vector<Edge> edges(m);

Память — O(m)O(m), минимальная из трёх. Но соседей вершины приходится искать проходом по всему списку, то есть за O(m)O(m).

Формат кажется бесполезным, и почти всегда так и есть. Пригождается он ровно в двух местах: алгоритм Форда—Беллмана и алгоритм Краскала — там нужен именно перебор всех рёбер, а не соседей.

Обратите внимание на struct вместо pair<int, pair<int, int>>: как только у ребра появляется третье поле, пары становятся нечитаемыми. Подробнее — в статье про свои структуры.

Сравнение

операция матрица список смежности список рёбер
память O(n2)O(n^2) O(n+m)O(n + m) O(m)O(m)
добавить ребро O(1)O(1) O(1)O(1) O(1)O(1)
удалить ребро O(1)O(1) O(degv)O(\deg v) O(m)O(m)
есть ли ребро uuvv O(1)O(1) O(degv)O(\deg v) O(m)O(m)
перебрать соседей vv O(n)O(n) O(degv)O(\deg v) O(m)O(m)

Правило выбора: список смежности, если ограничения не говорят обратного. Матрица — когда nn мало (до 1000–2000) и нужны частые проверки наличия ребра. Список рёбер — когда алгоритм сам просит перебор рёбер.

Как граф обычно дают в условии

Чаще всего — списком рёбер: сначала nn и mm, потом mm строк по паре вершин. Читают его при этом сразу в список смежности, как в коде выше.

Реже — матрицей из нулей и единиц. Ещё реже — списком соседей для каждой вершины.

Отдельная ловушка: нумерация вершин. В условиях почти всегда с единицы, в коде удобнее с нуля. Выберите одно и держитесь: смешение — источник ошибок, которые не видны на первом тесте, потому что нулевая вершина обычно тоже существует.