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

Пошук точок зчленування в графі за O(N+M)O(N+M)

Нам задано неорієнтований граф. Точка зчленування (англ. articulation point, або cut vertex) — це вершина, яка при видаленні разом з інцидентними їй ребрами робить граф незв'язним (точніше, збільшує кількість компонент зв'язності в графі). Завдання полягає в тому, щоб знайти всі точки зчленування в заданому графі.

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

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

Алгоритм

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

  • Припустимо, ми перебуваємо в DFS і переглядаємо ребра, що виходять з вершини vrootv\ne root. Якщо поточне ребро (v,to)(v, to) таке, що ані вершина toto, ані жоден з її нащадків у дереві обходу DFS не має зворотного ребра до жодного з предків vv, то vv є точкою зчленування. Інакше vv не є точкою зчленування.

  • Розглянемо випадок, що залишився, — v=rootv=root. Ця вершина буде точкою зчленування тоді й лише тоді, коли вона має більше ніж одного нащадка в дереві DFS.

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

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

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

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

Таким чином, вершина vv у дереві DFS є точкою зчленування тоді й лише тоді, коли low[to]tin[v]low[to] \geq tin[v].

Реалізація

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

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

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

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++;
int children=0;
for (int to : adj[v]) {
if (to == p) 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] && p!=-1)
IS_CUTPOINT(v);
++children;
}
}
if(p == -1 && children > 1)
IS_CUTPOINT(v);
}

void find_cutpoints() {
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_cutpoints; вона виконує необхідну ініціалізацію і запускає пошук у глибину в кожній компоненті зв'язності графа.

Функція IS_CUTPOINT(a) — це деяка функція, яка обробляє той факт, що вершина aa є точкою зчленування, наприклад, виводить її (зверніть увагу, що вона може бути викликана для однієї вершини кілька разів).

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

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