EduBrick

Компоненты сильной связности

Алгоритм Косарайю: два обхода и транспонированный граф. Почему работает именно эта комбинация и ни одна другая.

5 мин

В ориентированном графе вершины uu и vv называются сильно связанными, если есть путь из uu в vv и путь из vv в uu. Иначе говоря, они лежат на общем цикле.

Это отношение эквивалентности: если uu связана с vv, а vv с ww, то uu связана с ww — пути просто склеиваются. Значит, вершины разбиваются на компоненты сильной связности, и каждая вершина попадает ровно в одну.

Конденсация

Сожмём каждую компоненту в одну вершину и оставим рёбра между разными компонентами. Получится конденсация.

Конденсация — всегда DAG.

Будь в ней цикл, все компоненты этого цикла оказались бы взаимно достижимы и должны были слиться в одну.

Отсюда стандартная схема решения задач на ориентированных графах: решить задачу внутри каждой компоненты (там всё взаимно достижимо, обычно это просто), построить конденсацию и решить оставшееся на DAG — где работают топологическая сортировка и динамика.

Ключевая лемма

Пусть AA и BB — разные компоненты, и есть ребро из AA в BB. Тогда

maxvAtoutv>maxvBtoutv\max_{v \in A} tout_v > \max_{v \in B} tout_v

Максимальное время выхода в компоненте-источнике больше, чем в компоненте-приёмнике.

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

Обход первым попал в BB. Из BB в AA пути нет — иначе компоненты слились бы. Значит, этот запуск обойдёт всю BB и не заденет AA. Все времена выхода в BB проставятся раньше, чем хоть одно в AA.

Обход первым попал в AA. Из любой вершины AA достижима вся AA и, по ребру, вся BB. Значит, этот же запуск обойдёт и AA, и BB целиком. Вершина AA, в которую обход вошёл первой, выйдет последней из всех — её вызов объемлет все остальные.

В обоих случаях максимум по AA больше.

Транспонированный граф

Обозначим GTG^T граф, в котором все рёбра развёрнуты.

Компоненты сильной связности GG и GTG^T совпадают. Путь из uu в vv в одном соответствует пути из vv в uu в другом, поэтому взаимная достижимость сохраняется. А новых слияний не происходит: конденсация — DAG, а перевёрнутый DAG остаётся DAG.

Строится GTG^T за один проход:

vector<vector<int>> gt(n);
for (int v = 0; v < n; v++)
    for (int to : g[v]) gt[to].push_back(v);

Алгоритм Косарайю

  1. Обход по GG, собираем вершины в порядке выхода.
  2. Идём по этому списку в обратном порядке (то есть по убыванию touttout) и из каждой непосещённой вершины запускаем обход по GTG^T. Каждый такой запуск красит ровно одну компоненту.
vector<int> order, comp;
vector<char> used;

void dfs1(int v) {
    used[v] = 1;
    for (int to : g[v]) if (!used[to]) dfs1(to);
    order.push_back(v);
}

void dfs2(int v, int c) {
    comp[v] = c;
    for (int to : gt[v]) if (comp[to] == -1) dfs2(to, c);
}

// в main:
used.assign(n, 0);
for (int v = 0; v < n; v++) if (!used[v]) dfs1(v);

comp.assign(n, -1);
int c = 0;
for (int i = n - 1; i >= 0; i--)
    if (comp[order[i]] == -1) dfs2(order[i], c++);

Сложность — O(n+m)O(n + m): два обхода и построение GTG^T.

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

Почему именно эта комбинация

Вершина с наибольшим touttout по лемме лежит в компоненте, из которой рёбра только выходят (в конденсации это исток). В транспонированном графе такая компонента становится стоком: из неё никуда не выйти.

Значит, обход по GTG^T из этой вершины посетит ровно её компоненту. Красим, идём к следующей непосещённой вершине по убыванию touttout — она снова окажется истоком среди оставшихся, и так далее.

Важно, что работает только эта комбинация. Ни «по возрастанию touttout», ни «по временам входа», ни «обход по исходному графу» не годятся — на каждый из вариантов есть контрпример. Причина в лемме: она сформулирована про максимум времени выхода, и никакого симметричного утверждения про минимум нет.

Приятное следствие

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

Поэтому отдельно сортировать конденсацию не нужно. Строится она так:

vector<vector<int>> cond(c);
for (int v = 0; v < n; v++)
    for (int to : g[v])
        if (comp[v] != comp[to]) cond[comp[v]].push_back(comp[to]);

Условие comp[v] != comp[to] убирает петли. Кратные рёбра при этом остаются — если они мешают, уберите их через сортировку и unique.

Где применяется

Задачи «сделать граф сильно связным». Минимальное число рёбер, которое нужно добавить, считается по конденсации: это максимум из числа истоков и числа стоков (если компонент больше одной).

2-SAT. Логическая формула из дизъюнкций по две переменные выполнима тогда и только тогда, когда никакая переменная и её отрицание не лежат в одной компоненте сильной связности графа импликаций.

Игры и достижимость. «Из каких городов можно попасть в любой другой» — вопрос про исток конденсации.

Существует и алгоритм Тарьяна, находящий компоненты за один обход. Он быстрее по константе, но заметно сложнее для понимания; Косарайю почти всегда достаточно.