无向图的割点与点双连通分量

割点:对于一个无向图,如果删除某个节点后,连通块个数增加了,则该点为割点(也称割顶)。

割点的判定

  • 对于 DFS 树中的非根节点 \(u\),当至少存在一个子节点 \(v\),满足 \(low_v \ge dfn_u\) 时,\(u\) 为割点。
  • 对于 DFS 树的根节点 \(u\),当其存在至少两个子节点满足上述条件,则 \(u\) 为割点。

为什么根节点和非根节点条件不同:因为非根节点的“上面”有一个块,所以如果 \(low_v \ge dfn_u\) 说明 \(v\) 只能通过 \(u\) 与“上面”那块连通;而根节点没有“上面”,所以需要至少两个这样的 \(v\)

void tarjan(int u, int root) {
    dfn[u] = low[u] = ++tot;
    int ch = 0;
    for (int v : g[u]) {
        if (!dfn[v]) { // u-v为树边
            tarjan(v, root);        
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ch++;
                if (u != root || ch > 1) {
                    cut[u] = true;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}

习题:P3388 【模板】割点(割顶)

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::min;
const int N = 20005;
vector<int> g[N];
bool cut[N];
int dfn[N], low[N], tot;
void tarjan(int u, int root) {
    dfn[u] = low[u] = ++tot;
    int ch = 0;
    for (int v : g[u]) {
        if (!dfn[v]) { // u-v为树边
            tarjan(v, root);        
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ch++;
                if (u != root || ch > 1) {
                    cut[u] = true;
                }
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}
int main()
{
    int n, m; scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y; scanf("%d%d", &x, &y);
        g[x].push_back(y); g[y].push_back(x);
    }
    for (int i = 1; i <= n; i++) if (!dfn[i]) tarjan(i, i);
    int ans = 0;
    for (int i = 1; i <= n; i++) if (cut[i]) ans++;
    printf("%d\n", ans);
    for (int i = 1; i <= n; i++) if (cut[i]) printf("%d ", i);
    return 0;
}

若一张无向图不存在割点,则称其为点双连通图。无向图的极大点双连通子图称为点双连通分量(vertex Double Connected Components, vDCC)

image

void tarjan(int u, int root) {
    dfn[u] = low[u] = ++tot;
    int ch = 0;
    s.push(u);
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v, root);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ch++; cnt++;
                while (s.top() != v) {
                    ans[cnt].push_back(s.top()); s.pop();
                }
                ans[cnt].push_back(v); s.pop();
                ans[cnt].push_back(u); // 栈顶到v的部分和u共同组成一个点双连通分量
            }
        } else if (ins[v]) {
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (u == root && ch == 0) { // 孤立点要特判
        cnt++; ans[cnt].push_back(u);
    }
}

习题:P8435 【模板】点双连通分量

参考代码
#include <cstdio>
#include <algorithm>
#include <vector>
#include <stack>
using namespace std;
const int N = 500005;
vector<int> g[N], ans[N];
stack<int> s;
int low[N], dfn[N], tot, cnt;
void tarjan(int u, int root) {
    dfn[u] = low[u] = ++tot;
    int ch = 0;
    s.push(u);
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v, root);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ch++; cnt++;
                while (s.top() != v) {
                    ans[cnt].push_back(s.top()); s.pop();
                }
                ans[cnt].push_back(v); s.pop();
                ans[cnt].push_back(u);
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
    if (u == root && ch == 0) {
        cnt++; ans[cnt].push_back(u);
    }
}
int main()
{
    int n, m;
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= m; i++) {
        int x, y; scanf("%d%d", &x, &y);
        g[x].push_back(y); g[y].push_back(x);
    }
    for (int i = 1; i <= n; i++) 
        if (!dfn[i]) tarjan(i, i);
    printf("%d\n", cnt);
    for (int i = 1; i <= cnt; i++) {
        printf("%d", (int)ans[i].size());
        for (int j : ans[i]) printf(" %d", j);
        printf("\n");
    }
    return 0;
}

习题:P5058 [ZJOI2004] 嗅探器

解题思路

本题相当于求能将 \(a\)\(b\) 隔开的割点。

回想一下找割点的算法,如何判断 \(a\)\(b\) 刚好分布在找到的割点两侧?

考虑以 \(a\) 为搜索树的根节点,这样一来当根据割点条件发现某个割点 \(u\) 时,\(dfn_a\) 必然小于 \(dfn_u\),此时如果 \(b\) 在以 \(u\) 为根的 DFS 子树内,则 \(a\)\(b\) 就分布在割点两侧。因此在割点的基础条件上增加 \(dfn_b \ge dfn_u\) 的判断即可。

