跳转至

割点,点双连通分量

割点

在无向图上,如果删除某个点(包括所有和它相连的边),图的连通性受到破坏(即分解成多个部分),那么就称删除的这个点为图的割点

为了求出割点,我们仍然使用\(tarjan\)算法。

考虑 \(low\) 的定义,通常情况下,若一个节点 \(u\) 存在一个儿子 \(v\) ,有

\[ low[v] \ge pre[u] \]

这代表着,\(v\) 和它的子树被限制在了 \(u\)\(u\) 以下,而不能连接到 \(u\) 以上的任何节点。这样,只要移除节点 \(u\) ,图就会分成至少两个部分:\(u\)和它的祖先兄弟们、\(v\) 和它的孩子们,这样图的连通性就受到了破坏,根据定义 \(u\)​ 就是割点。

然而,存在一种特殊情况,即 \(u\)\(dfs\) 的起点,并且 \(u\) 只连接着一个点双分量。这时,\(u\) 不是割点。为了防止误判,应在 \(dfs\) 函数结尾对此种情况进行特判。

例题

P3388 【模板】割点(割顶)

代码
#include<iostream>
#include<vector>
using namespace std;
const int N = 2E4 + 10;

int n, m, dt;
int pre[N], low[N];
bool isCut[N];
vector<int> adj[N];

void tarjan(int u, int f){

    int child = 0;
    low[u] = pre[u] = ++dt;
    for(auto v : adj[u]){
        if(pre[v] == 0){
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] >= pre[u]){
                isCut[u] = 1;
            }
            child++;
        }
        // 因为是无向图,需要排除直接返回父亲的情况,这个判断在有向图的tarjan中并不存在
        else if(v != f){
            low[u] = min(low[u], pre[v]);
        }
    }
    // 特判 u 是根节点且只有一个真儿子
    if(f == 0 && child == 1) isCut[u] = 0;
}

int main(){

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

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

    int cnt = 0;
    for(int i = 1; i <= n; i++){
        if(isCut[i]) cnt++;
    }

    cout << cnt << endl;
    for(int i = 1; i <= n; i++){
        if(isCut[i]) cout << i << ' ';
    }
    cout << endl;

    return 0;
}

点双连通分量

若一个无向图的一个子图满足其中任意两个不同的节点 \(u\)\(v\) 之间都有至少两条路径(且没有重合的节点)能够相互到达,那么称这个子图为一个点双连通分量(bcc)。

性质:两个不同的点双之间的连接部分一定是割点。

我们在此提供两种方法求得点双分量:

  • 标记所有割点,并循环进行 \(dfs\)(不能越过标记的割点)。一次 \(dfs\) 访问到的所有节点就是一个点双分量。
  • 仿照 scc 的做法,在遇到每一个节点时将其入栈。当发现节点 \(u\) 的一个儿子 \(v\) 满足 \(low[v]\ge pre[u]\) 时,就把 \(v\) 及以上的节点都划入一个 bcc

第一种方法需要额外编写一个 dfs 函数,第二种方法可以快速统计每个 bcc 连接了多少个割点,连接了哪些割点,但需要特判根节点。

例题

P3469 [POI2008] BLO-Blockade

代码
#include<iostream>
#include<vector>
#define int long long
using namespace std;
const int N = 1E5 + 10;

int n, m, dt;
int pre[N], low[N], sz[N], ans[N];
vector<int> adj[N];

int tarjan(int u, int f){

    long long sum = 0, temp = 1;
    sz[u] = 1;
    low[u] = pre[u] = ++dt;
    for(auto v : adj[u]){
        if(v == f) continue;
        if(!pre[v]){
            sz[u] += tarjan(v, u);
            if(low[v] >= pre[u]){
                sum += sz[v] * sz[v];
                temp += sz[v];
            }
            low[u] = min(low[u], low[v]);
        }
        else if(pre[v] < pre[u]){
            low[u] = min(low[u], pre[v]);
        }
    }

    // 用 temp 先累加子节点中的点双分量大小总和,再用总体减去自己,以及子节点中的点双,剩下的就是祖先连通块的大小。
    temp = n - temp;
    // temp 祖先的连通块
    // sum 所有儿子连通块的平方和
    // 总体减去能相互到达的点的个数
    ans[u] = n * n - (temp * temp + sum + 1);

    return sz[u];
}

