[双连通分量] tarjan算法

在前面说的话

其实这个借鉴了网上的一些教程/总结,但是主要还是看lrj的蓝书并有部分引用以及自己的一些理解而成的,仅仅是为了给自己或他人总结用的,而不希望用于任何其他的用途亦或是被说抄袭。

引入

首先先来看几个概念
割点(割顶、关节点),在一个无向图中,如果删除了某一个点,能使连通分量的个数增加, 那么称这个点为割点
特殊的情况:对于一个连通图,割点就是在删除以后能让原图不再连通的点
桥,在一个无向图中,删除了一条边,能使连通分量个数增加,则这条边为桥
那么首先先来看看如何在一个无向图中求出割点
暴力
暴力显然是首先想到的,在一个无向图中,尝试删除每一个点(边),然后做一次dfs检查连通分量的个数是否增加。
但很显然这种办法很慢,预计的时间复杂度应为 O(n(n+m))
lrj蓝书上的基于DFS的做法
其实这种做法并不一定是lrj首创,但在这里也没有去找原作者,而lrj也没有提,或许是人人皆知的做法吧(其实也可能是tarjan算法的基础)
首先这个算法需要时间戳,于是这里用 pre 记录第一次访问的时间戳
那么DFS会产生一个DFS树,在这棵树中,又有几个概念要认识
首先是树边,就是DFS树上的边,但是除了树边还会有反向边,即由后代连回祖先的边。
那么考虑出现什么情况才能证明是割点呢。先考虑一棵树的树根,显然,当树根下有多个子树时,它会是割点,但当只有一棵子树时,它不是割点。
然后考虑下面的点,这里有定理
非根节点 u 是图G的割点时,当且仅当u存在一个子节点及该子节点的后代不存在连向 u 的祖先的反向边。证明
很显然,如果u的任何一个子节点都能够连接到 u 的祖先,那么砍掉u,它依然连通,如果有一个或是更多子节点不能连回 u 的祖先而只是连回u,那么这一棵子树在 u 被去掉后会成为一个单独的连通块,因此u是一个割点。
干说不好懂,那么上图。
这里写图片描述
那么显然地,如果在v以及其子树中,最多连回 u ,那么在u被砍掉之后,这块就独立了2333
试想,如果连回了 u 的祖先,那么这棵dfs树,可以在u被删除后,沿着这条反向边将这一块变成另外一棵子树,所以原图并没有增加连通分量,即 u 不是割点。
那么如果用low(u)来表示从 u 及其后代中能够通过反向边连回的最前的祖先,也就是pre值最小的那个
那么这个定理可以简写为当 u 存在一个子节点v,使得 low(v)pre(u) 时, u 为割点
那么在这里其实还有一种特殊情况,当low(v)>pre(u), 也就是连u都没连回,可以脑补一下上面的图,把那条反向边连到 v 上,我们可以发现这是u v <script type="math/tex" id="MathJax-Element-28">v</script>之间的边其实就是原图的一个桥
代码还是比较容易写的,而且在后面求双连通分量的tarjan算法的思路基本是一致的。

双连通分量

那么依旧,先来看看定义
什么是双连通分量呢?
就是说,在一个连通图中,任意两个点都能通过至少两条不经过同一个点的路径而互相到达,就说明它是点双连通的。
同样举几个栗子例子
这里写图片描述
我们暂且称这样的图叫铁三角好了23333
在这个图中1和2可以通过直接到达和走经过3的弯路而到达,1和3,2和3同理,所以这是一个点双连通的图,点双连通的判定条件比较复杂,这就涉及到我们的tarjan算法了,但是我们先放一放,看看边双连通。
如果是边双连通呢,那要求就更加简单,只要两条路径之间没有公共的边即可,就算经过同一个点也没关系。因此我们发现,只要一个图是点双连通的,就一定是边双连通的,那么判定就更加简单,只要是没有桥的一个图就好了。
那么双连通分量就是一个无向图中的双连通的一个极大子图,只要多画了几个图就会发现,割点总是多个点双连通分量的公共点,而边双连通分量则不会有公共点。
所以tarjan算法的基本思路就是,用找割点的办法, 凡是找到了一个割点,并且在这一棵子树会在这个点被去掉之后单独独立出来的,就是一个点双连通分量。因此这里用一个栈来保存访问过的边,直到发现了就不停地出栈,直到访问回这条边的时候就停止出栈,因为前面的边就是和祖先是一个双连通分量或是独立的了。那么代码和前面求割点的基本一致,只是在dfs处稍微多加了几句,还是比较好写的。

参考代码
#include <stack>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAXN = 1010,
          MAXM = 100010;

struct Edge {
    int u, v;
    int ne;
} e[MAXM * 2];

int pre[MAXN], bcc_cnt, dfs_clock, bccno[MAXN];
// 用bcc_cnt来记录有多少个双连通分量 bccno则是每一个点处于哪一个双连通分量中,对于割点bccno无意义,储存的是最后一个双连通分量的编号
bool iscut[MAXN];

int head[MAXN];

vector  bcc[MAXN];
stack  S;

int dfs(int u, int fa) {
    int lowu = pre[u] = ++dfs_clock;
    int child = 0;

    for(int i = head[u]; ~i; i = e[i].ne) {
        int v = e[i].v;
        if(!pre[v]) { // 未访问 
            S.push(e[i]);
            child++;
            int lowv = dfs(v, u);
            lowu = min(lowu, lowv);
            if(lowv >= pre[u]) { // 如果找到了双连通分量 
                iscut[u] = 1;
                ++bcc_cnt;
                bcc[bcc_cnt].clear();
                for(;;) { // 出栈
                    Edge x = S.top();
                    S.pop();
                    if(bccno[x.u] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.u);
                        bccno[x.u] = bcc_cnt;
                    }
                    if(bccno[x.v] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.v);
                        bccno[x.v] = bcc_cnt;
                    }
                    if(x.u == u && x.v == v) {
                        break;
                    }
                }
            }
        } else {
            if(pre[v] < pre[u] && v != fa) {
                S.push(e[i]);
                lowu = min(lowu, pre[v]); // 用反向边更新自己
            }
        }
    }

    if(fa < 0 && child == 1) {
        iscut[u] = 0;
    }
    return lowu;
}

void find_bcc(int n) {
    memset(pre, 0, sizeof pre);
    memset(iscut, 0, sizeof iscut);
    memset(bccno, 0, sizeof bccno);
    dfs_clock = bcc_cnt = 0;
    for(int i = 0; i < n; ++i) {
        if(!pre[i]) {
            dfs(i, -1);
        }
    }
}

int main(void) {
    int n, m;
    int cnt = 0;
    memset(head, -1, sizeof head);
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        e[cnt].u = u;
        e[cnt].v = v;
        e[cnt].ne = head[u];
        head[u] = cnt++;
        e[cnt].u = v;
        e[cnt].v = u;
        e[cnt].ne = head[v];
        head[v] = cnt++;
    }
    find_bcc(n);
    printf("%d\n", bcc_cnt);
    for(int i = 1; i <= bcc_cnt; ++i) {
        printf("%d : ", i);
        for(int j = 0; j < bcc[i].size(); j++) {
            printf("%d ", bcc[i][j]);
        }
        puts("");
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值