Tarjan 算法学习笔记(超详细)

一、问题引入

当你要求割点/割边/点双连通分量/边双连通分量/强连通分量的时候,它们都可以用一个算法来解决,而且代码差别不大,这个算法就是 Tarjan。

二、什么是强连通分量

对于一张有向图 S S S 的子图 V V V,满足 V V V 内的点两两之间可达,并且不存在一个子图 G G G 使得 G ⊇ V , G ≠ V G \supseteq V,G \not= V GV,G=V,通俗的讲就是无法继续扩张的两两可达的子图。

三、Tarjan 求强连通分量算法推导

首先对于一个有向图的搜索树中,它分为 4 4 4 种边:树边、返祖边、横叉边、前向边。树边就是搜索树中正常的边,返祖边就是一个点返回它的祖先的一条边,横叉边就是一个点连向了一个不是它的祖先也不是它的后代的点的这样一条边,前向边就是一个点连向它的后代(并非儿子)的边。

然后就是算法的流程,这个算法有两个数组 d f n dfn dfn l o w low low,分别表示当前点的 dfs ⁡ \operatorname{dfs} dfs 序和当前点所能到达的最小的 d f n dfn dfn,就跟并查集有点像,Tarjan 里的 l o w low low 数组就类似并查集里的 f f f 数组,首先根据搜索树的特性,对于任何一个强连通分量它们的点集在搜索树上的 d f n dfn dfn 是连续的,而且你会发现,任何一个强连通分量都有一个制高点 x x x(类似并查集找图的连通块时的根),使得 l o w x = x low_x = x lowx=x,也就相当于找到了并查集中一个连通块的根,于是我们就有了一个大胆的猜想,先准备一个栈,存放着所有点,我们先在搜索时求出 l o w low low 数组和 d f n dfn dfn 数组,搜索时如果发现 l o w x = x low_x = x lowx=x,说明找到了一个强连通分量的制高点,将强连通分量的数量加 1 1 1,同时将这个强连通分量从栈里删掉。

但是,我们会面临一个问题:如何求 l o w low low d f n dfn dfn 是好求的)?

你可能会立马想到在搜索时,使用 l o w x = min ⁡ ( l o w x , l o w s o n ) low_x = \min(low_x,low_{son}) lowx=min(lowx,lowson) 来求出 l o w low low,但是你会发现,这样不一定能得到正确的 l o w low low,因为你可能连向了一个点,而且你就是靠那个点更新 l o w low low,但是那个点还没搜完,你却搜完了,然后如果后面那个点的 l o w low low 又更新小了,但你却已经搜完,无法更新,但是……

真的有必要求出正确的 l o w low low 吗?

其实没必要。

因为我们的 l o w low low 它的作用其实只是找出强连通分量的制高点,也就是判断 l o w x low_x lowx 是否等于 d f n x dfn_x dfnx,所以就算我们的 l o w low low 无法实时更新出正确的答案也不影响我们找到每个强连通分量的制高点。

证明?很简单!其实我们只需要证明两个东西就可以说明 l o w low low 值不准确也没关系,这两个东西分别是:如果正确的 l o w x low_x lowx 等于 d f n x dfn_x dfnx,那么不准确的 l o w x low_x lowx 也一定得等于 d f n x dfn_x dfnx 才行,你会发现这显然是对的,因为正确的 l o w x low_x lowx 都连不向其它更小的点,那不准确的 l o w x low_x lowx 更不可能连向其它更小的点,然后第二个要证明的事情就是如果正确的 l o w x low_x lowx 不等于 d f n x dfn_x dfnx,那么不准确的 l o w x low_x lowx 也一定得不等于 d f n x dfn_x dfnx,这个就有意思了,你想想不准确的 l o w x low_x lowx 为什么不准确?那不就是因为 x x x 连向了一个点,但是 x x x 比那个点先搜完,那么后面如果那个点更新了那 l o w x low_x lowx 就变得不准确了,但是这有关系吗?没有,因为我们已经连出去了才会出现 l o w x low_x lowx 变得不准确,那我们都已经连出去了那肯定不准确的 l o w x low_x lowx 就一定不等于 d f n x dfn_x dfnx 了。

