Tarjan算法 推导应用 LCA 缩点 割边 割点 强联通分量 边双 点双 代码

仅供部落成员学习使用

Tarjan算法简介

Tarjan算法是基于对图深度优先搜索的算法,定义DFN(u)为节点的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的节点的次序号
C++代码,采用链式前向星存图

#include<bits/stdc++.h>
using namespace std;
struct node{
	int to, nxt;
}e[M];
int n, m, hd[N], dfn[N], low[N], cnt, tot;
void add(int u, int v)
{
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}     
void tarjan(int u)
{
	dfn[u] = low[u] = ++tot;
	for(int j = hd[u]; j; j = e[j].nxt)
	{
		int v = e[j].to; // v是u的子节点 
		if(!dfn[v])
		{
			tarjan(v);
			if(low[u] > low[v]) low[u] = low[v];
		}
		else
		{
			//这里要格外注意, 有的代码写成了下行注释写法
			//此写法在求割点的时候会出错,文章下面有分析 ???
			// if(low[u] > low[v]) low[u] = low[v];
			if(low[u] > dfn[v]) low[u] = dfn[v];
		}
	}
}
int main()
{
	cin >> n >> m;
	int u, v;
	for(int i = 1; i <= m; i++)
	{
		scanf("%d%d", &u, &v);
		add(u, v);
	}
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) tarjan(i);
	return 0;
}

Tarjan求割点

无向连通图中,如果将其中一个点以及所有连接该点的边去掉,图就不再连通,那么这个点就叫做割点。
如何利用Tarjan求割点
case1:非root节点且dfn[x] <= low[x的儿子]
对于边(u, v),如果dfn[u]<=low[v],即v即其子树能够回溯到的最早的点,最早也只能是u,要到u前面就需要u的回边或u的父子边。也就是说这时如果把u去掉,u的回边和父子边都会消失,那么v最早能够回溯到的最早的点,已经到了u后面,无法到达u前面的顶点了,此时u就是割点。在这里插入图片描述
case2:root节点 儿子个数>=2
在这里插入图片描述
这里要解释一下右边的图形,因为A,B之间有路径连接,所以dfs的顺序为root->A->B,所以root只有一个儿子,root非割点

为什么root要单独考虑呢,下图中Tarjan跑完后所有点的low都为1
low[root] == dfn[root],满足case1,但显然root非割点,所以需要单独考虑
在这里插入图片描述
详细代码

#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], tot, num;
bool iscut[N];
struct node{
	int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2 
void add(int u, int v){
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}
void tarjan(int root, int u){
	dfn[u] = low[u] = ++tot;
	int child = 0;
	for(int j = hd[u]; j; j = e[j].nxt){
		int v = e[j].to;
		if(!dfn[v]){
			tarjan(root, v);
			low[u] = min(low[u], low[v]);
			if(root == u) child++;
			//不能这里统计割点的数量,会重复 , dfn[u] <= low[v1], dfn[u] <= low[v2].....
			//u点会被重复计算多次 
			if((root == u && child > 1) || (root != u && dfn[u] <= low[v]))
				iscut[u] = true;
		}
		//这里必须是dfn[v], 不能是low[v]解释见下图 
		else low[u] = min(low[u], dfn[v]);
	}
}
int main()
{
	cin >> n >> m;
	int x, y;
	for(int i = 1; i <= m; i++)
		scanf("%d %d", &x, &y), add(x, y), add(y, x);
	for(int i = 1; i <= n; i++)
		if(!dfn[i])  tarjan(i, i);
	for(int i = 1; i <= n; i++)
		if(iscut[i]) num++; 
	cout << num << endl;
	for(int i = 1; i <= n; i++)
		if(iscut[i]) cout << i << " ";
	
	return 0;
}

注释解释,为什么不能是low[u] = min(low[u], low[v])呢
在这里插入图片描述
割点模板题
b站邋遢大哥233 视频详讲

Tarjan求割边(桥)

无向连通图中,如果将其中一条边删除,图就不再连通,那么这条边就叫做割边。
在这里插入图片描述
割边要注意无向图的边只能由父——>儿,不能儿——>父, 否则
在low[u] = min(low[u], dfn[v])的作用下,任何dfn[父] >= low[儿]
在这里插入图片描述
我们在用链式前向星存边的时候,边号从2开始计算,那么无向图的双向边的编号就是x和 x^1, ^1的作用是对偶数加1,奇数减1

#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], tot, num;
bool iscut[N];
struct node{
	int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2 
void add(int u, int v){
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}
void tarjan(int u, int eid){
	dfn[u] = low[u] = ++tot;
	for(int j = hd[u]; j; j = e[j].nxt){
		int v = e[j].to;
		if(!dfn[v]){
			tarjan(v, j);
			low[u] = min(low[u], low[v]);
			if (low[v] > dfn[u])
                bridge[i] = bridge[i ^ 1] = true; 
		}
		//保证不是指向父节点
		else if(i != (eid ^ 1))low[u] = min(low[u], dfn[v]);
	}
}
int main()
{
	cin >> n >> m;
	int x, y;
	cnt = 1;
	for(int i = 1; i <= m; i++)
		scanf("%d %d", &x, &y), add(x, y), add(y, x);
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) tarjan(i, 0);
	for (int i = 2; i < cnt; i += 2)
        if (bridge[i])
            printf("%d %d\n", e[i ^ 1].to, e[i].to);
	
