Tarjan算法与无向图的连通性

无向图的割点与桥

给定无向连通图G=(V,E):
若对于x∈V,从图中删去节点x以及所有与x关联的边之后,G分裂成两个

或两个以上不相连的子图,则称x为G的割点.
若对于e∈E,从图中删去边e之后,G分裂成两个不相连的子图,则称e为
G的桥或割边.
一般无向图(不一定连通)的“割点”和“桥”就是它的各个连通块的“割点”和
“桥”
根据著名计算机科学家RobertTarjan的名字命名的Tarjan算法能够在线性时间内
(求出无向图的割点与桥,进一步可求出无向图的双连通分量(本节后半部分将会介绍)。
在有向图方面,Tarjan算法能够求出有向图的强连通分量、必经点与必经边(下一节将
(会介绍)。罗伯特·塔尔健在数据结构方面也做出了很多卓有成效的工作,包括证明并查集的时间复杂度,提出斐波那契堆、Splay Tree和Lint-Cut Tree等.

时间戳

在图的深度优先遍历过程中,按照每个节点第一次被访间的时间顺序,依次给予N
个节点1~N的整数标记,该标记就被称为“时间戳”,记为dfn[x]。

搜索树

在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问- -次。 所有发
生递归的边(x,y) (换言之,从x到y是对y的第一次访问)构成- - 棵树,我们把
它称为“无向连通图的搜索树”。当然,一般无向图 (不- - 定连通)的各个连通块的搜
索树构成无向图的“搜索森林”。
下图左侧展示了一张无向连通图,灰色节点是深度优先遍历的起点,加粗的边是
“发生递归”的边(假没我们在遇到多个分支时,总是优先访问最靠左的一一条)。右侧
展示了深度优先遍历的搜索树,并标注了节点的时间戳。

追溯值

除了时间戳之外,Tarjan 算法还引入了一个“追溯值”low[x]。设subree(x) 表
示搜索树中以x为根的子树。low[x] 定义为以下节点的时间戳的最小值:

  1. subtree(x)中的节点。
    2.通过1条不在搜索树上的边,能够到达subtree(x) 的节点。
    以上图为例。为了叙述简便,我们用时间戳代替节点编号。subtree(2) = {2.3.4.5}。
    另外,节点1通过不在搜索树上的边(1,5) 能够到达subtree(2)。 所以low|2]= 1。
    根据定义,为了计算low[x], 应该先令low[x] = dfn[x],然后考虑从x出发的
    每条边(x,y):
    若在搜索树上x是y的父节点,则令low[x] = min(low[x], low[y])。
    若无向边(x,y) 不是搜索树上的边,则令low[x] = min(low[x], dfn[y]).
    下页图的中括号0里的数值标注了每个节点的“追溯值”low。

割边判定法则

d f n [ x ] < l o w [ y ] dfn[x]<low[y] dfn[x]<low[y]
y属于x的一个子节点
感性理解一下就是,没有其他的路径可比这个点x 更早地走到y点,因此删掉这个点之后整张图就断裂开了,所以说这条边必然是桥,但是桥必然是在搜索树上面的,因此当输入的图有重边的时候,就无法判断,因为其他的重边不在dfs树里面,因此搜索的时候,dfs的传参为那一条边,而且连接自己父节点的边都是成对出现的,因此可以用异或1来实现成对变换

#include<bits/stdc++.h>
#define I inline
#define RI register int 
#define N 1000100
#define LL long long 
using namespace std;
I int read()
{
	RI res=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-f;ch=getchar();}
	while(isdigit(ch))res=(res<<1)+(res<<3)+(ch&15),ch=getchar();
	return res*f;
}
int dfn[N],low[N],bridge[N],tot,head[N];
struct node{
	int to,next;
}edge[N<<1];
I void add(int x,int y){
	edge[++tot].next=head[x];
	edge[tot].to=y;
	head[x]=tot;
}
int n,m,num;
I void Tarjan(int x,int ineg){
	dfn[x]=low[x]=++num;
	for(RI i=head[x];i;i=edge[i].next){
		int to=edge[i].to;
		if(!dfn[to]){
			Tarjan(to,i);
			low[x]=min(low[x],low[to]);
			if(low[to]>dfn[x])
				bridge[i]=bridge[i^1]=1;
		}else if(i!=(ineg^1))low[x]=min(low[x],dfn[to]);
	}
}
int main()
{
	n=read();m=read();tot=1;
	for(RI u,v,i=1;i<=m;i++){
		u=read();v=read();
		add(u,v);add(v,u);
	}for(RI i=1;i<=n;i++)if(!dfn[i])Tarjan(i,0);
	for(RI i=2;i<tot;i+=2)if(bridge[i])printf("%d %d\n",edge[i].to,edge[i+1].to);
	return 0;
}

