Перейти до основного вмісту

Топологічне сортування

Нам задано орієнтований граф з nn вершинами та mm ребрами. Потрібно знайти порядок вершин такий, щоб кожне ребро вело від вершини з меншим індексом до вершини з більшим.

Іншими словами, ми хочемо знайти перестановку вершин (топологічний порядок), яка відповідає порядку, заданому всіма ребрами графа.

Ось приклад заданого графа разом із його топологічним порядком:

example directed graphone topological order

Топологічний порядок може бути неоднозначним (наприклад, якщо існують три вершини aa, bb, cc, для яких є шляхи з aa до bb і з aa до cc, але немає шляхів з bb до cc чи з cc до bb). Граф у прикладі також має кілька топологічних порядків, ось другий топологічний порядок:

second topological order

Топологічний порядок може й не існувати взагалі. Він існує лише тоді, коли орієнтований граф не містить циклів. Інакше виникає суперечність: якщо є цикл, що містить вершини aa і bb, то aa має мати менший індекс, ніж bb (оскільки з aa можна дістатися до bb), і водночас більший (оскільки з bb можна дістатися до aa). Алгоритм, описаний у цій статті, також показує конструктивно, що кожен ациклічний орієнтований граф містить щонайменше один топологічний порядок.

Поширена задача, у якій виникає топологічне сортування, така. Є nn змінних з невідомими значеннями. Для деяких змінних відомо, що одна з них менша за іншу. Потрібно перевірити, чи не суперечливі ці обмеження, і якщо ні — вивести змінні у зростаючому порядку (якщо можливих відповідей кілька, вивести будь-яку з них). Легко помітити, що це саме задача про знаходження топологічного порядку графа з nn вершинами.

Коли підходить цей алгоритм?
  • Граф орієнтований і ациклічний (DAG)? Топологічний порядок існує лише без циклів. (якщо граф може мати цикли — спершу знайди цикл або розбий на SCC)
  • Потрібно впорядкувати об'єкти за залежностями (порядок збірки, проходження курсів, ДП на DAG)?

Алгоритм

Щоб розв'язати цю задачу, ми скористаємося пошуком у глибину.

Припустимо, що граф ациклічний. Що робить пошук у глибину?

Стартуючи з деякої вершини vv, DFS намагається пройти вздовж усіх ребер, що виходять з vv. Він зупиняється на тих ребрах, кінці яких уже були відвідані раніше, і проходить вздовж решти ребер, рекурсивно продовжуючи з їхніх кінців.

Таким чином, до моменту, коли виклик функції dfs(v)\text{dfs}(v) завершився, усі вершини, досяжні з vv, були відвідані пошуком безпосередньо (через одне ребро) або опосередковано.

Додаваймо вершину vv до списку, коли завершуємо dfs(v)\text{dfs}(v). Оскільки всі досяжні вершини вже відвідані, вони вже будуть у списку на момент, коли ми додаємо vv. Зробімо це для кожної вершини графа за допомогою одного або кількох запусків пошуку у глибину. Для кожного орієнтованого ребра vuv \rightarrow u у графі uu з'явиться в цьому списку раніше за vv, бо uu досяжна з vv. Отже, якщо ми просто позначимо вершини в цьому списку числами n1,n2,,1,0n-1, n-2, \dots, 1, 0, то знайдемо топологічний порядок графа. Іншими словами, цей список представляє обернений топологічний порядок.

Ці пояснення можна також подати в термінах часів виходу алгоритму DFS. Час виходу для вершини vv — це момент, у який завершився виклик функції dfs(v)\text{dfs}(v) (часи можна нумерувати від 00 до n1n-1). Легко зрозуміти, що час виходу будь-якої вершини vv завжди більший за час виходу будь-якої досяжної з неї вершини (оскільки вони були відвідані або перед викликом dfs(v)\text{dfs}(v), або під час нього). Отже, шуканим топологічним порядком є вершини у спадному порядку їхніх часів виходу.

Реалізація

Ось реалізація, яка припускає, що граф ациклічний, тобто шуканий топологічний порядок існує. За потреби ви легко можете перевірити, що граф ациклічний, як описано в статті про пошук у глибину.

int n; // кількість вершин
vector<vector<int>> adj; // список суміжності графа
vector<bool> visited;
vector<int> ans;

void dfs(int v) {
visited[v] = true;
for (int u : adj[v]) {
if (!visited[u]) {
dfs(u);
}
}
ans.push_back(v);
}

void topological_sort() {
visited.assign(n, false);
ans.clear();
for (int i = 0; i < n; ++i) {
if (!visited[i]) {
dfs(i);
}
}
reverse(ans.begin(), ans.end());
}

Головна функція розв'язку — topological_sort, яка ініціалізує змінні DFS, запускає DFS і отримує відповідь у векторі ans. Варто зауважити, що коли граф не ациклічний, результат topological_sort усе одно буде певною мірою осмисленим у тому сенсі, що якщо вершина uu досяжна з вершини vv, але не навпаки, то вершина vv завжди стоятиме першою в результуючому масиві. Ця властивість наведеної реалізації використовується в алгоритмі Косарайю для виокремлення компонент сильної зв'язності та їх топологічного сортування в орієнтованому графі з циклами.

Задачі для практики

Відеоматеріали