然后再说一下,在搜索中遇到返祖边或目前没有被划入任何一个强连通分量的横叉边(前向边没用,因为指向的点已经被划分了)就不需要搜索,直接使用上述语句更新 l o w low low,如果是普通的树边就先搜索然后再更新 l o w low low,其它情况就什么都不用做。

四、Tarjan 求强连通分量板子

代码很简单,重要部分就几行。

#include<bits/stdc++.h>
using namespace std;
const int N = ;//这里是数组大小,根据题目数据范围定
vector<int>a[N];//边集
vector<int>g[N];//存每个强连通分量
int cnt,scc_cnt,top;//分别表示目前的dfs序、当前找到几个强连通分量,栈顶
int dfn[N],low[N];//跟上面说的dfn和low一样
int scc_color[N];//表示每个点属于第几个强连通分量
int sta[N];//栈
void dfs(int x)
{
	dfn[x] = low[x] = ++cnt;//先求出dfn,然后先给每个low赋一个初值
	sta[++top] = x;//放入栈
	for(int v:a[x])
	{
		//上面说的更新low的方法
		if(!dfn[v])
		{
			dfs(v);
			low[x] = min(low[x],low[v]);
		}
		else if(!scc_color[v])
		{
			low[x] = min(low[x],low[v]);
		}
	}
	if(dfn[x] == low[x])//找到制高点
	{
		scc_cnt++;//强连通分量数量+1
		int u;
		do
		{
			u = sta[top--];//取出栈顶
			scc_color[u] = scc_cnt;//标记所属强连通分量编号
			g[scc_cnt].push_back(u);//在这个点所属的强连通分量里放入这个点
		}
		while(u!=x);//因为对于任何一个强连通分量它们的点集在搜索树上的dfn是连续的,所以说从这个强连通分量的制低点(栈顶一定是这个强联通分量的制低点,这个原理很简单吧)删到制高点后就不能继续删了
	}
}
int vis[N];
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back(y);//连边
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])//因为图不一定连通,所以得将所有的连通块都搜一遍
		{
			dfs(i);
		}
	}
	//根据题目要求决定输出什么
	return 0;
}

注意:这只是板子,应用时请随机应变。

五、Tarjan 求强连通分量的缩点技巧

有一些题目需要将每个强连通分量缩成一个点,才能变得更好处理,码量也会更好写,同时会变得更加直观、好理解。

不过缩点的最重要的用处就是将有向有环图缩成有向无环图(反证法,如果缩完点后还有环,那么说明这环上所有强连通分量能合并成一个更大的强连通分量,这不就和强连通分量的定义相矛盾了吗),从而使不可解题目变成可解题目。

所以说我们先用 Tarjan 求出强连通分量,然后将所有边的两端不在同一个强连通分量的边保留即可。

代码:

vector<int>e[N];
int x[N],y[N];
for(int i = 1;i<=m;i++)
{
	if(scc_color[x[i]]!=scc_color[y[i]])
	{
		e[scc_color[x[i]]].push_back(scc_color[y[i]]);
	}
}

x , y x,y x,y 数组表示每条边的两端, e e e 数组表示缩点后的边集。

缩点后只有强连通分量的数量个点。

六、Tarjan 求强连通分量例题

B3609 [图论与代数结构 701] 强连通分量

此题就是求强连通分量的板子题,直接套板子即可。
由于题目要求很特殊,我们需要准备一个 v i s vis vis 数组,表示输出时当前点是否已经被输出过。
代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5;
vector<int>a[N];
vector<int>g[N];
int cnt,scc_cnt,top;
int dfn[N],low[N];
int scc_color[N];
int sta[N];
void dfs(int x)
{
	dfn[x] = low[x] = ++cnt;
	sta[++top] = x;
	for(int v:a[x])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[x] = min(low[x],low[v]);
		}
		else if(!scc_color[v])
		{
			low[x] = min(low[x],low[v]);
		}
	}
	if(dfn[x] == low[x])
	{
		scc_cnt++;
		int u;
		do
		{
			u = sta[top--];
			scc_color[u] = scc_cnt;
			g[scc_cnt].push_back(u);
		}
		while(u!=x);
		sort(g[scc_cnt].begin(),g[scc_cnt].end());//按题目要求点编号进行排序
	}
}
int vis[N];
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back(y);
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i);
		}
	}
	printf("%d\n",scc_cnt);
	for(int i = 1;i<=n;i++)
	{
		//按照题目要求输出
		if(vis[i])
		{
			continue;
		}
		for(int x:g[scc_color[i]])
		{
			vis[x] = 1;
			printf("%d ",x);
		}
		printf("\n");
	}
	return 0;
}
P3387 【模板】缩点