割点判定法则

若x不是搜索树的根节点(深度优先遍历的起点),则x是割点当且仅当搜索树
上存在x的一个子节点y,满足:
d f n [ x ] ≤ l o w [ y ] dfn[x]≤low[y] dfn[x]low[y]
特别地,若x是搜索树的根节点,则x是割点当且仅当搜索树上存在至少两个
子节点y1.Y2满足上述条件。
证明方法与割边的情形类似,这里就不再赘述。在“割边判定法则”画出的例子
中,共有2个割点,分别是时间戳为1和6的两个点。
下面的程序求出一张无向图中所有的割点。因为割点判定法则是小于等于号,所以
在求割点时,不必考虑父节点和重边的问题
,从x出发能访问到的所有点的时间戳都
可以用来更新low[x]。

#include<bits/stdc++.h>
#define I inline
#define RI register int 
#define N 6000100
#define LL long long 
using namespace std;
I int read()
{
	RI res=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-f;ch=getchar();}
	while(isdigit(ch))res=(res<<1)+(res<<3)+(ch&15),ch=getchar();
	return res*f;
}
int dfn[N],low[N],bridge[N],tot,head[N];
struct node{
	int to,next;
}edge[N<<1];
I void add(int x,int y){
	edge[++tot].next=head[x];
	edge[tot].to=y;
	head[x]=tot;
}
int n,m,num,root,vis[N],d;
I void Tarjan(int x){
	dfn[x]=low[x]=++num;int flag=0;
	for(RI i=head[x];i;i=edge[i].next){
		int to=edge[i].to;
		if(!dfn[to]){
			Tarjan(to);
			low[x]=min(low[x],low[to]);
			if(low[to]>=dfn[x]){
				flag++;
				if(flag>1||x!=root)vis[x]=1;
			}
				
		}else low[x]=min(low[x],dfn[to]);
	}
}
int main()
{
//	freopen("P3388_4.in","r",stdin);
//	freopen("1.txt","w",stdout);
	n=read();m=read();
	for(RI u,v,i=1;i<=m;i++){
		u=read();v=read();if(u==v)continue;
		add(u,v);add(v,u);
	}for(RI i=1;i<=n;i++)if(!dfn[i])root=i,Tarjan(i);
	for(RI i=1;i<=n;i++)if(vis[i])d++;
	cout<<d<<endl;
	for(RI i=1;i<=n;i++)if(vis[i])printf("%d ",i);
	return 0;
}

◆无向图的双连通分量

若一张无向连通图不存在割点,则称它为“点双连通图”。若-张无向连通图不存
在桥,则称它为“边双连通图”。
无向图的极大点双连通子图被称为“点双连通分量”。简记为“v-DCC"中。无向
连通图的极大边双连通子图被称为“边双连通分量”,简记为“e-DCC".二者统称
为“双连通分量”,简记为“DCC"。
E"SE并且G"也是双连通子图。
定理
-张无向连通團是“点双连通图”,当且仅当满足下列两个条件之一 :
1.图的项点数不超过2。
2.图中任意两点都同时包含在至少一个简单环中。其中“简单环”指的是不自交
的环,也就是我们通常画出的环。
一张无向连通图是“边双连通图”,当且仅当任意-条边都包含在至少一个简单
环中。

