无向图的割顶、桥、BCC和eBCC相关

本文介绍了割顶、桥、点双连通分量和边双连通分量的概念及其应用,包括求解方法和实际问题的解决策略,如添加最少边使图成为边双、定向使图成为强连通分量等问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个例题代码待填

割顶:若去掉一个点和与这个点相连的边后,图不再连通,则这个点是割顶。
​  求法:若节点\(u\)存在一棵子树\(v\)满足\(v\)中所有节点的回边都指向\(u\)及以下的节点(即\(low[v] \ge pre[u]\)),则\(u\)是割顶;所以一次DFS即可求出所有割顶。但注意一个特殊情况!根节点是割顶当且仅当它的子节点数严格大于1。

:去掉这条边,图不再连通。
  求法:若节点\(u\)存在一棵子树\(v\)满足\(v\)中所有节点的回边都指向\(u\)以下(不含\(u\))的节点(即\(low[v] > pre[u]\)),则边\((u, v)\)是桥;所以一次DFS即可求出所有桥。

BCC(点-双连通分量):连通分量中任意两点间都至少有两条“点不重复路径”。
  求法:点双等价于不含割顶的极大子图。但一个问题是割顶本身可能属于多个点双。解决方法是把分开,即保存一个边的栈。

eBCC(边-双连通分量):连通分量中任意两点间都至少有两条“边不重复路径”。
  求法:边双等价于不含桥的极大子图。一条点不可能分属多个边双(否则可以将这些个边双合并)。所以一次DFS求出所有桥,再来一次DFS把边双分开即可(DFS到桥就不走)。

 void Tarjan_eBCC(int x, int dad) {
    pre[x] = low[x] = ++DFS_clock;
    reg(i,x) {
        int y = e[i].to;
        if (y == dad) continue;
        if (pre[y])
            low[x] = min(low[x], pre[y]);
        else {
            Tarjan_eBCC(y, x);
            low[x] = min(low[x], low[y]);
            if (low[y] > pre[x]) mark_bridge(x, y);
        }
    }
 }

 


 

例题(均要求给出O(\(n+m\))的做法)

POJ 3352:给出一张无向图,问最少需要添加多少条边使得整张图成为边双。
  sol:首先将边双缩点,就变成了一棵树(不是DAG!)。把一棵树添加尽量少的边变成边双,一个最优构造方法是把相邻的叶子节点相邻。但注意那个特殊情况!如果根节点有且只有一个儿子,那还要把根节点和任一个儿子连一下。综上可以看出,需要加边的点都是度数为1的。因此,若设度为1的节点数为\(x\),则答案是\((x+1)/2\)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

 #define rep(i,a,b) for (int i = a; i <= b; i++) 
 #define dep(i,a,b) for (int i = a; i >= b; i--)
 #define read(x) scanf("%d", &x)
 #define fill(a,x) memset(a, x, sizeof(a))

 const int N = 1000 + 5, M = 1000 + 5;

 int n, m, u, v, es, DFS_clock, cnt, ans;
 int pre[N], last[N], low[N], bel[N], deg[N];
 bool is_bridge[N][N], vis[N];

 struct Edge { int from, to, pre; } e[M*2];
 void ine(int a, int b) {
    es++;
    e[es].from = a; e[es].to = b; e[es].pre = last[a];
    last[a] = es;
 }
 void ine2(int a, int b) {
    ine(a, b);
    ine(b, a);
 }
 #define reg(i,x) for (int i = last[x]; i; i = e[i].pre)

 void init() {
    fill(last, 0); fill(pre, 0); fill(deg, 0);
    fill(vis, false); fill(is_bridge, false);
    fill(low, 0x3f);
    DFS_clock = es = cnt = ans = 0;
 }

 void Tarjan_eBCC(int x, int dad) {
    pre[x] = low[x] = ++DFS_clock;
    reg(i,x) {
        int y = e[i].to;
        if (y == dad) continue;
        if (pre[y])
            low[x] = min(low[x], pre[y]);
        else {
            Tarjan_eBCC(y, x);
            low[x] = min(low[x], low[y]);
            if (low[y] > pre[x]) is_bridge[x][y] = is_bridge[y][x] = true;
        }
    }
 }

 void DFS(int x) {
    bel[x] = cnt;
    reg(i,x) {
        int y = e[i].to;
        if (bel[y] || is_bridge[x][y]) continue;
        DFS(y);
    }
 }

 void count_eBCC() {
    rep(i,1,n) if (!bel[i]) {
        cnt++;
        DFS(i);
    }
    for (int i = 1; i <= es; i += 2) {
        int u = e[i].from, bu = bel[u], v = e[i].to, bv = bel[v];
        if (bu != bv) deg[bu]++, deg[bv]++;
    }
    rep(i,1,cnt) if (deg[i] == 1) ans++;
    ans = (ans + 1)/2;
 }

int main()
{
    init();

    read(n); read(m);
    rep(i,1,m) read(u), read(v), ine2(u, v);

    Tarjan_eBCC(1, 0);
    count_eBCC();

    printf("%d\n", ans);

    return 0;
}