参考代码
#include <cstdio>
#include <vector>
#include <algorithm>
using std::vector;
using std::min;
const int N = 200005;
vector<int> g[N];
int a, b, dfn[N], low[N], tot;
bool cut[N];
void tarjan(int u) {
    dfn[u] = low[u] = ++tot;
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u] && u != a && dfn[b] >= dfn[v]) {
                cut[u] = true;
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}
int main()
{
    int n;
    scanf("%d", &n);
    while (true) {
        int i, j;
        scanf("%d%d", &i, &j);
        if (i == 0 && j == 0) break;
        g[i].push_back(j); g[j].push_back(i);
    }
    scanf("%d%d", &a, &b);
    tarjan(a);
    for (int i = 1; i <= n; i++) 
        if (cut[i]) {
            printf("%d\n", i);
            return 0;
        }
    printf("No solution\n");
    return 0;
}

习题:P3225 [HNOI2012] 矿场搭建

解题思路

整张图可以拆分成若干个点双连通分量的组合,对于分解出来的每个 vDCC 来说,如果其中没有割点,说明该连通块整个构成一个点双连通分量,此时需要在其中放置两个救援出口(这样当某个点坍塌后剩下点都能通向另一个出口),设大小为 \(s\),则其中任选两个点作为出口的方案数为 \(\dfrac{s(s-1)}{2}\);如果其中有一个割点,说明该分量通过那个割点与图中其他部分连接,如果有不止一个割点,则说明跟多个其他的点双连通分量连接,而一个连通块中如果由多个 vDCC 组成,则只需要在每个割点数为 \(1\) 的 vDCC 中分别选一个非割点的位置放置一个出口即可(这样一来当坍塌的是某个 vDCC 中的非割点时,该 vDCC 中其他点可以沿着割点去到其他 vDCC 中的某个出口,如果坍塌的刚好是割点,那么该割点涉及到的每个 vDCC 中都预先准备过出口),每个 vDCC 中可以选择的方案数是 \(s-1\),每个 vDCC 之间独立,方案数累乘。

参考代码
#include <cstdio>
#include <vector>
#include <stack>
#include <algorithm>
using std::vector;
using std::stack;
using std::min;
using ull = unsigned long long;
const int N = 1005;
vector<int> g[N], vDCC[N];
int dfn[N], low[N], tot, ans1, cnt;
bool cut[N];
ull ans2;
stack<int> s;
void tarjan(int u, int root) {
    dfn[u] = low[u] = ++tot;
    s.push(u);
    int ch = 0;
    for (int v : g[u]) {
        if (!dfn[v]) {
            tarjan(v, root);
            low[u] = min(low[u], low[v]);
            if (low[v] >= dfn[u]) {
                ch++;
                if (ch > 1 || u != root) { // u是割点
                    cut[u] = true;
                } 
                cnt++;
                while (true) {
                    int t = s.top(); s.pop();
                    vDCC[cnt].push_back(t);
                    if (t == v) break;
                }
                vDCC[cnt].push_back(u);
            }
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }
}
int main()
{
    int n, case_id = 0;
    while (true) {
        scanf("%d", &n);
        if (n == 0) break;
        case_id++;
        tot = 0; ans1 = 0; ans2 = 1; cnt = 0;
        for (int i = 1; i <= 1000; i++) {
            g[i].clear(); vDCC[i].clear();
            dfn[i] = low[i] = 0;
            cut[i] = false;
        }
        for (int i = 1; i <= n; i++) {
            int x, y; scanf("%d%d", &x, &y);
            g[x].push_back(y); g[y].push_back(x);
        }
        while (!s.empty()) s.pop();
        for (int i = 1; i <= 1000; i++)
            if (!dfn[i]) tarjan(i, i);
        for (int i = 1; i <= cnt; i++) {
            int cut_cnt = 0;
            for (int j : vDCC[i]) {
                if (cut[j]) cut_cnt++;
            }
            ull sz = vDCC[i].size();
            if (cut_cnt == 0) {
                ans1 += 2; ans2 *= (sz - 1) * sz / 2;
            } else if (cut_cnt == 1) {
                ans1++; ans2 *= (ull)(sz - 1);
            }
        }
        printf("Case %d: %d %llu\n", case_id, ans1, ans2);
    }
}
posted @ 2025-04-19 10:39  RonChen  阅读(96)  评论(0)    收藏  举报