首先你会发现如果你能重复走到一个点的条件是这个点在一个环上,由于是有向有环图,所以我们可以先缩点,然后就变成了一个有向无环图,求简单路径边权和最大值(因为没有环了,就不可能重复走到一个点了),很容易发现可以用拓扑排序求出答案。
代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
vector<int>a[N];
int cnt,scc_cnt,top;
int dfn[N],low[N];
int scc_color[N];
int sta[N];
vector<int>e[N];
int x[N],y[N];
int dep[N];
int q[N];
int val[N];
int f[N];
int s[N];
void dfs(int x)
{
	dfn[x] = low[x] = ++cnt;
	sta[++top] = x;
	for(int v:a[x])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[x] = min(low[x],low[v]);
		}
		else if(!scc_color[v])
		{
			low[x] = min(low[x],low[v]);
		}
	}
	if(dfn[x] == low[x])
	{
		scc_cnt++;
		int u;
		do
		{
			u = sta[top--];
			scc_color[u] = scc_cnt;
		}
		while(u!=x);
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=n;i++)
	{
		scanf("%d",&val[i]);
	}
	for(int i = 1;i<=m;i++)
	{
		scanf("%d %d",&x[i],&y[i]);
		a[x[i]].push_back(y[i]);
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i);
		}
	}
	for(int i = 1;i<=n;i++)
	{
		s[scc_color[i]]+=val[i];
	}
	for(int i = 1;i<=scc_cnt;i++)
	{
		f[i] = s[i];
	}
	for(int i = 1;i<=m;i++)
	{
		if(scc_color[x[i]]!=scc_color[y[i]])
		{
			e[scc_color[x[i]]].push_back(scc_color[y[i]]);
			dep[scc_color[y[i]]]++;
		}
	}
	int h = 1,t = 0;
	for(int i = 1;i<=scc_cnt;i++)
	{
		if(!dep[i])
		{
			q[++t] = i;
		}
	}
	while(h<=t)
	{
		int x = q[h++];
		for(int v:e[x])
		{
			f[v] = max(f[v],f[x]+s[v]);
			dep[v]--;
			if(!dep[v])
			{
				q[++t] = v;
			}
		}
	}
	int maxx = 0;
	for(int i = 1;i<=scc_cnt;i++)
	{
		maxx = max(maxx,f[i]);
	}
	printf("%d",maxx);
	return 0;
}
CF999E Reachability from the Capital

对于一个强联通分量的点集,能到达其中一个点,那么其它的点也一定能够到达,发现可以缩点,缩点后你会发现只需要找到所有入度为 0 0 0 的点(不包含首都所在的强连通分量缩完后的点)全部新建道路,你就会发现其它首都无法到达但是原本入度不为 0 0 0 的点也变得首都能够到达了。显而易见,这肯定是最优方案。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
vector<int>a[N];
vector<int>g[N];
int cnt,scc_cnt,top;
int dfn[N],low[N];
int scc_color[N];
int sta[N];
vector<int>e[N];
int x[N],y[N];
int dep[N];
void dfs(int x)
{
	dfn[x] = low[x] = ++cnt;
	sta[++top] = x;
	for(int v:a[x])
	{
		if(!dfn[v])
		{
			dfs(v);
			low[x] = min(low[x],low[v]);
		}
		else if(!scc_color[v])
		{
			low[x] = min(low[x],low[v]);
		}
	}
	if(dfn[x] == low[x])
	{
		scc_cnt++;
		int u;
		do
		{
			u = sta[top--];
			scc_color[u] = scc_cnt;
			g[scc_cnt].push_back(u);
		}
		while(u!=x);
		sort(g[scc_cnt].begin(),g[scc_cnt].end());
	}
}
signed main()
{
	int n,m,s;
	scanf("%d %d %d",&n,&m,&s);
	for(int i = 1;i<=m;i++)
	{
		scanf("%d %d",&x[i],&y[i]);
		a[x[i]].push_back(y[i]);
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i);
		}
	}
	for(int i = 1;i<=m;i++)
	{
		if(scc_color[x[i]]!=scc_color[y[i]])
		{
			e[scc_color[x[i]]].push_back(scc_color[y[i]]);
			dep[scc_color[y[i]]]++;
		}
	}
	int num = 0;
	for(int i = 1;i<=scc_cnt;i++)
	{
		if(!dep[i]&&i!=scc_color[s])
		{
			num++;
		}
	}
	printf("%d",num);
	return 0;
}

