Tarjan

求强连通分量

适用于单向边。

维护两个数组 \(dfn_u\) 表示 \(u\) 在搜索树上的访问顺序,\(low_u\) 表示在 \(u\) 的子树里的点经过一条反祖边能到达的点的 \(dfn\) 最小值(只能经过反祖边;但其实经过了前向边是无所谓的,不会影响 \(low_u\) 的值,横叉边是绝对不能经过的)。

当一个点 \(low_u=dfn_u\) 时,他就是某个强连通分量的根

怎么知道有那些点呢?维护一个栈,每当出现一个强连通分量,就把点出栈直到点为这个强连通分量的根

其实不难发现,如果点 \(u\) 遍历到的点 \(v\) 还在栈里的话,\(u,v\) 肯定是有祖先关系的,那么 \((u,v)\) 就是一条反祖边前向边,那么就可以更新 \(low_u=\min(low_u,dfn_v)\)

如果 \(v\) 在搜索树上是 \(u\) 的儿子,那么有 \(low_u=\min(low_u,low_v)\)

P3387 【模板】缩点

考虑求出强连通分量后,把一个强连通分量的点缩成一个点。

然后这个图就变成了有向无环图,做拓扑排序即可。

#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

vector< int> G[N], scc[N], sta, E[N];

int dfn[N], low[N], vis[N], a[N], col[N], in[N], dp[N];
int sum[N];

int tot, cnt;

void tarjan( int u) {
    sta.push_back(u), vis[u] = 1;
    dfn[u] = low[u] = ++ tot;
    
    for ( auto v : G[u]) {
        if (! dfn[v]) tarjan(v), low[u] = min(low[u], low[v]);
        else if (vis[v]) low[u] = min(low[u], dfn[v]);
    }
    
    if (dfn[u] != low[u]) return ;
    
    int v; cnt ++;
    
    do {
        v = sta.back(); sta.pop_back(), vis[v] = 0;
        scc[cnt].push_back(v);
        sum[cnt] += a[v];
    } while (v != u) ;
}

int n, m;

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;

    for ( int i = 1; i <= n; i ++) cin >> a[i];

    for ( int i = 1, u, v; i <= m; i ++) {
        cin >> u >> v;
        G[u].push_back(v);
    }

    for ( int i = 1; i <= n; i ++)
        if (! dfn[i]) tarjan(i);

    for ( int i = 1; i <= cnt; i ++)
        for ( auto u : scc[i]) col[u] = i;

    for ( int u = 1; u <= n; u ++)  
        for ( auto v : G[u])
            if (col[u] != col[v]) E[col[u]].push_back(col[v]), in[col[v]] ++;  

    queue< int> q;

    for ( int i = 1; i <= cnt; i ++) if (! in[i]) q.push(i);

    for ( int i = 1; i <= cnt; i ++) dp[i] = sum[i];

    while (q.size()) {
        int u = q.front(); q.pop();

        for ( auto v : E[u]) {
            dp[v] = max(dp[v], dp[u] + sum[v]);

            if (! -- in[v]) q.push(v);
        }
    }

    int ans = 0;
    
    for ( int i = 1; i <= cnt; i ++) ans = max(ans, dp[i]);

    cout << ans;

    return 0;
}

割点

适用于双向边。

也是维护两个数组 \(dfn,low\)

\(dfn_u\) 意义相同,而 \(low_u\) 表示在 \(u\) 的子树里的点不经过搜索树上 \(u\) 的父亲能到达的点的 \(dfn\) 最小值。

如何判断割点呢?

如果 \(u\) 为搜索树的根,如果子树个数大于 \(1\),那么 \(u\) 即为割点。

否则,对于一组父子 \((u,v)\) 并且如果 \(low_v\ge dfn_u\),那么 \(u\) 即为割点。

P3388 【模板】割点(割顶)

#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e5 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

vector< int> G[N];

int dfn[N], low[N], cut[N];
int tot, sum;

void tarjan( int u, int fu, int rt) {
    dfn[u] = low[u] = ++ tot;
    int siz = 0;
    
    for ( auto v : G[u]) {
        if (v == fu) continue ;
        
        if (! dfn[v]) {
            tarjan(v, u, rt);
            siz ++;
            low[u] = min(low[u], low[v]);
            
            if ((u == rt && siz > 1) || (u != rt && low[v] >= dfn[u]))
           		cut[u] = 1;
        } else low[u] = min(low[u], dfn[v]);
    }
}

int n, m;

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;

    for ( int i = 1, u, v; i <= m; i ++) {
        cin >> u >> v;

        G[u].push_back(v), G[v].push_back(u);
    }

    for ( int i = 1; i <= n; i ++)
        if (! dfn[i]) tarjan(i, 0, i);

    for ( int i = 1; i <= n; i ++) sum += cut[i];

    cout << sum << '\n';

    for ( int i = 1; i <= n; i ++) if (cut[i]) cout << i << " ";

    return 0;
}

