EduBrick

Топологическая сортировка

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

4 мин

Дан ориентированный граф. Нужно упорядочить вершины так, чтобы для каждого ребра u→vu \to v вершина uu стояла левее vv.

Такой порядок называется топологическим. Он существует тогда и только тогда, когда в графе нет циклов: внутри цикла нельзя расставить вершины так, чтобы все рёбра смотрели вперёд.

Ориентированный граф без циклов называют DAG (directed acyclic graph). Это главный случай, где работает динамика на графе: состояния можно считать в топологическом порядке, и все зависимости к моменту вычисления уже готовы.

Через времена выхода

Ключевое наблюдение: если есть ребро u→vu \to v, то toutu>toutvtout_u > tout_v.

Два случая. Если к моменту просмотра ребра vv ещё не посещена — обход спустится в неё и выйдет раньше, чем из uu. Если посещена и уже завершена — её touttout проставлен, а toututout_u ещё нет. Третий случай, «посещена и не завершена», означал бы обратное ребро, то есть цикл, — а его нет.

Значит, порядок по убыванию touttout и есть топологический.

Сортировать при этом не нужно: достаточно записывать вершины в момент выхода и в конце развернуть список.

vector<int> order;

void dfs(int v) {
    color[v] = 1;
    for (int to : g[v]) if (color[to] == 0) dfs(to);
    color[v] = 2;
    order.push_back(v);
}

// в main:
for (int v = 0; v < n; v++) if (color[v] == 0) dfs(v);
reverse(order.begin(), order.end());

Сложность — O(n+m)O(n + m). Именно поэтому запись в момент выхода лучше сортировки: та добавила бы лишний логарифм ни за что.

Проверка на цикл встраивается прямо сюда — третьим цветом. Если встретили ребро в вершину цвета 1, топологического порядка не существует.

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

Через степени входа

Второй способ, алгоритм Кана, не использует рекурсию — и потому безопаснее при больших nn.

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

vector<int> inDegree(n, 0);
for (int v = 0; v < n; v++) for (int to : g[v]) inDegree[to]++;

queue<int> q;
for (int v = 0; v < n; v++) if (inDegree[v] == 0) q.push(v);

vector<int> order;
while (!q.empty()) {
    int v = q.front(); q.pop();
    order.push_back(v);
    for (int to : g[v]) if (--inDegree[to] == 0) q.push(to);
}

bool hasCycle = (int)order.size() < n;

Проверка на цикл получается бесплатно: если в ответе оказались не все вершины, значит остались те, чей счётчик так и не обнулился, — а это цикл.

Дополнительное удобство: заменив очередь на кучу минимума, получим лексикографически наименьший топологический порядок. Через времена выхода так не выйдет.

Порядок не единственный

Топологических порядков обычно много. Для «ромба» 1→21 \to 2, 1→31 \to 3, 2→42 \to 4, 3→43 \to 4 подходят и 1,2,3,41,2,3,4, и 1,3,2,41,3,2,4.

Если задача просит конкретный — обычно лексикографически минимальный, — берите алгоритм Кана с кучей. Если любой, берите вариант с обходом: он короче.

Зачем это нужно

Динамика на DAG. Считаем что-нибудь для каждой вершины, зная значения для всех, куда из неё есть рёбра. Например, длиннейший путь: идём в обратном топологическом порядке, dpv=1+max⁡dptodp_v = 1 + \max dp_{to}. В графе с циклами эта задача NP-полна, в DAG — линейна.

Порядок задач. Классическая постановка «есть зависимости между делами, в каком порядке их выполнять». Ответ «невозможно» означает цикл в зависимостях.

Проверка на противоречивость. Даны отношения вида a<ba < b; существует ли согласованный порядок? Это ровно наличие топологической сортировки.

Основа для конденсации. Граф компонент сильной связности — всегда DAG, и работать с ним удобно именно в топологическом порядке.