证明:

该定理给出了无向连通图是“点双连通图”或“边双连通图”的充要条件。我们
以“点双连通图”为例进行证明,对于“边双连通图"的证明,请读者自己完成。
对于顶点数不超过2的情况,定理显然成立,下 面假设图中顶点数不小于3.
先证充分性。若任意两点x,y都同时包含在至少一个简单环中,则x,y之间至
少有两条不相交的路径。无论从图中删除哪个节点。xy均能通过两条路径之一相连。
故图中不存在割点,是点双连通图。
再证必要性。反证法,假设- -张无向连通图是“点双连通图" ,并且存在两点x,y. .
它们不同时处于任何一个简单环中。
如果x,y之间仅存在1条简单路径,那么路径上至少有一个割点,与“点双连通”
矛盾。
如果x,y之间存在2条或2条以上的简单路径,那么容易发现,任意两条都至少
有一个除x,y之外的交点:进-一步可推导出,x,y 之间的所有路径必定同时交于除
x,y之外的某- -点p(不然就会存在两条没有交点的路径,形成- -个简单环,如下图所
示)。
根据定义,p是一个割点。与“点双连通”矛盾。故假设不成立。
证毕。

边双连通分量(e-DCC)的求法

边双连通分量的计算非常容易。只需求出无向图中所有的桥,把桥都删除后,无向
图论
图会分成若干个连通块,每一个连通块就是一个“边双连通分量”。
下面的无向图共有2条“桥”边。3个“边双连通分量" :

在具体的程序实现中,- -般先用Tarjan算法标记出所有的桥边。然后,再对整个
无向图执行-次深度优先遍历(遍历的过程中不访问桥边)。划分出每个连通块。下面
的代码在Taran求桥的参考程序基础上,计算出数组e, c[x]表示节点x所属的“边
双连通分量”的编号。

#include<bits/stdc++.h>
#define I inline
#define RI register int 
#define N 6000100
#define LL long long 
using namespace std;
I int read()
{
	RI res=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-f;ch=getchar();}
	while(isdigit(ch))res=(res<<1)+(res<<3)+(ch&15),ch=getchar();
	return res*f;
}
int dfn[N],low[N],bridge[N],tot,head[N];
struct node{
	int to,next;
}edge[N<<1];
I void add(int x,int y){
	edge[++tot].next=head[x];
	edge[tot].to=y;
	head[x]=tot;
}
int n,m,num;
I void Tarjan(int x,int ineg){
	dfn[x]=low[x]=++num;
	for(RI i=head[x];i;i=edge[i].next){
		int to=edge[i].to;
		if(!dfn[to]){
			Tarjan(to,i);
			low[x]=min(low[x],low[to]);
			if(low[to]>dfn[x])
				bridge[i]=bridge[i^1]=1;
		}else if(i!=(ineg^1))low[x]=min(low[x],dfn[to]);
	}
}
vector<int>G[N];
int vis[N],cnt;
I void dfs(int x){
	vis[x]=cnt;G[cnt].push_back(x);
	for(RI i=head[x];i;i=edge[i].next){
		int to=edge[i].to;
		if(vis[to]||bridge[i])continue;
		dfs(to);
	}
} 
int main()
{
	n=read();m=read();tot=1;
	for(RI u,v,i=1;i<=m;i++){
		u=read();v=read();
		add(u,v);add(v,u);
	}for(RI i=1;i<=n;i++)if(!dfn[i])Tarjan(i,0);
	for(RI i=1;i<=n;i++)if(!vis[i])++cnt,dfs(i); 
	printf("%d \n",cnt);
	for(RI i=1;i<=cnt;i++){printf("%d ",G[i].size());
		for(RI j=0;j<G[i].size();j++)
		printf("%d ",G[i][j]);putchar('\n'); 
	}
//	for(RI i=2;i<tot;i+=2)if(bridge[i])printf("%d %d\n",edge[i].to,edge[i+1].to);
	return 0;
}