	return 0;
}

Tarjan求强连通分量

强连通:有向图,任意两点双向互通
强连通分量:有向图的极大强连通子图,极大的概念指即一个有向图,分成很多部分,每一部分内部都可以互相抵达,而不同部分之间不能互相到达或者只能单向到达.
具体代码


#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], S[N], beLong[N], top, tot, scNum;
bool inStack[N];
struct node{
	int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2 
void add(int u, int v){
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}
//新增一个栈,inStack[], 一个栈顶下标top
//新增一个标记数组beLong[],记录各个顶点属于哪一个强连通分量, scNum强连通分量的标号
void tarjan(int u){
	dfn[u] = low[u] = ++tot;
	inStack[u] = true; //标记u点已经在栈里了
	S[++top] = u; 
	for(int j = hd[u]; j; j = e[j].nxt){
		int v = e[j].to;
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		//已经确定的强连通分量里的点不能更新当前点
		//避免出现两个强连通分量之间有单向边的情况,见下图 
		else if(inStack[v])low[u] = min(low[u], dfn[v]);
	}
	//当u的所有子孙节点都搜索完成后
	if(dfn[u] == low[u]){
		//strong connectivity 强连通分量的标号 
		scNum++;
		int v;
		do{
			v = S[top--];
			inStack[v] = false;
			beLong[v] = scNum;
		}while(v != u);
	} 
}
int main()
{
	cin >> n >> m;
	int x, y;
	cnt = 1;
	for(int i = 1; i <= m; i++)
	//	单向边 
		scanf("%d %d", &x, &y), add(x, y);
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= n; i++) 
		cout << beLong[i] << " "; 
	
