Tarjan( 系列4)求点双 / 边双连通分量

双连通分量

边/点双连通: 如果无论删去哪个边/点 (只能删去一个), 两点仍然连通.

边/点双连通分量: 任意两个点都满足条件的子图.

其中边双连通具有传递性. =>

在这里插入图片描述

AB, BC, AC 边双连通; AB, BC 点双连通

由此可见: 点双连通两点一定在一个环上, 而边双连通的两点可能是在一个扭起来的环上(如图, 点多次使用, 但边只用一次的环, 可以拆解为多个环) (见题 link, 只需边双连通就能满足 Lun 边的要求.)

也就是说, 边双可能含有割点. 不同点双可能公用某点 (割点), 边双无公用.

1. 求边双连通分量

  1. Tarjan 算法 1

    //消耗内存较大, 内存较小时可以用差分法求桥, 并查集优化
    // ac [https://www.luogu.com.cn/problem/P8436]
    int dfsn[Maxn], low[Maxn], vis[Maxn], cnt = 0;
    vector<vector<int>>ans;
    vector<int>eg[Maxn];
    set<pair<int, int>>is_bridge;
    void tarjan(int tis, int fa)
    {   low[tis] = dfsn[tis] = ++cnt;
        for (auto c : eg[tis]) {
            if (!dfsn[c]) {
                tarjan(c, tis);
                low[tis] = min(low[c], low[tis]);
                if (low[c] > dfsn[tis]) {
                    //(tis,c) is bridge
                    is_bridge.emplace(tis, c);
                    is_bridge.emplace(c, tis);
                }
            }
            else if (fa != c) {
                low[tis] = min(low[tis], dfsn[c]);//反向边仅在此出现
                if (is_bridge.count({c, tis})) { // 处理重边
                    is_bridge.erase(is_bridge.find({c, tis}));
                    is_bridge.erase(is_bridge.find({tis, c}));
                }
            }
        }
    }
    void dfs(int tis, int num) {
        vis[tis] = 1;
        ans[num].emplace_back(tis);
        for (auto &c : eg[tis]) {
            if (vis[c])continue;
            if (is_bridge.count({c, tis}))continue;
            dfs(c, num);
        }
    }
    signed main()
    {
        ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
        int n, m, v, u;
        cin >> n >> m;
        for (int i = 1; i <= m; ++i) {
            cin >> v >> u;
            if (u == v)continue; // 去掉自环
            eg[u].emplace_back(v);
            eg[v].emplace_back(u);
        }
    
    
        for (int i = 1; i <= n; ++i) {
            if (dfsn[i])continue;
            tarjan(i, i);
        }
    
        debug(is_bridge)
    
        int num = 0;
        for (int i = 1; i <= n; ++i) {
            if (vis[i])continue;
            ans.emplace_back(vector<int>());
            dfs(i, ++num - 1);
        }
    }
    /* ================================== */
    //前向星版
    const int maxn = 5e5 + 10, maxm = 4e6 + 10;
    int dfsn[maxn], low[maxn], head[maxn], vis[maxn];
    struct edge {
        int nxt, to;
    } e[maxm];
    bool b[maxm];
    vector< vector<int > > Ans;
    int cnt = 1, id = 0, ans = 0;
    void add(int u, int v)
    {
        e[++cnt].nxt = head[u];
        e[cnt].to = v;
        head[u] = cnt;
    }
    void tarjan(int now, int in_edge)
    {
        dfsn[now] = low[now] = ++id;
        for (int i = head[now]; i > 0; i = e[i].nxt)
        {
            int to = e[i].to;
            if (!dfsn[to])
            {
                tarjan(to, i);
                if (dfsn[now] < low[ to])
                    b[i] = b[i ^ 1] = 1;
                low[now] = min(low[now], low[to]);
            } else if (i != (1 ^ in_edge))
                low[now] = min(low[now], dfsn[to]);
        }
    }
    void dfs(int now, int num)
    {
        vis[now] = 1;
        Ans[num].push_back(now);
        for (int i = head[now]; i > 0; i = e[i].nxt)
        {
            int to = e[i].to;
            if (vis[to] || b[i]) continue;
            dfs(to, num);
        }
    }
    //输入
    for (int i = 1; i <= m; i++)
    {
        int x, y;
        cin >> x >> y;
        if (x == y) continue;
        add(x, y);
        add(y, x);
    }
    

2.点双

两个性质:

  1. 两个点双最多只有一个公共点,且一定是割点。
  2. 对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根 (原理参照 Tarjan 求割点 )。

故: 一个点是割点时, 它一定是点双分量的根. 如果再往上包含它的父亲, 那它就成为分量内的割点了. 操作类似求有向图的强连通分量, 用一个栈去存点. =>Tarjan( 系列1) 求强连通子图

// AC [https://www.luogu.com.cn/problem/P8435]
const int Maxn = 5e5 + 10;

int n, m, v, u;
vector<int>eg[Maxn];
vector<vector<int>>dcc;
int dfsn[Maxn], low[Maxn], st[Maxn];
int dfsn_cnt = 0, dcc_cnt = 0, top = 0;

void tarjan(int tis, bool rot = true) {
    low[tis] = dfsn[tis] = ++dfsn_cnt;
    st[top++] = tis;
    if (rot && eg[tis].empty()) { // 根为孤立点
        dcc.emplace_back(vector<int>());
        dcc[dcc_cnt++].emplace_back(tis);
    }
    for (auto c : eg[tis]) {
        if (!dfsn[c]) {
            tarjan(c, 0);
            low[tis] = min(low[c], low[tis]);
            if (low[c] >= dfsn[tis]) { // tis 为割开 tis->c 的割点
                dcc.emplace_back(vector<int>());
                while (top && st[top - 1] != c) {
                    dcc[dcc_cnt].emplace_back(st[--top]);
                }
                dcc[dcc_cnt].emplace_back(st[--top]);
                dcc[dcc_cnt].emplace_back(tis); // 割点可能与其他分量公用, 不从栈中移除
                dcc_cnt++;
            }
        }
        else
            low[tis] = min(low[tis], dfsn[c]);
    }
}

signed main()
{
    ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    cin >> n >> m;

    for (int i = 1; i <= m; ++i) {
        cin >> v >> u;
        if (u == v)continue;
        eg[u].emplace_back(v);
        eg[v].emplace_back(u);
    }

    for (int i = 1; i <= n; ++i) {
        if (!dfsn[i]) {
            tarjan(i);
        }
    }
}
  • 21
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值