Tarjan算法——强连通、割点、桥

Tarjan算法

概念区分

  • 有向图
    • 强连通:在有向图 G G G中,如果两个顶点 u , v   ( u ≠ v ) u, v\ (u \neq v) u,v (u=v)间有一条从 u u u v v v的有向路径,同时还有一条从 v v v u u u的有向路径,则称 u , v u, v u,v强连通
    • 强连通图:如果有向图 G G G的任意两个不同的顶点都强连通,则称 G G G是一个强连通图
    • 强连通分量:有向图 G G G的极大强连通子图称为图 G G G的强连通分量
  • 无向图
    • 连通:和强连通类似(只是无向图的任意边都是双向的,如果存在 u → v u\rightarrow v uv的路径,必然存在 v → u v\rightarrow u vu的路径)
    • 连通图:如果无向图 G G G的任意两不同的顶点都连通,则称 G G G是一个连通图
    • 连通分量:无向图 G G G的极大连通子图称为图 G G G的连通分量
    • 割点(割顶):在无向图 G G G中,如果删除一个点 u u u及以 u u u为端点的所有边后,图的连通分量个数增多,则称点 u u u割点(割顶)
    • 桥:在无向图 G G G中,如果删除一条边 e = ( u , v ) e=(u,v) e=(u,v)(连接 u , v u,v u,v两点的边)后,图的连通分量个数增多,则称边 e e e,也称割边
    • 双连通:
      • 双连通图:分为点双连通图边双连通图,若一个无向图 G G G中的去掉任意一个节点(一条边)都不会改变图 G G G的连通性,即不存在割点(桥),则称图 G G G点(边)双连通图
      • 边双连通图:一个无向图 G G G中的每一个极大点双连通子图边双连通子图分别称为无向图 G G G点双连通分量(BCC)边双连通分量(e-BCC)

Tarjan

DFS​树

t a r j a n tarjan tarjan的过程实际上就是一个 d f s dfs dfs的过程,对图 G G G进行 d f s dfs dfs会得到一棵 d f s dfs dfs树,在有向图的 d f s dfs dfs树中有四种边,树边、回边(返祖边、后向边)、横叉边和前向边。树边顾名思义,回边是指在 d f s dfs dfs过程中搜索到已经访问过的点,但它的子树未访问完成;横叉边是指搜索到已经访问过的点且它的子树也已访问完成,而且横叉边连接的两个点不是祖先和后代的关系;前向边与横叉边唯一的不同在于前向边连接的两个点祖先和后代的关系,读者可以结合下图理解这四种边
在这里插入图片描述
⏫图片来源:https://www.cnblogs.com/gongpixin/p/5003049.html

强连通分量
思想
  • d f n [ u ] dfn[u] dfn[u]:顶点 u u u被访问到的时间戳,每个点的 d f n dfn dfn被赋值后就不会改变

  • l o w [ u ] low[u] low[u]:顶点 u u u能够到达的点中的 d f n dfn dfn最小值

  • s t a c k [ t o p ] stack[top] stack[top]:可能构成强连通分量的点的集合

  • v i s [ u ] : vis[u]: vis[u]顶点 u u u是否在栈中

  • c o l o r [ u ] color[u] color[u]:顶点 u u u的颜色,用于区分不同的强连通分量

    有些题目添加 c o l o r color color数组比较方便,不是必要的

采用深度优先搜索的思想,对每一个可能的强连通分量进行 d f s dfs dfs:维护一个可能构成强连通分量的集合 s t a c k stack stack,将搜索到的点加入 s t a c k stack stack,并维护其 d f n ,   v i s dfn,\ vis dfn, vis值,往下搜它的边连接的顶点,在回溯的时候维护 l o w low low的值,如果点 u u u d f n = = l o w dfn==low dfn==low说明它是某个强连通分量子树的根,此时我们找到了一个强连通分量,只要将栈 s t a c k stack stack中的点弹出,直到弹出 u u u

  • 同一个强连通分量中的点的 l o w low low值是相同的

看到这儿,有些读者可能会问,那无向图的连通分量咋求?也是直接套 t a r j a n tarjan tarjan的板子吗?如果您有这样困惑反正我是有过,那是学算法学傻了不是,无向图还 t a r j a n tarjan tarjan啥,直接 d f s dfs dfs b f s bfs bfs一遍就完了,那善于思考的读者们又会问了,为啥无向图这么方便?因为对于无向图,如果存在路径 u → v u\rightarrow v uv,那路径 v → u v\rightarrow u vu必然存在, u u u能跑到的点(包括 u u u自己)都是一个连通分量里的

板子
void tarjan(int u) {
	dfn[u] = low[u] = ++tim;
	vis[u] = true; // 入栈
	stack[++top] = u;
	int size = g[u].size();
	for(int i = 0; i < size; ++i) {
		int v = g[u][i];
		if(!dfn[v]) { // 树边,继续下搜
			tarjan(v);
			low[u] = min(low[u], low[v]);
		} else if(vis[v]) // 回边,更新low[u]
            low[u] = min(low[u], dfn[v]);
	}
	if(dfn[u] == low[u]) {
		color[u] = ++sum;
		vis[u] = false;
		while(stack[top] != u) {
			color[stack[top]] = sum;
			vis[stack[top--]] = false;
		}
		top--;
	}
}