	return 0;
}

else if(inStack[v])low[u] = min(low[u], dfn[v]),如果不加inStack[v]的限制,会出现下图情况,
dfs后所有点的dfn和low都是1,导致3,4点组成的强连通分量未被找出
在这里插入图片描述

参考洛谷资料

在这里插入图片描述在这里插入图片描述在这里插入图片描述
双连通分量(biconnected component)

Tarjan求边双(E-BCC)

不存在割边(桥)的无向连通图称为边双连通图
无向图中,如果无论删去那条边都不能使得u和v不连通,则称u和v边双连通
即u到v的路径上无必经边

求解:Tarjan求出割边,标记好割边,再dfs染色一遍(不使用割边)

Trajan求割边 
void dfs(int u)
{
    beLong[u] = eNum;//染色
    for(int j = hd[u]; j; j = e[j].nxt){
    {
        int v = e[j].to;
        if (bridge[j]) // 跳过割边 
            continue;
        if(!beLong[v])  dfs(v);
    }
}
for(int i = 1; i <= n; i++){
	if(!beLong[i]) dfs(i); 
} 

P4214 [CERC2015]Juice Junctions
P2860 [USACO06JAN]Redundant Paths G

Tarjan求点双(V-BCC)

无向图中,如果无论删去那个点(非u点和v点)都不能使得u和v不连通,则称u和v点双连通
即u到v的路径上无必经点(u,v除外)
注意:两个点和一条边构成的图也是BCC
在这里插入图片描述
无向连通图中割点一定属于至少两个BCC,非割点只属于一个BCC
割点就算相邻也会属于至少两个BCC;BCC间的交点都是割点,所以非割点只属于一个BCC

解决方法:
到一个结点就将该结点入栈,回溯时若目标结点low值不小于当前结点dfn值就出栈直到目标结点(目标结点也出栈),将出栈结点和当前结点存入BCC,对于每个BCC,它在DFS树中最先被发现的点一定是割点或DFS树的树根

证明:割点是BCC间的交点,故割点在BCC的边缘,且BCC间通过割点连接,所以BCC在DFS树中最先被发现的点是割点;特殊情况是对于开始DFS的点属于的BCC,其最先被发现的点就是DFS树的树根

上面的结论等价于每个BCC都在其最先被发现的点(一个割点或树根)的子树中

这样每发现一个BCC(low[v]>=dfn[u]),就将该子树出栈,并将该子树和当前结点(割点或树根)加入BCC中。

#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int dfn[N], low[N], S[N], top, tot, bNum;
vector <int> bcc [N];
struct node{
	int to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2 
void add(int u, int v){
	e[++cnt].to = v;
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}
//新增bcc 可变数组 vector<int>bcc[N];
void tarjan(int u) {
	dfn[u] = low[u] = ++tot;
	S[++top] = u;
	for(int j = hd[u]; j; j = e[j].nxt){
		int v = e[j].to;
		if(!dfn[v]) {
			tarjan(v);
			low[u] = min(low[u], low[v]);
			if(low[v] >= dfn[u]){ //u是割点或者是根 
				bNum++;
				int v;
				do{ 
					v = S[top--];
					bcc[bNum].push_back(v); 
				}while(u != v);
				//割点属于多个点双联通分量,所以要把割点重新放回去 
				S[++top] = v; 
				//割点什么时候能出栈呢, 只有遍历到另一个割点的时候 
			}
		} else {
			low[u] = min(low[u], dfn[v]);
		}
	}
}
int main()
{
	cin >> n >> m;
	int x, y;
	cnt = 1;
	for(int i = 1; i <= m; i++)
	//	单向边 
		scanf("%d %d", &x, &y), add(x, y), add(y, x);
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) tarjan(i);
	for(int i = 1; i <= bNum; i++){
		for(int j = 0; j < bcc[i].size(); j++)
			cout << bcc[i][j] << " ";
		cout << endl;
	} 
	return 0;
}

dfs时不越过割点,即可求解点双连通图
SP2878 KNIGHTS - Knights of the Round Table
P3225 [HNOI2012]矿场搭建
P5058 [ZJOI2004]嗅探器

Tarjan缩点

就是 tarjan求出的所有强连通分量都变成点(集合或共享某些信息),这样有向有环图就变成有向无环图(DAG)

#include<bits/stdc++.h>
using namespace std;
const int N = 20001, M = 100001;
int n, m, hd[N], cnt;
int a[N], dfn[N], low[N], S[N], beLong[N], top, tot;
bool inStack[N], exist[N];
struct node{
	int u, to, nxt;
}e[M * 2]; // 注意,因为是无向图,所以边的数量是M*2 
void add(int u, int v){
	e[++cnt].to = v;
	e[cnt].u = u; 
	e[cnt].nxt = hd[u];
	hd[u] = cnt;
}
//新增一个栈,inStack[], 一个栈顶下标top
//新增一个标记数组beLong[],记录各个顶点属于哪一个强连通分量, scNum强连通分量的标号
void tarjan(int u){
	dfn[u] = low[u] = ++tot;
	inStack[u] = true; //标记u点已经在栈里了
	S[++top] = u; 
	for(int j = hd[u]; j; j = e[j].nxt){
		int v = e[j].to;
		if(!dfn[v]){
			tarjan(v);
			low[u] = min(low[u], low[v]);
		}
		//已经确定的强连通分量里的点不能更新当前点
		//避免出现两个强连通分量之间有单向边的情况,见下图 
		else if(inStack[v])low[u] = min(low[u], dfn[v]);
	}
	//当u的所有子孙节点都搜索完成后
	if(dfn[u] == low[u]){
		//strong connectivity 强连通分量的标号 
		int v;
		exist[u] = true;
		do{
			v = S[top--];
			inStack[v] = false;
			beLong[v] = u;
			//合并强连通分量的权值 
			a[u] += a[v]; 
		}while(v != u);
		//最后多算了一次a[u] + a[u] 
		a[u] /= 2;
	} 
}
int main()
{
	cin >> n >> m;
	int x, y;
	cnt = 1;
	for(int i = 1; i <= m; i++)
	//	单向边 
		scanf("%d %d", &x, &y), add(x, y);
	// 输入每个点的权值 
	for(int i = 1; i <= n; i++) 
		scanf("%d", &a[i]);
	for(int i = 1; i <= n; i++)
		if(!dfn[i]) tarjan(i);
	//重新建图 
	cnt = 0;
	for(int j = 1; j <= m; j++){
	//缩点后会存在重边,一般来说没有影响
	//如果属于两个不同的缩点,则创建新边 
		if(beLong[e[j].u] != beLong[e[j].to]){
			add(beLong[e[j].u], beLong[e[j].to]);
		} 
	} 		
	return 0;
}

缩点
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值