Компоненты сильной связности: алгоритм Тарьяна за один DFS — как работает low-link?
Нужно найти компоненты сильной связности (SCC) в ориентированном графе. Знаю про Косарайю (два DFS), но видел, что Тарьян делает это за один проход через значения tin/low и стек. Не понимаю, что такое low-link и почему вершина — «корень» SCC, когда low[v]==tin[v]. Объясните и дайте рабочую реализацию.
2 ответа
Идея. В DFS каждой вершине даём время входа tin[v]. low[v] — минимальный tin, достижимый из поддерева v, идя по любым рёбрам дерева/назад/вперёд, но только через вершины, ещё лежащие на стеке (т.е. не из уже закрытых SCC). Если low[v] == tin[v], то v — «корень» своей SCC: из поддерева v нельзя «выпрыгнуть» в более раннюю живую вершину, значит всё, что лежит на стеке выше v (включая v), образует одну компоненту.
int timer = 0;
vector<int> tin, low, comp;
vector<bool> onstk;
stack<int> st;
vector<int> g[N];
int scc_cnt = 0;
void dfs(int v) {
tin[v] = low[v] = timer++;
st.push(v); onstk[v] = true;
for (int u : g[v]) {
if (tin[u] == -1) { // дерево
dfs(u);
low[v] = min(low[v], low[u]);
} else if (onstk[u]) { // обратное/перекрёстное в живую вершину
low[v] = min(low[v], tin[u]);
}
}
if (low[v] == tin[v]) { // v — корень SCC
while (true) {
int u = st.top(); st.pop(); onstk[u] = false;
comp[u] = scc_cnt;
if (u == v) break;
}
scc_cnt++;
}
}
Инициализация: tin.assign(n,-1); low.assign(n,0); comp.assign(n,-1); onstk.assign(n,false); и for v: if tin[v]==-1 dfs(v).
Сложность O(V+E), память O(V). Ключевая тонкость — на стеке держим вершины незакрытых SCC; обновлять low по tin[u] можно только если onstk[u], иначе утянем low в чужую компоненту.
Грабля: при больших V замените рекурсию на итеративную (см. вопросы про переполнение стека), глубина может быть V.
Сравнение с Косарайю, чтобы выбрать осознанно. Косарайю = два DFS: первый на исходном графе выписывает вершины по времени завершения (как в топсорте), второй — на транспонированном графе, обходя вершины в порядке убывания времени завершения; каждый запуск второго DFS = одна SCC. Тоже O(V+E), но строит транспонированный граф (×2 память на рёбра) и делает два прохода.
Косарайю проще понять и доказать (через свойство «времён завершения в конденсации»), Тарьян — за один проход и без транспонирования. На практике на CF берут любой; если важна константа памяти/времени — Тарьян. И помните: номера SCC у Тарьяна выдаются в обратном топологическом порядке конденсации (корни-SCC закрываются последними), что иногда удобно сразу использовать.