后面还会有更多习题,敬请期待!!

七、什么是割边

对于无向图(注意:图不一定连通),割边的定义为:去掉这条边后图的连通块数量增加。

八、Tarjan 求割边

先放一个图(转载自shi_kan老师):

我们要判断 x → s o n x \rightarrow son xson 这条边是否是割边(绿色的边表示 s o n son son l o w low low,被打叉的蓝色的边不用管),首先我们先看第一个图, s o n son son l o w low low 是自己,删掉 x → s o n x \rightarrow son xson 这条边后明显图的连通块数量增加,所以说第一个图的情况是割边,再看第二个图, s o n son son l o w low low x x x,你会发现删掉 x → s o n x \rightarrow son xson 这条边后 s o n son son 依旧可以连向 x x x,图连通块数量不增加,第二个图的情况是割边,最后看第三个图, s o n son son l o w low low 是比 x x x 更早遍历的点,那删掉 x → s o n x \rightarrow son xson 这条边后图的连通块数量更不可能增加,所以第三个图的情况不是割边,于是我们总结出了结论:在搜索的时候只要 l o w s o n ≥ d f n s o n low_{son} \ge dfn_{son} lowsondfnson,那么说明 x → s o n x \rightarrow son xson 这条边是割边。

无向图只存在树边和返祖边,而返祖边又不可能是割边,因为删掉返祖边不会使图的连通块数量增加,所以判断完返祖边后不需要判断这条边是否是割边,只需要在判断树边的时候判断是否是割边,详见割边板子。

九、探讨 l o w low low 无法实时更新出正确答案对求割边的影响

前面就说过 Tarjan 算法的 l o w low low 并没有办法实时更新出正确答案,我们已经证明过 l o w low low 没有办法实时更新出正确答案对 Tarjan 求强连通分量是没有影响的,那对求割边是否存在影响?首先我们依然要证明两个东西,第一个是如果正确的 l o w s o n low_{son} lowson 大于等于 d f n s o n dfn_{son} dfnson,那么不准确的 l o w s o n low_{son} lowson 也一定得大于等于 d f n s o n dfn_{son} dfnson 才行,你会发现这显然是对的,因为正确的 l o w s o n low_{son} lowson 都连不向其它更小的点,那不准确的 l o w s o n low_{son} lowson 更不可能连向其它更小的点,然后第二个要证明的事情就是如果正确的 l o w x low_x lowx 小于 d f n s o n dfn_{son} dfnson,那么不准确的 l o w x low_x lowx 也一定得小于 d f n s o n dfn_{son} dfnson,这个就有意思了,你想想不准确的 l o w s o n low_{son} lowson 为什么不准确?那不就是因为 s o n son son 连向了一个点,但是 s o n son son 比那个点先搜完,那么后面如果那个点更新了那 l o w s o n low_{son} lowson 就变得不准确了,但是这有关系吗?没有,因为我们已经连出去了才会出现 l o w s o n low_{son} lowson 变得不准确,那我们都已经连出去了那肯定不准确的 l o w s o n low_{son} lowson 就一定小于 d f n s o n dfn_{son} dfnson 了。

十、Tarjan 求割边板子

#include<bits/stdc++.h>
using namespace std;
const int N = ;//数据范围自己定
struct node
{
	int x;
	int id;
};
vector<node>a[N];
int cnt;
int dfn[N],low[N];
int cut;
int is_cut[N];
void dfs(int x,int id)//id表示走到x的边的编号
{
	dfn[x] = low[x] = ++cnt;
	for(node v:a[x])
	{
		if(!dfn[v.x])
		{
			dfs(v.x,v.id);
			low[x] = min(low[x],low[v.x]);
			if(low[v.x]>=dfn[v.x])
			{
				is_cut[v.id] = 1;
				cut++;
			}
		}
		else if(id!=v.id)//因为前面建图的时候正着连了一条边,反着也连了一条边,所以说这里不能把x连向son的另外一条边当成返祖边
		{
			low[x] = min(low[x],low[v.x]);
		}
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back({y,i});
		a[y].push_back({x,i});
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,0);
		}
	}
	//自定义输出
	return 0;
}

