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

Компоненти сильної зв'язності та граф конденсації

Означення

Нехай G=(V,E)G=(V,E) — орієнтований граф з множиною вершин VV і множиною ребер EV×VE \subseteq V \times V. Позначимо через n=Vn=|V| кількість вершин, а через m=Em=|E| — кількість ребер у GG. Усі означення в цій статті легко узагальнити на мультиграфи, але ми на цьому не зосереджуватимемось.

Підмножину вершин CVC \subseteq V називають компонентою сильної зв'язності, якщо виконуються такі умови:

  • для всіх u,vCu,v\in C при uvu \neq v існує шлях з uu до vv і шлях з vv до uu, і
  • CC є максимальною в тому сенсі, що жодну вершину не можна додати, не порушивши наведену вище умову.

Позначимо через SCC(G)\text{SCC}(G) множину компонент сильної зв'язності графа GG. Ці компоненти сильної зв'язності не перетинаються між собою і покривають усі вершини графа. Отже, множина SCC(G)\text{SCC}(G) є розбиттям множини VV.

Розгляньмо граф GexampleG_\text{example}, у якому виділено компоненти сильної зв'язності:

drawing

Тут ми маємо SCC(Gexample)={{0,7},{1,2,3,5,6},{4,9},{8}}.\text{SCC}(G_\text{example})=\{\{0,7\},\{1,2,3,5,6\},\{4,9\},\{8\}\}. Можемо переконатися, що в межах кожної компоненти сильної зв'язності всі вершини досяжні одна з одної.

Означимо граф конденсації GSCC=(VSCC,ESCC)G^{\text{SCC}}=(V^{\text{SCC}}, E^{\text{SCC}}) так:

  • вершинами графа GSCCG^{\text{SCC}} є компоненти сильної зв'язності графа GG; тобто VSCC=SCC(G)V^{\text{SCC}} = \text{SCC}(G), і
  • для всіх вершин Ci,CjC_i,C_j графа конденсації існує ребро з CiC_i до CjC_j тоді й лише тоді, коли CiCjC_i \neq C_j і існують такі aCia\in C_i та bCjb\in C_j, що в GG є ребро з aa до bb.

Граф конденсації для GexampleG_\text{example} виглядає так:

drawing

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

Алгоритм, описаний у наступному розділі, знаходить усі компоненти сильної зв'язності в заданому графі. Після цього можна побудувати граф конденсації.

Коли підходить цей алгоритм?
  • Граф орієнтований, і потрібно згрупувати вершини за взаємною досяжністюuu в vv і назад)? (якщо граф неорієнтований → пошук компонент зв'язності)
  • Потрібно звести граф до ациклічного графа конденсації (наприклад, для ДП на DAG чи 2-SAT)?
  • Достатньо двох проходів DFS за O(n+m)O(n + m)?

Алгоритм Косараджу

Опис алгоритму

Описаний алгоритм незалежно запропонували Косараджу та Sharir приблизно у 1980 році. Він ґрунтується на двох серіях пошуку в глибину і має час роботи O(n+m)O(n + m).

На першому кроці алгоритму ми виконуємо послідовність пошуків у глибину (dfs), відвідуючи весь граф. Тобто доки ще є невідвідані вершини, ми беремо одну з них і запускаємо з неї пошук у глибину. Для кожної вершини ми відстежуємо час виходу tout[v]t_\text{out}[v]. Це «мітка часу», у яку завершується виконання dfs для вершини vv, тобто момент, коли всі досяжні з vv вершини вже відвідано і алгоритм повертається до vv. Лічильник міток часу не слід скидати між послідовними викликами dfs. Часи виходу відіграють ключову роль в алгоритмі, що стане зрозумілим, коли ми розглянемо наступну теорему.

Спершу означимо час виходу tout[C]t_\text{out}[C] компоненти сильної зв'язності CC як максимум значень tout[v]t_\text{out}[v] по всіх vC.v \in C. Крім того, у доведенні теореми ми згадуватимемо час входу tin[v]t_{\text{in}}[v] для кожної вершини vGv\in G. Число tin[v]t_{\text{in}}[v] є «міткою часу», у яку на першому кроці алгоритму викликається рекурсивна функція dfs для вершини vv. Для компоненти сильної зв'язності CC означимо tin[C]t_{\text{in}}[C] як мінімум значень tin[v]t_{\text{in}}[v] по всіх vCv \in C.

Теорема

Нехай CC і CC' — дві різні компоненти сильної зв'язності, і нехай у графі конденсації є ребро з CC до CC'. Тоді tout[C]>tout[C]t_\text{out}[C] > t_\text{out}[C'].