UVa 10972:给出一张无向图,要求将所有边定向,并添加最少的边,使整张图成为一个强连通分量。
  sol:一个著名的定理是“一个无向图点双总存在一种定向方法使得其成为一个有向图强连通分量”。事实上,我们可以将其推广至边双。之所以称之为“推广”,是因为一个无向图如果不存在割顶,那么一定不存在桥(由那两个不等式显然)。所以,点双比边双更难达到(换句话说,如果一个图是点双,则它一定是边双)。至此,问题就和POJ 3352一样了。
  (其实有一点不一样… 这个题的原图不一定连通,所以可能出现缩点后度数为0的点。设度数为0的点的数目为\(y\),则答案是\((x+2y+1)/2\)。但这样又带来一个特殊情况:原图就是边双,它的度数也是0。此时特判答案为0即可。)

#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

 #define rep(i,a,b) for (int i = a; i <= b; i++) 
 #define dep(i,a,b) for (int i = a; i >= b; i--)
 #define read(x) scanf("%d", &x)
 #define fill(a,x) memset(a, x, sizeof(a))

 const int N = 1000 + 5, M = 1000*1000 + 5;

 int n, m, u, v, es, DFS_clock, cnt, ans;
 int pre[N], last[N], low[N], bel[N], deg[N];
 bool is_bridge[N][N], vis[N];

 struct Edge { int from, to, pre; } e[M*2];
 void ine(int a, int b) {
    es++;
    e[es].from = a; e[es].to = b; e[es].pre = last[a];
    last[a] = es;
 }
 void ine2(int a, int b) {
    ine(a, b);
    ine(b, a);
 }
 #define reg(i,x) for (int i = last[x]; i; i = e[i].pre)

 void init() {
    fill(last, 0); fill(pre, 0); fill(bel, 0); fill(deg, 0);
    fill(vis, false); fill(is_bridge, false); 
    fill(low, 0x3f);
    DFS_clock = es = cnt = ans = 0;
 }

 void Tarjan_eBCC(int x, int dad) {
    pre[x] = low[x] = ++DFS_clock;
    reg(i, x) {
        int y = e[i].to;
        if (y == dad) continue;
        if (pre[y]) 
            low[x] = min(low[x], pre[y]);
        else {
            Tarjan_eBCC(y, x);
            low[x] = min(low[x], low[y]);
            if (low[y] > pre[x]) is_bridge[x][y] = is_bridge[y][x] = true;
        }

    }
 }

 void DFS(int x) {
    bel[x] = cnt;
    reg(i, x) {
        int y = e[i].to;
        if (bel[y] || is_bridge[x][y]) continue;
        DFS(y);
    }
 }

 void count_eBCC() {
    rep(i,1,n) if (!bel[i]) ++cnt, DFS(i);
    for (int i = 1; i < es; i += 2) {
        int x = bel[e[i].from], y = bel[e[i].to];
        if (x != y) deg[x]++, deg[y]++;
    }
    rep(i,1,cnt) if (deg[i] == 1) ans++; else if (deg[i] == 0) ans += 2;
    ans = (ans + 1)/2;
 }

int main()
{
    while (scanf("%d%d", &n, &m) == 2) {
        init();
        rep(i,1,m) read(u), read(v), ine2(u, v);

        rep(i,1,n) if (!pre[i]) Tarjan_eBCC(i, 0);
        count_eBCC();

        printf("%d\n", cnt == 1 ? 0 : ans);
    }

    return 0;
}

