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

Пошук мостів у графі за O(N+M)O(N+M)

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

Неформально задачу можна сформулювати так: маємо карту міст, з'єднаних дорогами; потрібно знайти всі «важливі» дороги, тобто такі дороги, видалення яких призводить до зникнення шляху між якоюсь парою міст.

Описаний тут алгоритм ґрунтується на пошуку в глибину і має складність O(N+M)O(N+M), де NN — кількість вершин, а MM — кількість ребер у графі.

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

Коли підходить цей алгоритм?
  • Граф заданий повністю заздалегідь (статичний), а мости треба знайти один раз? (якщо ребра додаються по одному й мости треба перераховувати після кожного додавання → Пошук мостів онлайн)
  • Потрібні саме ребра, видалення яких роз'єднує граф? (якщо потрібні вершиниТочки зчленування)
  • Достатньо одного проходу DFS за O(N+M)O(N+M)?

Алгоритм

Виберемо довільну вершину графа rootroot і запустимо з неї пошук у глибину. Зауважимо такий факт (який легко довести):

  • Нехай ми перебуваємо в DFS і переглядаємо ребра, що виходять з вершини vv. Поточне ребро (v,to)(v, to) є мостом тоді й лише тоді, коли ні вершина toto, ні жоден з її нащадків у дереві обходу DFS не має зворотного ребра до вершини vv або до будь-кого з її предків. Справді, ця умова означає, що з vv до toto немає іншого шляху, крім ребра (v,to)(v, to).

Тепер нам треба навчитися ефективно перевіряти цей факт для кожної вершини. Ми скористаємося «часом входу до вершини», який обчислюється під час пошуку в глибину.

Отже, нехай tin[v]\mathtt{tin}[v] позначає час входу до вершини vv. Введемо масив low\mathtt{low}, який дозволить нам зберігати найраніший час входу серед вершин, знайдених під час пошуку DFS, до яких вершина vv може дістатися за допомогою одного ребра з себе самої або зі своїх нащадків. Значення low[v]\mathtt{low}[v] — це мінімум з tin[v]\mathtt{tin}[v], часів входу tin[p]\mathtt{tin}[p] для кожної вершини pp, з'єднаної з вершиною vv зворотним ребром (v,p)(v, p), та значень low[to]\mathtt{low}[to] для кожної вершини toto, яка є безпосереднім нащадком vv у дереві DFS:

low[v]=min{tin[v]tin[p] для всіх p, для яких (v,p) — зворотне реброlow[to] для всіх to, для яких (v,to) — деревне ребро}\mathtt{low}[v] = \min \left\{ \begin{array}{l} \mathtt{tin}[v] \\ \mathtt{tin}[p] &\text{ для всіх }p\text{, для яких }(v, p)\text{ — зворотне ребро} \\ \mathtt{low}[to] &\text{ для всіх }to\text{, для яких }(v, to)\text{ — деревне ребро} \end{array} \right\}

Тоді зворотне ребро з вершини vv або з одного з її нащадків до одного з її предків існує тоді й лише тоді, коли вершина vv має такого нащадка toto, для якого low[to]tin[v]\mathtt{low}[to] \leq \mathtt{tin}[v]. Якщо low[to]=tin[v]\mathtt{low}[to] = \mathtt{tin}[v], то зворотне ребро веде безпосередньо до vv, інакше — до одного з предків vv.

Таким чином, поточне ребро (v,to)(v, to) у дереві DFS є мостом тоді й лише тоді, коли low[to]>tin[v]\mathtt{low}[to] > \mathtt{tin}[v].

Реалізація

У реалізації потрібно розрізняти три випадки: коли ми спускаємося вздовж ребра в дереві DFS, коли ми знаходимо зворотне ребро до предка вершини і коли ми повертаємося до батька вершини. Ось ці випадки:

  • visited[to]=false\mathtt{visited}[to] = false — ребро є частиною дерева DFS;
  • visited[to]=true\mathtt{visited}[to] = true && toparentto \neq parent — ребро є зворотним ребром до одного з предків;
  • to=parentto = parent — ребро веде назад до батька в дереві DFS.

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

Для випадків кратних ребер треба бути обережними, ігноруючи ребро до батька. Щоб розв'язати цю проблему, ми можемо додати прапорець parent_skipped, який гарантує, що ми пропустимо батька лише один раз.

void IS_BRIDGE(int v,int to); // якась функція для обробки знайденого мосту
int n; // кількість вершин
vector<vector<int>> adj; // список суміжності графа

vector<bool> visited;
vector<int> tin, low;
int timer;

void dfs(int v, int p = -1) {
visited[v] = true;
tin[v] = low[v] = timer++;
bool parent_skipped = false;
for (int to : adj[v]) {
if (to == p && !parent_skipped) {
parent_skipped = true;
continue;
}
if (visited[to]) {
low[v] = min(low[v], tin[to]);
} else {
dfs(to, v);
low[v] = min(low[v], low[to]);
if (low[to] > tin[v])
IS_BRIDGE(v, to);
}
}
}

void find_bridges() {
timer = 0;
visited.assign(n, false);
tin.assign(n, -1);
low.assign(n, -1);
for (int i = 0; i < n; ++i) {
if (!visited[i])
dfs(i);
}
}

Головна функція — find_bridges; вона виконує необхідну ініціалізацію і запускає пошук у глибину в кожній компоненті зв'язності графа.

Функція IS_BRIDGE(a, b) — це якась функція, що оброблятиме той факт, що ребро (a,b)(a, b) є мостом, наприклад, виводитиме його.

Зауважимо, що ця реалізація працює некоректно, якщо в графі є кратні ребра, оскільки вона їх ігнорує. Звісно, кратні ребра ніколи не будуть частиною відповіді, тож IS_BRIDGE може додатково перевіряти, що повідомлений міст не є кратним ребром. Як альтернатива, можна передавати у dfs індекс ребра, використаного для входу до вершини, замість батьківської вершини (і зберігати індекси всіх вершин).

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

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