Доведення

Маємо два різні випадки залежно від того, якої компоненти першою досягне пошук у глибину:

  • Випадок 1: компоненту CC було досягнуто першою (тобто tin[C]<tin[C]t_{\text{in}}[C] < t_{\text{in}}[C']). У цьому випадку пошук у глибину відвідує деяку вершину vCv \in C у момент, коли всі інші вершини компонент CC і CC' ще не відвідано. Оскільки в графі конденсації є ребро з CC до CC', то з vv у GG досяжні не лише всі інші вершини компоненти CC, а й усі вершини компоненти CC'. Це означає, що це виконання dfs, яке запущене з вершини vv, у майбутньому також відвідає всі інші вершини компонент CC і CC', тож ці вершини будуть нащадками vv у дереві пошуку в глибину. Звідси випливає, що для кожної вершини u(CC){v}u \in (C \cup C')\setminus \{v\} маємо tout[v]>tout[u]t_\text{out}[v] > t_\text{out}[u]. Отже, tout[C]>tout[C]t_\text{out}[C] > t_\text{out}[C'], що завершує цей випадок доведення.

  • Випадок 2: компоненту CC' було досягнуто першою (тобто tin[C]>tin[C]t_{\text{in}}[C] > t_{\text{in}}[C']). У цьому випадку пошук у глибину відвідує деяку вершину vCv \in C' у момент, коли всі інші вершини компонент CC і CC' ще не відвідано. Оскільки в графі конденсації є ребро з CC до CC', то через властивість ациклічності CC не досяжна з CC'. Тому виконання dfs, запущене з вершини vv, не досягне жодної вершини CC, але відвідає всі вершини CC'. Вершини CC буде відвідано пізнішим виконанням dfs на цьому ж кроці алгоритму, тож справді маємо tout[C]>tout[C]t_\text{out}[C] > t_\text{out}[C']. Це завершує доведення.

Доведена теорема дуже важлива для знаходження компонент сильної зв'язності. Вона означає, що будь-яке ребро в графі конденсації йде з компоненти з більшим значенням toutt_\text{out} до компоненти з меншим значенням.

Якщо відсортувати всі вершини vVv \in V за спаданням їхнього часу виходу tout[v]t_\text{out}[v], то перша вершина uu належатиме «кореневій» компоненті сильної зв'язності, яка не має вхідних ребер у графі конденсації. Тепер ми хочемо запустити з цієї вершини uu якийсь різновид пошуку так, щоб він відвідав усі вершини її компоненти сильної зв'язності, але не інші вершини. Повторюючи це, ми зможемо поступово знайти всі компоненти сильної зв'язності: видаляємо всі вершини, що належать першій знайденій компоненті, потім знаходимо наступну з решти вершин з найбільшим значенням toutt_\text{out}, запускаємо з неї цей пошук і так далі. Урешті ми знайдемо всі компоненти сильної зв'язності. Щоб знайти метод пошуку, який поводиться так, як нам потрібно, розгляньмо таку теорему:

Теорема

Нехай GTG^T позначає транспонований граф графа GG, отриманий обертанням напрямків ребер у GG. Тоді SCC(G)=SCC(GT)\text{SCC}(G)=\text{SCC}(G^T). Крім того, граф конденсації графа GTG^T є транспонованим до графа конденсації графа GG.

Доведення опускаємо (воно прямолінійне). Як наслідок цієї теореми, у графі конденсації графа GTG^T не буде ребер з «кореневої» компоненти до інших компонент. Отже, щоб відвідати всю «кореневу» компоненту сильної зв'язності, яка містить вершину vv, достатньо запустити пошук у глибину з вершини vv у транспонованому графі GTG^T! Це відвідає рівно всі вершини цієї компоненти сильної зв'язності. Як було згадано раніше, потім ми можемо видалити ці вершини з графа. Далі знаходимо наступну вершину з максимальним значенням tout[v]t_\text{out}[v] і запускаємо пошук у транспонованому графі з цієї вершини, щоб знайти наступну компоненту сильної зв'язності. Повторюючи це, ми знаходимо всі компоненти сильної зв'язності.