点双连通分量(-DCC)的求法

“点双连通分量”是一个极其容易误解的概念。它与前面的例题“BLO"中“删除
割点后图中剩余的连通块”是不一样的, 请读者格外留意。
若某个节点为孤立点,则它自己单独构成一个v-DCC.除了孤立点之外。点双连通
分量的大小至少为2。根据v-DCC定义中的“极大”性,虽然桥不属于任何e-DCC,但
是制点可能属于多个v-DCC.还是来看本节- - 直使用的例子,下面的无向图共有2个
割点,4个“点双连通分量”.。
为了求出“点双连通分量”,需要在Tarlan算法的过程中维护一个栈,并按照如
下方法维护栈中的元素: .
1.当一个节点第一次被访问时,把该节点入栈。
2.当割点判定法则中的条件dfn[x] s low[y]成立时,无论x是否为根,都要:
(1)从栈顶不断弹出节点,直至节点y被弹出。
(2)刚才弹出的所有节点与节点x -起构成一-个v-DCC.
下面的程序在求出割点的同时。计算出vector数组dcc. dee[i] 保存编号为i的
v-DCC中的所有节点。

#include<bits/stdc++.h>
#define I inline
#define RI register int 
#define N 6000100
#define LL long long 
using namespace std;
I int read()
{
	RI res=0,f=1;char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')f=-f;ch=getchar();}
	while(isdigit(ch))res=(res<<1)+(res<<3)+(ch&15),ch=getchar();
	return res*f;
}
int dfn[N],low[N],bridge[N],tot,head[N];
struct node{
	int to,next;
}edge[N<<1];
I void add(int x,int y){
	edge[++tot].next=head[x];
	edge[tot].to=y;
	head[x]=tot;
}
int stk[N];
int n,m,num,root,vis[N],top,cnt;
vector<int>G[N];
//int root;
I void Tarjan(int x){
	if(head[x]==0&&x==root){G[++cnt].push_back(x);return;}
	stk[++top]=x;dfn[x]=low[x]=++num;int flag=0;
	for(RI i=head[x];i;i=edge[i].next){
		int to=edge[i].to;
		if(!dfn[to]){
			Tarjan(to);
			low[x]=min(low[x],low[to]);
			if(low[to]>=dfn[x]){
				flag++;
				if(flag>1||x!=root)vis[x]=1;RI z;
				cnt++;do{ z=stk[top--];G[cnt].push_back(z);}while(z!=to);
				G[cnt].push_back(x);
			}
				
		}else low[x]=min(low[x],dfn[to]);
	}
}
int main()
{
	n=read();m=read();
	for(RI u,v,i=1;i<=m;i++){
		u=read();v=read();if(u==v)continue;
		add(u,v);add(v,u);
	}for(RI i=1;i<=n;i++)if(!dfn[i])root=i,Tarjan(i);cout<<cnt<<endl;
//	for(RI i=1;i<=n;i++)if(vis[i])printf("%d ",i);
	for(RI i=1;i<=cnt;i++){cout<<G[i].size()<<' ';if(!G[i].size())continue;
		for(RI j=0;j<G[i].size();j++)cout<<G[i][j]<<' ';cout<<endl;
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 在 Python 中,可以使用 NetworkX 库来实现 Tarjan 算法。首先需要安装 NetworkX,可以使用 pip 安装: ``` pip install networkx ``` 然后可以使用下面的代码来输出无向图中的所有环: ``` import networkx as nx G = nx.Graph() # 添加边 G.add_edges_from([(1, 2), (2, 3), (3, 1), (3, 4), (4, 5), (5, 3)]) cycles = nx.simple_cycles(G) for cycle in cycles: print(cycle) ``` 运行上面的代码将会输出无向图中所有环的信息,每个环是一个由结点组成的列表。 ### 回答2: Tarjan算法是一种用于查找无向图中所有环的算法,它可以有效地找到所有环并输出。 首先,我们需要了解Tarjan算法的基本原理。Tarjan算法使用深度优先搜索(DFS)来遍历图中的所有节点,并使用一个栈维护访问过的节点。在DFS过程中,它会记录每个节点的访问次序(即dfs_num)和最小可以到达的节点的次序(即dfs_low)。如果发现存在一个节点v的子节点u,使得dfs_num[u] < dfs_low[v],则表示存在一个环。 下面是使用Python实现的Tarjan算法来输出无向图中所有的环的例子: ```python def tarjan(graph): n = len(graph) dfs_num = [-1] * n dfs_low = [-1] * n on_stack = [False] * n stack = [] result = [] def dfs(v, parent): dfs_num[v] = dfs_low[v] = len(stack) stack.append(v) on_stack[v] = True for u in graph[v]: if dfs_num[u] == -1: # 如果u未被访问过 dfs(u, v) if on_stack[u]: dfs_low[v] = min(dfs_low[v], dfs_low[u]) if dfs_low[v] == dfs_num[v]: # v是一个新的强连通分量的根节点 cycle = [] while True: u = stack.pop() on_stack[u] = False cycle.append(u) if u == v: break result.append(cycle) for v in range(n): if dfs_num[v] == -1: dfs(v, -1) return result # 示例使用方式 graph = [ [1, 3], # 节点0的连接节点,注意连接关系是双向的 [0, 2], [1, 3], [0, 2, 4], [3] ] cycles = tarjan(graph) for cycle in cycles: print(cycle) ``` 在这个例子中,我们首先定义一个无向图(使用邻接表表示)和一些用于DFS的辅助数据结构。然后,我们定义了DFS函数,它使用递归的方式进行深度遍历,并根据节点的访问次序和最小次序来判断是否存在环。最后,我们在主函数中调用DFS函数,并输出所有的环。 这就是使用Python实现Tarjan算法输出无向图中所有的环的方法。 ### 回答3: Tarjan算法是一种求解图中所有环的算法。Python中可以通过实现Tarjan算法来输出无向图中的所有环。 首先,我们需要构建图的数据结构。可以使用字典来表示图,其中键为图中的节点,值为该节点连接的其他节点的列表。 接下来,我们定义一个函数来实现Tarjan算法,该函数接受当前节点、已访问过的节点集合、当前路径上的节点集合和结果集合作为参数。在函数中,我们先将当前节点加入已访问过的节点集合和路径上的节点集合。 然后,我们遍历当前节点连接的所有节点,若该节点未被访问过,则递归调用Tarjan算法,并更新当前节点的低点。若该节点在路径上,说明找到了一个环,将路径上的节点加入结果集合。 最后,若当前节点的低点等于其自身节点,说明已经处理完了一个环,需要将结果集合输出。 以下是一个实现Tarjan算法输出无向图中所有环的示例代码: ```python def Tarjan(node, visited, path, result, graph): visited.add(node) path.append(node) low = node for neighbor in graph.get(node, []): if neighbor not in visited: low = min(low, Tarjan(neighbor, visited, path, result, graph)) elif neighbor in path: low = min(low, neighbor) if low == node: cycle = [] while path[-1] != node: cycle.append(path.pop()) cycle.append(path.pop()) result.append(cycle) return low def findCycles(graph): visited = set() result = [] for node in graph: if node not in visited: Tarjan(node, visited, [], result, graph) return result # 示例无向图 graph = { 'A': ['B', 'C'], 'B': ['A', 'C', 'D'], 'C': ['A', 'B'], 'D': ['B'], } cycles = findCycles(graph) print(cycles) ``` 运行上述代码,可以输出无向图中的所有环。对于示例无向图,输出结果为:[['B', 'A', 'C', 'B'], ['B', 'D', 'B']]。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值