//for(int i = 1; i <= n; ++i)
//    if(!dfn[i])
//        tarjan(i);

割点(顶) / 桥

原理

以下所说的根节点,指的是在 D F S DFS DFS树里的根节点。在 t a r j a n tarjan tarjan算法求割点的过程中,主要涉及到两种边:树边和回边(返祖边),意义已在上文中提到⏫

  • 定理 1 1 1:在无向图 G G G中,点 u ( u u(u u(u不是是根节点)是割点    ⟺    \iff u u u的某个子树 T T T不含有返回 u u u的祖先不包括 u u u的边    ⟺    \iff l o w [ v ] > = d f n [ u ] low[v] >= dfn[u] low[v]>=dfn[u](注意是不含 不包括

    特别地,根节点是割顶    ⟺    \iff 它的子节点数目大于 1 1 1

    画图YY一下:
    在这里插入图片描述
    上图的 u u u的某个子树 T T T中虽然有返回 u u u的回边,但是没有返回 u u u的祖先(不包含 u u u)的回边,所以 u u u是割点,但是边 e e e并不是桥

  • 定理 2 2 2:在无向图 G G G中,边 ( u , v ) (u,v) (u,v)是桥    ⟺    \iff u u u的某个子树 T T T中不含有返回 u u u的祖先包括 u u u的边    ⟺    \iff l o w [ v ] > d f n [ u ] low[v] > dfn[u] low[v]>dfn[u]

    还是看之前的图, f a fa fa的某个子树中不含有返回 f a fa fa的祖先(包括 f a fa fa)的回边,所以边 e f ef ef是桥

  • 可以看出如果一个图 G G G有桥,它必有割点,因为桥连接的两端必然至少有一个是割点,但反过来不成立

板子

还有点细节:

c h i l d child child是点 f a fa fa的子节点个数,注意 f a fa fa的孙子不计算在内

特别地,像:
在这里插入图片描述
这个图里没有割点,因为去掉哪个点,这个图的连通分量的个数都不会增加(其实可以发现简单环都是这样

void Tarjan(int u, int fa) { // 割点
    low[u] = dfn[u] = ++tim;
    int sz = g[u].size(), child = 0;
    for(int i = 0; i < sz; i++) {
        int v = g[u][i];
        if(!dfn[v]) {
            Tarjan(v, fa);
            low[u] = min(low[v], low[u]);
            if(low[v] >= dfn[u] && u != fa) iscut[u] = true;
			// if(low[v] > dfn[u]) u - v 是桥
            if(u == fa) child++;	
        } else
            low[u] = min(low[u], dfn[v]);
    }
    if(fa == u && child >= 2) iscut[u] = true;
}

//for(int i = 1; i <= n; ++i)
//    if(!dfn[i])
//        tarjan(i, i);

缩点

啥是缩点

就把一个强连通分量(或边双连通分量等,视题目而定)缩成一个点

再看张图YY一下:
在这里插入图片描述
光这么讲,大家肯定还是云里雾里,这缩点到底能干啥?下面我们通过道例题来看看缩点到底能干啥

例题
  • 洛谷P3387 缩点

    思路:缩点,把一个强连通分量缩成一个点,新点的权值等于强连通分量里的所有点的点权之和,缩完点就能得到一个有向无环图 ( D A G ) (DAG) (DAG),然后就可以在这个 D A G DAG DAG上跑 d p dp dp了,然后就能得到答案

双连通分量

一点性质
  • 边双连通分量(e-BCC)满足任意两点间都有至少两条边不重复的路径
  • 点双连通分量(BCC)满足任意两点间都有至少两条点不重复路径
  • 有桥必有割点,一个点双连通分量必然是边双连通分量,反之,不一定成立
边双连通分量(e-BCC)

在这里插入图片描述

如上图,左右两个用大圈圈出来的分别是两个边双连通分量

求法:

把桥全部去掉剩下的独立的分量都是边双连通分量,所以只要一遍 t a r j a n tarjan tarjan找到桥,再一遍 d f s dfs dfs就能能求出边双连通分量

点双连通分量(BCC)

在这里插入图片描述
如上图,左边红圈圈出的和紫色圈圈出的分别是两个点双连通分量,红点是整张图的一个割点

两个点双连通分量间至多有一个公共点,且它一定是割点;不是割点的点,一定属于某个点双连通分量

求法:

只要分离了割点,就可以把点双连通分量分离出来,但是一个割点可能同属于两个点双连通分量,所以一般用分量中的边来输出,可以用栈保存遍历到的边,一旦发现割顶就弹出属于一个BCC的边

例题
  • POJ3352 Road Construction

    思路:题意是问最少加几条边可以使整个图变成边双连通的,即问最少加多少条边可以使整张图的任意两点间至少有两条边不重复路径,所以我们可以将已有的边双连通分量缩点,缩完点形成一棵树。现在,题目变为在缩完点后的树上最少添加几条边能使其变为边双连通图,有一个结论是对于一棵无向树,我们要使得其变成边双连通图,需要添加的边数= (树上度数为1的点的个数+1)/2

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值