注意:这只是一个板子,应用时请随机应变。

十一、Tarjan 求割边练习题

T103481 【模板】割边

割边模板题,代码放上,供参考:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e4+5;
struct node
{
	int x;
	int id;
};
vector<node>a[N];
int cnt;
int dfn[N],low[N];
int cut;
void dfs(int x,int id)
{
	dfn[x] = low[x] = ++cnt;
	for(node v:a[x])
	{
		if(!dfn[v.x])
		{
			dfs(v.x,v.id);
			low[x] = min(low[x],low[v.x]);
			if(low[v.x]>=dfn[v.x])
			{
				cut++;
			}
		}
		else if(id!=v.id)
		{
			low[x] = min(low[x],low[v.x]);
		}
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back({y,i});
		a[y].push_back({x,i});
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,0);
		}
	}
	printf("%d",cut);
	return 0;
}
CF118E Bertown roads(有重要知识点,必看)

首先,图里不能存在割边,不然你给割边标成任何方向都无法构成强连通图(强连通图的意思就是整个图是一个强连通分量),然后我们用 Tarjan 跑一遍割边,你就会发现可以在跑 Tarjan 的时候顺便记录边的方向,树边就正常连,返祖边就反着连,你会很容易地发现这样图一定构成了强连通图。然后还有一个问题,之前的割边板子,我没有在判断返祖边的时候加上 dfn[v.x]<dfn[x] 这个判断条件,首先这个判断是帮我们去除掉返祖边的另一条边(因为我们无向图建图必须得正向反向都建,那一条返祖边肯定也存在另一条边,只是那条边的 x x x s o n son son 互换了,就有点类似有向图中的前向边,注意,它不是前向边,因为无向图不存在前向边),但是割边的板子判断返祖边只是用来更新一下 l o w low low,所以就算遍历到了返祖边的另外一条边也不影响 l o w low low 的更新,但是我们这道题判断返祖边后还要连边,这个时候就必须加上这句话了,不然就会出现连边错误的情况。

代码:

#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+5;
struct node
{
	int x;
	int id;
}ans[N];//本来应该再定义一个结构体来存边的,但是懒得再弄了,反正变量数量都一样,虽然变量名有点怪…… 
vector<node>a[N];
int cnt;
int dfn[N],low[N];
int cut;
void dfs(int x,int id)
{
	dfn[x] = low[x] = ++cnt;
	for(node v:a[x])
	{
		if(!dfn[v.x])
		{
			dfs(v.x,v.id);
			low[x] = min(low[x],low[v.x]);
			if(low[v.x]>=dfn[v.x])
			{
				printf("0");
				exit(0);
			}
			ans[v.id] = {x,v.x};
		}
		else if(id!=v.id&&dfn[v.x]<dfn[x])
		{
			low[x] = min(low[x],low[v.x]);
			ans[v.id] = {x,v.x};
		}
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back({y,i});
		a[y].push_back({x,i});
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,0);
		}
	}
	for(int i = 1;i<=m;i++)
	{
		printf("%d %d\n",ans[i].x,ans[i].id);//不要搞混淆了,本人只是懒得再定义一个结构体,id就是y 
	}
	return 0;
}

后面还会有更多习题,敬请期待!!

十二、什么是割点

对于无向图(注意:图不一定连通),割点的定义为:去掉这个点后图的连通块数量增加。

十三、Tarjan 求割点

还是这个图:

这次我们要判断的是 x x x 是否是割点,首先看第一个图,明显删掉 x x x 后图连通块数量增加,所以第一个图的情况是割点,再看第二个图,明显删掉 x x x 后图连通块数量增加,所以第二个图的情况是割点,最后是第三个图,明显删掉 x x x 后图连通块数量不增加,所以第二个图的情况不是割点,于是结论又来了,当 l o w s o n ≥ d f n x low_{son} \ge dfn_{x} lowsondfnx 时, x x x 是割点。

