C++Tarjan算法——图论求割点(附例题)

文章介绍了如何使用Tarjan算法判断无向图中的割点,重点讲解了割点的定义以及两种判定条件,并通过深度优先搜索(DFS)配合dfn和low数组来实现。算法中,dfn表示节点的发现时间,low表示能到达的祖先的最低dfn值,通过这两个值判断节点是否为割点。最后,给出了洛谷模板题的示例和AC代码。
摘要由CSDN通过智能技术生成

 今天早上算是勉强学了Tarjan算法求无向图中的割点,虽然还不是特别透彻,但结合一点理解再背板子会比较容易一些,下面我简单讲讲我的理解。

首先是割点的定义,就是在无向图中去掉后,使得图会变成两个部分的点,可以理解为把图连接在一起的关键的点。那么对于割点的判定,就有以下两种情况:

一,一个点是根的时候。因为Tarjan算法需要对图进行dfs深度搜索,所以对于第一次dfs的点,就把它视作是根。那么这样的根是割点,需要它至少有2个子树,也就是至少有两个顶点与它相连,那么如果这个点去掉,它的两个子树就分开了,所以是割点。

二,一个点不是根的时候,如果这个点是割点,那么会有这样的性质:它的子树只能通过这个点这一条路径来访问到图的其他部分。

至于证明的话我也不是很清楚,我是根据Tarjan算法的代码得出上面的两个判定,就稍微背一下。

下面来讲一下Tarjan算法是如何利用这两个判定条件来找到割点的。

Tarjan算法利用了两个数组,dfn[i]和low[i],分别表示第i个点的dfn遍历的顺序和第i个 点能够访问到的最老的祖先。因为第二个判定条件需要用到子树访问到的祖先,如果一个点的子树能够访问到的最上面的点就是这个点,就说明这个点是割点。每次dfs时,传递的参数是两个变量,u和anc,u表示当前dfs访问到的点,anc代表这一次dfs的根。对u相邻的结点进行遍历,设这个点为v,如果没有访问过v,就对它dfs,dfs(v,anc),因为v是u出发到达的点,所以它们的根相同,都是anc。算了还是直接看代码吧,具体的解释在代码注释里,结合代码看得更容易一些。(代码我是背的洛谷用户wind_seeker的板子)

void tarjan(int u, int anc) {
	dfn[u] = low[u] = ++t;//更新时间戳
	int child = 0;//记录u结点的孩子个数
	for (auto v : G[u]) {//访问顶点u相邻的顶点v
		if (!dfn[v]) {//如果该顶点未访问过
			tarjan(v, anc);//因为是从u继续访问的,所以追溯到的祖先相同
			low[u] = min(low[u], low[v]);//u能访问到的最上面的是u和v能访问到的中偏上的那个
			if (low[v] >= dfn[u] && u != anc) cut[u] = 1;//u的孩子最多访问到u且u不是根结点
			if (u == anc) child++;//如果u是根结点,且有孩子v,则u的孩子数量增加
		}
		else
			low[u] = min(low[u], dfn[v]);//已访问过的顶点v,有可能dfs序更小
	}
	if (child >= 2 && u == anc) cut[u] = 1;//有2个孩子的根是割点
}

代码里的t代表时间戳,从0开始,用来记录dfs遍历的顺序,cut数组记录点是否为割点,整个算法总体还是比较短的,直接背就完了,下面放一道洛谷P3388的求割点模板题。

题目描述

给出一个 n 个点,m 条边的无向图,求图的割点。

输入格式:

第一行输入两个正整数 n,m。

下面 m 行每行输入两个正整数 ,x,y 表示 x 到 y 有一条边。

对于全部数据,1<=n<=2e4,1<=m<=1e5

输出格式:

第一行输出割点个数。

第二行按照节点编号从小到大输出节点,用空格隔开。

 

输入样例:

6 7
1 2
1 3
1 4
2 5
3 5
4 5
5 6

 输出样例:

1 
5

AC代码:

#include<bits/stdc++.h>
#define ll long long
#define pii pair<int,int>
using namespace std;
const int maxn = 2e5 + 10;
vector<int>G[maxn];
int dfn[maxn], low[maxn],ge[maxn],t=0,cut[maxn];
int read() {
	int x = 0, f = 0, ch = getchar();
	while (!isdigit(ch)) { if (ch == '-')f = 1;ch = getchar(); }
	while (isdigit(ch)) { x = x * 10 + ch - '0';ch = getchar(); }
	return f ? -x : x;
}
void tarjan(int u, int anc) {
	dfn[u] = low[u] = ++t;//更新时间戳
	int child = 0;//记录u结点的孩子个数
	for (auto v : G[u]) {//访问顶点u相邻的顶点v
		if (!dfn[v]) {//如果该顶点未访问过
			tarjan(v, anc);//因为是从u继续访问的,所以追溯到的祖先相同
			low[u] = min(low[u], low[v]);//u能访问到的最上面的是u和v能访问到的中偏上的那个
			if (low[v] >= dfn[u] && u != anc) cut[u] = 1;//u的孩子最多访问到u且u不是根结点
			if (u == anc) child++;//如果u是根结点,且有孩子v,则u的孩子数量增加
		}
		else
			low[u] = min(low[u], dfn[v]);//已访问过的顶点v,有可能dfs序更小
	}
	if (child >= 2 && u == anc) cut[u] = 1;//有2个孩子的根是割点
}
int main() {
	int n = read(), m = read();
	while (m--) {
		int u = read(), v = read();
		G[u].push_back(v);G[v].push_back(u);
	}
	for (int i = 1;i <= n;i++)
		if (!dfn[i]) tarjan(i, i);
	int ans = 0;
	for (int i = 1;i <= n;i++)
		if (cut[i]) ans++;
	cout << ans << endl;
	for (int i = 1;i <= n;i++)
		if (cut[i])
			cout << i << " ";
	return 0;
}

 以上。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值