codeforces 732F:定义一个节点\(x\)的C(\(x\))值为节点\(x\)能到达的的节点的数目。给出一张无向连通图,要求将所有的边定向,使得C(\(x\))最小的节点\(x\)的C(\(x\))最大。
  sol:有向图强连通分量中的节点的C值都相同 ==> 原图中每个边双中节点的C值都相同。这样故伎重演,边双缩点后得到一棵树。然后我们考虑答案是什么。一棵无向树定向后一定存在入度为0的点(因为它一定是一个DAG)。这个(些)点是得不到任何支援的。所以容易想到,这个(些)点应该是权值最大的点(这里的权值就是指这个树点代表的原边双的节点数)。用反证法即可立刻得到这一结论。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;

 #define rep(i,a,b) for (int i = a; i <= b; i++) 
 #define dep(i,a,b) for (int i = a; i >= b; i--)
 #define vep(i,v) for (int i = 0; i < (int)v.size(); i++)
 #define read(x) scanf("%d", &x)
 #define fill(a,x) memset(a, x, sizeof(a))
 #define mp make_pair
 #define pb push_back

 typedef pair<int, int> P;

 const int N = 400000 + 5, M = 400000 + 5;

 int n, m, u, v, es, ret, DFS_clock, cnt;
 int pre[N], last[N], low[N], bel[N];
 bool is_bridge[M*2], vis[N];
 vector<P> G[N];
 P ans;

 struct Edge { 
    int from, to, pre, rev; 
    bool exist;
 } e[M*2];
 void ine(int a, int b) {
    es++;
    e[es].exist = false;
    e[es].rev = es % 2 == 0 ? es - 1 : es + 1;
    e[es].from = a; e[es].to = b; e[es].pre = last[a];
    last[a] = es;
 }
 void ine2(int a, int b) {
    ine(a, b);
    ine(b, a);
 }
 #define reg(i,x) for (int i = last[x]; i; i = e[i].pre)

 void init() {
    fill(last, 0); fill(pre, 0);
    fill(vis, false); fill(is_bridge, false);
    fill(low, 0x3f);
    DFS_clock = es = cnt = 0;
 }

 void Tarjan_eBCC(int x, int dad) {
    pre[x] = low[x] = ++DFS_clock;
    reg(i,x) {
        int y = e[i].to;
        if (y == dad) continue;
        if (pre[y]) 
            low[x] = min(low[x], pre[y]);
        else {
            Tarjan_eBCC(y, x);
            low[x] = min(low[x], low[y]);
            if (low[y] > pre[x]) is_bridge[i] = is_bridge[e[i].rev] = true;
        }
    }
 }

 void DFS1(int x, int dad) {
    vis[x] = true;
    bel[x] = cnt;
    ret++;
    reg(i,x) {
        int y = e[i].to;
        if (y == dad || is_bridge[i]) continue;
        if (!e[e[i].rev].exist) e[i].exist = true;
        if (vis[y]) continue;
        DFS1(y, x);
    }
 }

 void DFS2(int x) {
    vis[x] = true;
    vep(i,G[x]) {
        int y = G[x][i].first, cur = G[x][i].second;
        if (!vis[y]) {
            e[e[cur].rev].exist = true;
            DFS2(y);
        }
    }
 }

 void count_eBCC() {
    rep(i,1,n) if (!vis[i]) {
        cnt++;
        ret = 0;
        DFS1(i, 0);
        ans = max(ans, mp(ret, cnt));
    }
    rep(i,1,es) G[bel[e[i].from]].pb(mp(bel[e[i].to], i));
    fill(vis, false);
    DFS2(ans.second);
 }

int main()
{
    init();

    read(n); read(m);
    rep(i,1,m) read(u), read(v), ine2(u, v);

    Tarjan_eBCC(1, 0);
    count_eBCC();

    printf("%d\n", ans.first);
    rep(i,1,es) if (e[i].exist) printf("%d %d\n", e[i].from, e[i].to);

    return 0;
}

UVa 10765:定义一个节点的“鸽子值”为去掉这个点之后图中连通分量的个数。给出一张\(n\)个点的无向连通图,输出鸽子值前\(m\)大的节点。
  sol:如果一个点不是割顶,那么它的鸽子值为1;否则是直接与之相连的点所属的不同的点双的数目。

UVa 1364:有\(n\)个骑士召开圆桌会议,\(m\)对骑士不能坐在相邻座位。一场会议必须有不少于3人的奇数人参加。问有哪些骑士不能参加任何一个会议。
  sol:如果我们把能参加会议的骑士对之间连无向边,那么图中的一个奇圈就相当于一个会议圆桌。反过来,一个合法的会议圆桌一定存在一个简单奇圈与之对应。这样,问题转化为不再任何一个简单奇圈中的点的数目。
  如何判断一个点是否在简单奇圈中呢?我们把“奇”和“圈”分开考虑。一个点在一个环上等价于它在一个点双中。点双上的任意一个点都至少在一个简单圈上。关于“奇”,我们不难想到一个必要条件,即这个BCC不是二分图——如果是二分图,一定不含奇圈;如果不是二分图,还需要进一步判断这个条件是否充分。即,一个“非二分图点双”至少含一个奇圈,但是不是所有的点都在奇圈上呢?我们弄一个房子型的图,上面是三角形,下面是正方形。容易发现,在构造出的偶圈(即正方形)上的点(即正方形顶点)也在一个奇圈(房子的轮廓,7个点)上。这个证明过程不难推广:把三角形改成奇圈,正方形改成任意圈即可。
  这样,问题得到解决:统计所有不在任何一个“非二分图点双”上的点的数目即可。

UVa 1108:给出一张无向图,要求染黑尽量少的点(初始时均为白点),使得删除任意一个点后,每个连通分量至少有一个黑点。输出这个数量以及最优方案数。
  sol:看着题目,不难想到与割顶有关。容易发现,把割顶染黑是不划算的。深入一些,不难得出一个点双中至多染一个点。但如果我们在每个点双中都选点染黑,还不够优——两个点双之间可能有联系。如果一个点双的度数超过1,那么即使删除任意一个与之相关的割顶,也仍剩余至少一个点双与之相连。在这样的点双中选点染黑就不划算了。所以,我们只在度数为1的点双中选点染黑。注意不能选割顶。注意特殊情况:整张图不存在割顶,染黑任何一个点都可以。
  事实上,不求点双也行,因为这道题不带割顶玩,我们只需事先求出割顶,然后持续DFS,每次DFS不经过割顶就可以统计了。代码量可能稍小一点。

http://nanoape.is-programmer.com/posts/184805.html

转载于:https://www.cnblogs.com/yearwhk/p/6013547.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值