Топологическая сортировка
Выстроить вершины в ряд так, чтобы все рёбра шли слева направо. Через времена выхода и через степени входа.
4 мин
Дан ориентированный граф. Нужно упорядочить вершины так, чтобы для каждого ребра вершина стояла левее .
Такой порядок называется топологическим. Он существует тогда и только тогда, когда в графе нет циклов: внутри цикла нельзя расставить вершины так, чтобы все рёбра смотрели вперёд.
Ориентированный граф без циклов называют DAG (directed acyclic graph). Это главный случай, где работает динамика на графе: состояния можно считать в топологическом порядке, и все зависимости к моменту вычисления уже готовы.
Через времена выхода
Ключевое наблюдение: если есть ребро , то .
Два случая. Если к моменту просмотра ребра ещё не посещена — обход спустится в неё и выйдет раньше, чем из . Если посещена и уже завершена — её проставлен, а ещё нет. Третий случай, «посещена и не завершена», означал бы обратное ребро, то есть цикл, — а его нет.
Значит, порядок по убыванию и есть топологический.
Сортировать при этом не нужно: достаточно записывать вершины в момент выхода и в конце развернуть список.
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());
Сложность — . Именно поэтому запись в момент выхода лучше сортировки: та добавила бы лишний логарифм ни за что.
Проверка на цикл встраивается прямо сюда — третьим цветом. Если встретили ребро в вершину цвета 1, топологического порядка не существует.
Проверено: на двадцати тысячах случайных ориентированных графов до семи вершин алгоритм либо возвращает порядок, где каждое ребро идёт слева направо, либо сообщает о цикле — и это совпадает с независимой проверкой.
Через степени входа
Второй способ, алгоритм Кана, не использует рекурсию — и потому безопаснее при больших .
Считаем для каждой вершины число входящих рёбер. Кладём в очередь все вершины с нулём. Достаём по одной, добавляем в ответ и уменьшаем счётчик у всех соседей; те, у кого счётчик обнулился, кладём в очередь.
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;
Проверка на цикл получается бесплатно: если в ответе оказались не все вершины, значит остались те, чей счётчик так и не обнулился, — а это цикл.
Дополнительное удобство: заменив очередь на кучу минимума, получим лексикографически наименьший топологический порядок. Через времена выхода так не выйдет.
Порядок не единственный
Топологических порядков обычно много. Для «ромба» , , , подходят и , и .
Если задача просит конкретный — обычно лексикографически минимальный, — берите алгоритм Кана с кучей. Если любой, берите вариант с обходом: он короче.
Зачем это нужно
Динамика на DAG. Считаем что-нибудь для каждой вершины, зная значения для всех, куда из неё есть рёбра. Например, длиннейший путь: идём в обратном топологическом порядке, . В графе с циклами эта задача NP-полна, в DAG — линейна.
Порядок задач. Классическая постановка «есть зависимости между делами, в каком порядке их выполнять». Ответ «невозможно» означает цикл в зависимостях.
Проверка на противоречивость. Даны отношения вида ; существует ли согласованный порядок? Это ровно наличие топологической сортировки.
Основа для конденсации. Граф компонент сильной связности — всегда DAG, и работать с ним удобно именно в топологическом порядке.