点双连通分量

还是和求割点差不多,但是因为割点会处于多个点双,所以出栈时不能把割点出栈了。(所以注意图不连通多测时栈的清空)。

还有就是注意,有些连通块只有一个点,这也是个点双。

然后就是,求点双时,要假装根节点是一个割点(即使根节点并不是割点),这是因为把根节点看成割点,才能让根节点所在的点双连通分量被判定到。

P8435 【模板】点双连通分量

#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 1e6 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

vector< int> G[N];

int low[N], dfn[N];
vector< int> sta, dcc[N];

int cnt, tot;

void tarjan( int u, int fu) {
    sta.push_back(u);
    dfn[u] = low[u] = ++ tot;
    int siz = 0;
    
    for ( auto v : G[u]) {
        if (v == fu) continue ;
        
        if (! dfn[v]) {
            tarjan(v, u);
            siz ++;
            low[u] = min(low[u], low[v]);
            
            if (low[v] >= dfn[u]) {
                int p; cnt ++;

                do {
                    p = sta.back(), sta.pop_back();
                    dcc[cnt].push_back(p);
                } while (p != v) ;
                
                dcc[cnt].push_back(u);
            }
        } else low[u] = min(low[u], dfn[v]);
    }
    
    if (! fu && ! siz) dcc[++ cnt].push_back(u);
}

int n, m;

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;

    for ( int i = 1, u, v; i <= m; i ++) {
        cin >> u >> v;

        G[u].push_back(v), G[v].push_back(u);
    }

    for ( int i = 1; i <= n; i ++)
        if (! dfn[i]) {
            sta.clear();
            tarjan(i, 0);
        }
    
    cout << cnt << '\n';

    for ( int i = 1; i <= cnt; i ++, cout << '\n') {
        cout << dcc[i].size() << " ";

        for ( auto u : dcc[i]) cout << u << " ";
    }

    return 0;
}

割边 & 边双连通分量

这两个放一起说了,因为求了割边就可以直接求边双连通分量。

适用于双向边。

\(dfn,low\) 定义和割点的相同。

对于一组父子 \((u,v)\) 如果 \(low_v > dfn_u\) 则边 \((u,v)\) 为割边。

那么怎么求边双连通分量呢?

先求一次割边,然后把这些边割掉,剩下的若干个连通块就是边双连通分量。

为什么可以呢?因为一个点只会出现在一个边双里

P8436 【模板】边双连通分量

#include <bits/stdc++.h>

void Freopen() {
    freopen("", "r", stdin);
    freopen("", "w", stdout);
}

using namespace std;
const int N = 2e6 + 10, M = 2e5 + 10, inf = 1e9, mod = 998244353;

vector< pair< int, int> > G[N];
vector< int> bcc[N];

int cut[N * 2], dfn[N], low[N];

int tot, cnt;

void tarjan( int u, int fe) {
    dfn[u] = low[u] = ++ tot;
    
    for ( auto i : G[u]) {
        int v = i.first, e = i.second;
        
        if (e == (fe ^ 1)) continue ;
        
        if (! dfn[v]) {
            tarjan(v, e);
            low[u] = min(low[u], low[v]);
            
            if (low[v] > dfn[u]) cut[e] = cut[e ^ 1] = 1;
        } else low[u] = min(low[u], dfn[v]);
    }
}

int vis[N];

void dfs( int u, int col) {
    if (vis[u]) return ;
    vis[u] = 1, bcc[col].push_back(u);

    for ( auto i : G[u]) {
        int v = i.first, e = i.second;

        if (cut[e]) continue ;

        dfs(v, col);
    }
}

int n, m;

signed main() {
    ios :: sync_with_stdio(false);
    cin.tie(0), cout.tie(0);

    cin >> n >> m;

    for ( int i = 1, u, v; i <= m; i ++) {
        cin >> u >> v;
        G[u].push_back({v, 2 * i}), G[v].push_back({u, 2 * i + 1});
    }

    for ( int i = 1; i <= n; i ++)
        if (! dfn[i]) tarjan(i, 0);

    for ( int i = 1; i <= n; i ++)
        if (! vis[i]) dfs(i, ++ cnt);

    cout << cnt << '\n';

    for ( int i = 1; i <= cnt; i ++, cout << '\n') {
        cout << bcc[i].size() << " ";

        for ( auto u : bcc[i]) cout << u << " ";
    }

    return 0;
}
posted @ 2025-08-14 09:42  咚咚的锵  阅读(6)  评论(0)    收藏  举报