十四、Tarjan 求割点的注意事项(必看,否则就掉坑)

  • 注意注意再注意!!!!!!!!!这里的 l o w low low 和我们之前的 l o w low low 的求法有区别!!!也就是说这里的 l o w low low 不是我们之前定义的 l o w low low 了,因为如果我们的 s o n son son 由一条返祖边连向 x x x,这个时候我们的 l o w s o n low_{son} lowson 就有可能通过 x x x 更新为了其它更小的点,那如果这是 s o n son son 唯一能走到的比 x x x 小的点的方法,那根据我们的判断我们就会认为 x x x 不是割点,但是你会发现其实 x x x 是割点,因为你删掉 x x x 后, s o n son son 不可能走到比 x x x 更小的点,所以说判断返祖边的时候,得使用 l o w x = min ⁡ ( l o w x , d f n s o n ) low_x = \min(low_x,dfn_{son}) lowx=min(lowx,dfnson) 来更新 l o w low low
  • 注意!!!!!!当 x x x 是根并且 x x x 的儿子只有 0 0 0 个或 1 1 1 个的时候我们会把 x x x 当成割点,但是实际上 x x x 不是割点,所以我们得加个特判!!!

十五、Tarjan 求割点板子

#include<bits/stdc++.h>
using namespace std;
const int N = ;//数据范围自己定
vector<int>a[N];
int cnt;
int dfn[N],low[N];
int cut;
int root;
int is_cut[N];//标记每个点是否是割点 
void dfs(int x,int fa)
{
	dfn[x] = low[x] = ++cnt;
	int son = 0;
	for(int v:a[x])
	{
		if(!dfn[v])
		{
			son++;
			dfs(v,x);
			low[x] = min(low[x],low[v]);
			if(low[v]>=dfn[x]&&fa!=0)
			{
				cut+=1-is_cut[x];//因为可能会遍历很多个son都满足要求,但只能统计一次 
				is_cut[x] = 1;
			}
		}
		else
		{
			low[x] = min(low[x],dfn[v]);
		}
	}
	if(!fa&&son>1)
	{
		cut++;
		is_cut[x] = 1;
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back(y);
		a[y].push_back(x);
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,0);
		}
	}
	//自定义输出
	return 0;
}

注意:这只是板子,应用时请随机应变。

十六、Tarjan 求割点例题

P3388 【模板】割点(割顶)

割边模板题,代码放上,供参考:

#include<bits/stdc++.h>
using namespace std;
const int N = 5e4+5;
vector<int>a[N];
int cnt;
int dfn[N],low[N];
int cut;
int root;
int is_cut[N];//标记每个点是否是割点 
void dfs(int x,int fa)
{
	dfn[x] = low[x] = ++cnt;
	int son = 0;
	for(int v:a[x])
	{
		if(!dfn[v])
		{
			son++;
			dfs(v,x);
			low[x] = min(low[x],low[v]);
			if(low[v]>=dfn[x]&&fa!=0)
			{
				cut+=1-is_cut[x];//因为可能会遍历很多个son都满足要求,但只能统计一次 
				is_cut[x] = 1;
			}
		}
		else
		{
			low[x] = min(low[x],dfn[v]);
		}
	}
	if(!fa&&son>1)
	{
		cut++;
		is_cut[x] = 1;
	}
}
signed main()
{
	int n,m;
	scanf("%d %d",&n,&m);
	for(int i = 1;i<=m;i++)
	{
		int x,y;
		scanf("%d %d",&x,&y);
		a[x].push_back(y);
		a[y].push_back(x);
	}
	for(int i = 1;i<=n;i++)
	{
		if(!dfn[i])
		{
			dfs(i,0);
		}
	}
	printf("%d\n",cut);
	for(int i = 1;i<=n;i++)
	{
		if(is_cut[i])
		{
			printf("%d ",i);
		}
	}
	return 0;
}

后面还会更新更多例题,敬请期待!!

后面还会更新 Tarjan 求点双连通分量/边双连通分量,敬请期待!!

原创作者: linjinkun 转载于: https://www.cnblogs.com/linjinkun/p/18760907
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值