signed main(){

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

    tarjan(1, 0);

    for(int i = 1; i <= n; i++){
        cout << ans[i] << endl;
    }

    return 0;
}

P3225 [HNOI2012] 矿场搭建

思路:

为了保证任何一个节点坍塌时,其他节点都能到达一个安全出口,考虑几种情况:

  • 只有一个双连通分量,修两个,一个塌了还能用另一个。

  • 坍塌的节点是割点(着重考虑)

  • 坍塌的节点不是割点,不影响其他节点的连通性

若坍塌的节点是割点,若其连接的一个连通分量还连接着其他分量,那么这个连通分量的节点就可以从另一个割点离开

但是若坍塌的割点是这个连通分量连接的唯一割点,那么这个分量里应该至少有一个安全出口(排除割点)

答案即是叶子节点的数量;每个叶子分量(只连接了一个割点)的非割点节点个数累乘。

技巧: \(Vector\) 数组也可以直接用 \(memset\) 初始化!

易错点:

  • 初始化要完整,看好哪些变量和数组要初始化
  • 在割点判断中,只有一个儿子的根节点较为特殊,注意是否需要特殊处理
代码
#include<iostream>
#include<vector>
#include<stack>
#include<cstring>
using namespace std;
const int N = 1E3 + 10;
const int M = 505;

int n, m, dt, bccCnt;
int pre[N], low[N], isCut[N], isLeaf[N];
vector<int> adj[N];
vector<int> bcc[N];
stack<int> sta;

void tarjan(int u, int f){

    sta.push(u);
    low[u] = pre[u] = ++dt;
    int child = 0;
    for(auto v : adj[u]){
        if(v == f) continue;
        if(pre[v]){
            low[u] = min(low[u], pre[v]);
        }
        else{
            child++;
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] >= pre[u]){
                isCut[u] = 1;
                bcc[++bccCnt].push_back(u);
                int tmp = 1; // u是一个割点
                while(sta.top() != v){
                    bcc[bccCnt].push_back(sta.top());
                    if(isCut[sta.top()]) tmp++;
                    sta.pop();
                }
                bcc[bccCnt].push_back(sta.top());
                if(isCut[sta.top()]) tmp++;
                sta.pop();
                if(tmp == 1) isLeaf[bccCnt] = 1;
            }
        }
    }

// 若根节点只有一个有效儿子
// 在上面的tarjan算法中,这个根节点将会被计算为割点,然而实际上不是,因为它没有父亲(f=0)
// 然而,在上面tarjan内部统计isLeaf时,代码并不知道u不是割点,可能会导致tmp统计的比真实割点数量多一个
// 导致isLeaf被统计为0,故此处需要重新统计。
    if(f == 0 && child == 1){
        isCut[u] = 0;
        int tmp = 0;
        for(auto j : bcc[bccCnt]){
            if(isCut[j]) tmp++;
        }
        if(tmp == 1) isLeaf[bccCnt] = 1;
    }

}

int main(){

    int CaseNum = 0;
    while(1){
//        for(int i = 1; i < N; i++){
//            bcc[i].clear();
//        }
// 注意,Vector也可以直接用memset初始化
        memset(bcc, 0, sizeof(bcc));
        memset(adj, 0, sizeof(adj));
        memset(isCut, 0, sizeof(isCut));
        memset(pre, 0, sizeof(pre));
        memset(low, 0, sizeof(low));
        memset(isLeaf, 0, sizeof(isLeaf));
        dt = 0;
        bccCnt = 0;

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

        tarjan(1, 0);

        cout << "Case " << ++CaseNum << ": ";
        if(bccCnt == 1){
            cout << "2 ";
            cout << bcc[1].size() * (bcc[1].size() - 1) / 2 << endl;
            continue;
        }

        long long leafCnt = 0, sum = 1;
        for(int i = 1; i <= bccCnt; i++){
            if(isLeaf[i]){
                leafCnt++;
                if(bcc[i].size() != 1) sum *= bcc[i].size() - 1;
                else sum *= 1; // 可注释
            }
        }
        cout << leafCnt << ' ' << sum << endl;
    }

    return 0;
}