Отже, підсумовуючи, ми розглянули такий алгоритм для знаходження компонент сильної зв'язності:

  • Крок 1. Запускаємо послідовність пошуків у глибину на GG, що дасть деякий список (наприклад, order) вершин, відсортований за зростанням часу виходу toutt_\text{out}.

  • Крок 2. Будуємо транспонований граф GTG^T і запускаємо серію пошуків у глибину на вершинах у зворотному порядку (тобто за спаданням часів виходу). Кожен пошук у глибину дасть одну компоненту сильної зв'язності.

  • Крок 3 (необов'язковий). Будуємо граф конденсації.

Часова складність алгоритму становить O(n+m)O(n + m), оскільки пошук у глибину виконується двічі. Побудова графа конденсації також має складність O(n+m).O(n+m).

Нарешті, доречно тут згадати топологічне сортування. На кроці 1 ми знаходимо вершини в порядку зростання часу виходу. Якщо GG ациклічний, це відповідає (оберненому) топологічному сортуванню GG. На кроці 2 алгоритм знаходить компоненти сильної зв'язності за спаданням їхніх часів виходу. Тобто він знаходить компоненти — вершини графа конденсації — у порядку, що відповідає топологічному сортуванню графа конденсації.

Реалізація

vector<bool> visited; // відстежує, які вершини вже відвідано

// запускає пошук у глибину, починаючи з вершини v.
// кожну відвідану вершину додаємо до вихідного вектора, коли dfs її залишає.
void dfs(int v, vector<vector<int>> const& adj, vector<int> &output) {
visited[v] = true;
for (auto u : adj[v])
if (!visited[u])
dfs(u, adj, output);
output.push_back(v);
}

// вхід: adj -- список суміжності G
// вихід: components -- компоненти сильної зв'язності в G
// вихід: adj_cond -- список суміжності G^SCC (за кореневими вершинами)
void strongly_connected_components(vector<vector<int>> const& adj,
vector<vector<int>> &components,
vector<vector<int>> &adj_cond) {
int n = adj.size();
components.clear(), adj_cond.clear();

vector<int> order; // буде відсортованим за часом виходу списком вершин G

visited.assign(n, false);

// перша серія пошуків у глибину
for (int i = 0; i < n; i++)
if (!visited[i])
dfs(i, adj, order);

// створюємо список суміжності G^T
vector<vector<int>> adj_rev(n);
for (int v = 0; v < n; v++)
for (int u : adj[v])
adj_rev[u].push_back(v);

visited.assign(n, false);
reverse(order.begin(), order.end());

vector<int> roots(n, 0); // дає кореневу вершину SCC, до якої належить вершина

// друга серія пошуків у глибину
for (auto v : order)
if (!visited[v]) {
std::vector<int> component;
dfs(v, adj_rev, component);
components.push_back(component);
int root = *component.begin();
for (auto u : component)
roots[u] = root;
}

// додаємо ребра до графа конденсації
adj_cond.assign(n, {});
for (int v = 0; v < n; v++)
for (auto u : adj[v])
if (roots[v] != roots[u])
adj_cond[roots[v]].push_back(roots[u]);
}

Функція dfs реалізує пошук у глибину. На вхід вона приймає список суміжності та початкову вершину. Вона також приймає посилання на вектор output: кожну відвідану вершину буде додано до output, коли dfs залишає цю вершину.

Зауважте, що ми використовуємо функцію dfs як на першому, так і на другому кроці алгоритму. На першому кроці ми передаємо список суміжності GG, і впродовж послідовних викликів dfs ми передаємо той самий «вихідний вектор» order, тож зрештою отримуємо список вершин у порядку зростання часів виходу. На другому кроці ми передаємо список суміжності GTG^T, і в кожному виклику передаємо порожній «вихідний вектор» component, який даватиме нам по одній компоненті сильної зв'язності за раз.

Алгоритм Тар'яна для компонент сильної зв'язності

Опис алгоритму

Описаний алгоритм уперше запропонував Тар'ян у 1972 році. Він ґрунтується на виконанні послідовності викликів DFS і використовує інформацію, властиву самій його структурі, щоб визначити компоненти сильної зв'язності (SCC), маючи час роботи O(n+m)O(n+m).

Застосовуючи DFS до вершини, ми обходимо її список суміжності, і якщо знаходимо вершину, яку ще не відвідано, рекурсивно застосовуємо до неї DFS.

Розгляньмо дерево, породжене послідовністю викликів DFS, яке ми називатимемо деревом DFS. Щойно ми вперше викликаємо DFS на вершині з якоїсь SCC, усі вершини цієї SCC буде відвідано до завершення цього виклику, оскільки всі вони досяжні одна з одної. У дереві DFS ця перша вершина буде спільним предком усіх інших вершин SCC; цю вершину ми означаємо як корінь SCC.

Теорема

Усі вершини SCC породжують зв'язний підграф дерева DFS.

Доведення

Ми встановили, що всі вершини SCC мають спільного предка — першу вершину, яку відвідав виклик DFS. Розгляньмо вершину vv та її корінь — вершину rr. Усі вершини на шляху з rr до vv належать тій самій SCC. Усі ці вершини досяжні з rr, і всі вони досягають vv, а оскільки за означенням vv досягає rr, то всі ці вершини досягають одна одної. Оскільки всі шляхи з кореня до кожної іншої вершини SCC належать тій самій SCC, утворений підграф є зв'язним.

Зауважте, що SCC ідеально розбивають дерево DFS на зв'язні підграфи.

Тоді ідея алгоритму така:

  • Ми виконуємо послідовність викликів DFS, рекурсивно застосовуючи їх до вершин зі списків суміжності.

  • Щойно ми завершуємо обхід списку суміжності вершини, ми якимось чином можемо визначити, чи є вона коренем. Цей метод буде пояснено пізніше.

  • Якщо вершина є коренем, ми одразу знаходимо й привласнюємо всі вершини її SCC.

Коли всі виклики завершуються, усі корені буде виявлено, і всі вершини буде привласнено до якоїсь SCC.

Тепер проаналізуймо властивості DFS, коли вводиться цей процес привласнення.

Теорема

Розгляньмо вершину vv і нехай ми щойно завершили обхід її списку суміжності. Усі непривласнені вершини в її піддереві належать тій самій SCC.

Доведення

Алгоритм привласнює вершини SCC, коли знаходить її корінь. Оскільки список суміжності vv обійдено, усі виклики DFS на її піддереві завершилися, корені виявлено, а вершини, що належать їхнім SCC, привласнено. Коренем решти непривласнених вершин буде предок, процес привласнення якого ще не виконувався, тож це або vv, або предок vv. Оскільки vv лежить на шляху від усіх вершин до їхнього кореня, а SCC мають породжувати зв'язний підграф дерева, то і vv, і всі решта вершин належать тій самій SCC.

Теорема

Розгляньмо вершину vv і нехай ми обходимо її список суміжності, наразі обробляючи ребро (v,u)(v, u). Якщо uu вже було відвідано якимось викликом DFS і вона лишається непривласненою, то vv і uu належать тій самій SCC.

Доведення

Маємо різні випадки залежно від виду ребра:

  • Деревне ребро (tree-edge): якщо це деревне ребро, то ми вперше знаходимо вершину uu. Це означає, що ми спершу маємо рекурсивно застосувати виклик DFS до uu і розглянути її після завершення її виклику DFS. Якщо вершина uu лишається непривласненою, то її корінь — це або vv, або предок vv, тож вони мають належати тій самій SCC.

  • Зворотне ребро (back-edge): це простіший випадок; якщо uu є предком vv, то вони досяжні одна з одної і за означенням належать тій самій SCC.

  • Пряме ребро (forward-edge): перш ніж це ребро було оброблено, була послідовність викликів DFS, яка завершилася, не знайшовши кореня uu, повернувшись до vv, чий виклик DFS продовжився. Тоді коренем uu буде предок, процес привласнення якого ще не виконувався, тож це або vv, або предок vv, отже, вони мають належати тій самій SCC.

  • Перехресне ребро (cross-edge): аналогічно, перш ніж це ребро було оброблено, була послідовність викликів DFS, яка завершилася, не знайшовши кореня uu, повернувшись до спільного предка uu і vv, чий виклик DFS продовжився та започаткував нову послідовність викликів DFS, що привела до виклику на vv. Тоді коренем uu буде предок, процес привласнення якого ще не виконувався, і всі можливі кандидати є спільними предками з vv. Оскільки корінь uu є предком vv, він досягає vv, а оскільки vv тепер досягає uu, то вони мають належати тій самій SCC.

Зауважте: коли дві вершини належать одній компоненті, їхній корінь має бути спільним предком обох вершин.

Теорема

Нехай vv — вершина. Наступні твердження рівносильні:

  1. Деяка вершина в піддереві vv досягає непривласненої вершини поза піддеревом.
  2. vv не є коренем SCC.
Доведення
  • 1.    2.1. \implies 2.: Припустимо, що деяка вершина uu в піддереві vv досягає непривласненої вершини ww поза піддеревом. Ми встановили, що uu і ww належать тій самій SCC і що їхній корінь має бути спільним предком обох. Цей спільний предок обов'язково лежить поза піддеревом, і він також буде предком vv. Оскільки vv лежить на шляху від кореня до uu, вона має належати тій самій SCC, коренем якої не є vv.

  • ¬1.    ¬2.\neg 1. \implies \neg 2.: Припустимо, що жодна вершина в піддереві vv не досягає непривласненої вершини поза піддеревом. Це має означати, що жодна вершина в піддереві vv не досягає предка vv. Єдині можливі ребра до вершин поза піддеревом — це перехресні ребра до вершин, які вже привласнено; ці вершини не можуть досягати предка vv, бо якби могли, то належали б тій самій SCC, що й vv, що неможливо, оскільки їхню SCC вже визначено. Оскільки жоден предок vv не досяжний з її піддерева, коренем vv має бути сама vv.

Тепер ми маємо знайти метод, який дозволить нам визначити, чи є вершина коренем, і властивості процесу привласнення необхідні для його коректності. Із цією метою ми означаємо час входу tin[v]t_{in}[v] для кожної вершини vGv \in G, який відповідає «мітці часу», у яку було викликано DFS на vv. За означенням корінь є першою вершиною SCC, яку відвідує DFS, тож він матиме мінімальне значення tint_{in} серед своєї SCC.

Нехай vv — вершина, і розгляньмо її піддерево. У момент, коли ми завершуємо обхід її списку суміжності, будь-яка вершина, уже відвідана DFS поза піддеревом, матиме менше значення tint_{in}, оскільки DFS було викликано на ній раніше, ніж він почався на vv.

Якщо врахувати процес привласнення, значення tint_{in} усіх непривласнених вершин поза піддеревом vv менше за tin[v]t_{in}[v]. Тепер ми можемо побачити, як використати tint_{in} для визначення коренів. Ми розглядаємо мінімальне значення tint_{in} серед непривласнених вершин, яких можемо досягти, і поширюємо цю інформацію до предків через деревні ребра. Поширене значення ми називатимемо tlowt_{low}.

Формальніше, ми означаємо tlow[v]t_{low}[v] як найменше значення tint_{in}, якого вершина в піддереві vv може досягти через пряме ребро. Тому ми можемо виявити, чи є вершина vv коренем, перевіривши, чи tlow[v]<tin[v]t_{low}[v] < t_{in}[v].

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

  • Коли ми вперше відвідуємо вершину, ми просто вставляємо її в структуру даних, оскільки ця вершина непривласнена.

  • Коли ми знаходимо корінь, ми маємо знайти всі решта непривласнених вершин у його піддереві й видалити їх зі структури даних.

Ми можемо знайти альтернативний спосіб опису операції видалення, зауваживши, що одразу після обходу списку суміжності вершини vv усі вершини, поміщені в структуру даних після vv, належать її піддереву. Якщо vv є коренем, усі решта вершин, які було вставлено після vv, мають бути видалені. Отже, операцію видалення можна натомість описати так:

  • Коли ми знаходимо корінь, ми маємо знайти й видалити всі решта вершин, які було вставлено після нього.

Тепер бачимо, що це можна реалізувати за допомогою стеку:

  • Коли ми вперше відвідуємо вершину, ми кладемо її на стек.

  • Коли ми знаходимо корінь, ми знімаємо зі стеку всі елементи, доки не знімемо сам корінь.

Це нарешті дозволяє нам реалізувати алгоритм.

Часова складність послідовності викликів DFS становить O(n+m)O(n + m). Якщо врахувати стек, його складність амортизується до O(n)O(n), оскільки кожен вузол лише раз кладуть і раз знімають. Тому загальна часова складність становить O(n+m)O(n + m).

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

Реалізація

vector<int> st; // - стек, що містить непривласнені вершини
vector<int> roots; // - відстежує корені SCC для вершин
int timer; // - лічильник міток часу dfs
vector<int> t_in; // - відстежує мітку часу dfs для вершин
vector<int> t_low; // - відстежує найменше t_in непривласнених вершин,
// досяжних у піддереві

// реалізує алгоритм Тар'яна для компонент сильної зв'язності
void dfs(int v, vector<vector<int>> const &adj, vector<vector<int>> &components) {

t_low[v] = t_in[v] = timer++;
st.push_back(v);

for (auto u : adj[v]) {
if (t_in[u] == -1) { // деревне ребро
dfs(u, adj, components);
t_low[v] = min(t_low[v], t_low[u]);
} else if (roots[u] == -1) { // зворотне, перехресне або пряме ребро до непривласненої вершини
t_low[v] = min(t_low[v], t_in[u]);
}
}

if (t_low[v] == t_in[v]) { // вершина є коренем
components.push_back({v}); // ініціалізує нову компоненту з коренем v
while (true) {
int u = st.back();
st.pop_back();
roots[u] = v; // привласнює вершину
if (u == v)
break;
components.back().push_back(u); // додає вершину u до компоненти v
}
}
}

// вхід: adj -- список суміжності G
// вихід: components -- компоненти сильної зв'язності в G
// вихід: adj_cond -- список суміжності G^SCC (за кореневими вершинами)
void strongly_connected_components(vector<vector<int>> const &adj,
vector<vector<int>> &components,
vector<vector<int>> &adj_cond) {
components.clear();
adj_cond.clear();

int n = adj.size();

st.clear();
roots.assign(n, -1);
timer = 0;
t_in.assign(n, -1);
t_low.assign(n, -1);

// застосовує алгоритм Тар'яна до всіх вершин
// додає вершини до компонент в оберненому топологічному порядку
for (int v = 0; v < n; v++) {
if (t_in[v] == -1) {
dfs(v, adj, components);
}
}

// додає ребра до графа конденсації
adj_cond.assign(n, {});
for (int v = 0; v < n; v++) {
for (auto u : adj[v])
if (roots[v] != roots[u])
adj_cond[roots[v]].push_back(roots[u]);
}
}

Ми маємо прийняте розв'язання з цим кодом у Library Checker.

Як останнє зауваження, є альтернативний спосіб ітерувати списком суміжності. Наразі ми робимо так:

for (auto u : adj[v]) {
if (t_in[u] == -1) { // деревне ребро
dfs(u, adj);
t_low[v] = min(t_low[v], t_low[u]);
} else if (roots[u] == -1) { // зворотне, перехресне або пряме ребро до непривласненої вершини
t_low[v] = min(t_low[v], t_in[u]);
}
}

Натомість можна зробити так:

for (auto u : adj[v]) {
if (t_in[u] == -1) // вершину не відвідано
dfs(u, adj);
if (roots[u] == -1) // вершину не привласнено
t_low[v] = min(t_low[v], t_low[u]);
}

tlowt_{low} використовується для поширення інформації до кореня, і коли ми виконуємо t_low[v] = min(t_low[v], t_in[u]), ми знаємо, що uu і vv належать тій самій SCC. Якщо tlow[u]t_{low}[u] поширюється аж до кореня uu, то його також можна поширити через vv, оскільки корінь той самий. Оскільки tlow[u]tin[u]t_{low}[u] \leq t_{in}[u], це не вносить жодних конфліктів, а лише покращує оцінку на корінь vv.

Побудова графа конденсації

Будуючи список суміжності графа конденсації, ми обираємо корінь кожної компоненти як першу вершину в її списку вершин (це довільний вибір). Ця коренева вершина представляє всю свою SCC. Для кожної вершини v значення roots[v] вказує кореневу вершину SCC, до якої належить v.

Наш граф конденсації тепер задається вершинами components (одна компонента сильної зв'язності відповідає одній вершині графа конденсації), а список суміжності задається adj_cond, де використовуються лише кореневі вершини компонент сильної зв'язності. Зауважте, що ми породжуємо одне ребро з CC до CC' у GSCCG^\text{SCC} для кожного ребра з деякої aCa\in C до деякої bCb\in C' у GG (якщо CCC\neq C'). Це означає, що в нашій реалізації між двома компонентами графа конденсації може бути кілька ребер.

Література

  • Thomas Cormen, Charles Leiserson, Ronald Rivest, Clifford Stein. Introduction to Algorithms [2005].
  • M. Sharir. A strong-connectivity algorithm and its applications in data-flow analysis [1979].
  • Robert Tarjan. Depth-first search and linear graph algorithms [1972].

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

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