【算法每日一练]-图论(保姆级教程篇11 tarjan模板篇)无向图的桥 #无向图的割点 #有向图的强连通分量

目录

预备知识

模板1:无向图的桥

模板2:无向图的割点

模板3:有向图的强连通分量


        

        

讲之前先补充一下必要概念:

预备知识

无向图的【连通分量】:
即极大联通子图,再加入一个节点就不再连通(对于非连通图一定两个以上的连通分量)

无向图的【(割边或)桥】:
即去掉该边,图就变成了两个连通子图

无向图的【割点】:
将该点和相关联的边去掉,图将变成两个及以上的子图
注意:有割点不一定有桥,但是有桥一定有割点

        
无向图的【边双连通图】:
无向图中不存在桥,即删除一条边后仍然连通(每两个点间有至少两条路径,且路径上的边互不重复)    

        
无向图的【点双连通图】:
无向图中不存在割点,删除一个点后仍然联通(顶点大于2时,每两个点间有至少两条路径,且路径上的点互不重复) 

        
 【边双连通分量eDDC】:极大边双连通图 
 【点双连通分量vDDC】: 极大点双连通图

        
无向图的【缩点】1:
把每个【eDDC】看成一个点,桥看成连接两个点的无向边,得到一棵树。这种方式称为eDCC缩点

无向图的【缩点】2:
把每个【vDDC】看成一个点,割点看成一个点,每个割点向包含它vDDC连接一条边,得到一棵树。这种方式称为vDCC缩点

有向图的【强连通分量】:

即极大连通子图(任何两个节点都能相互到达)

        

模板1:无向图的桥

 首先介绍时间戳和回溯点(我更喜欢叫它走回点,听我往下分析)

dfn[u] (时间戳) 表示u节点深度优先遍历最先访问的序号,

low[u](走回点)表示u节点或子孙能走回到的最小节点序号。

给出结论:

无向边是桥,当搜索树上存在x的某个子节点y满足 low[y]>dfn[x](没有等号)

怎么理解呢?
首先就是你不能走父子边回去,也就是怎么来的怎么回去,这样肯定是不行的。我们必须要走非父子边回去的。因为当从非父子边能回去时,也就意味着走到了环,那么此时就获取能回到的最小dfn为low,然后回溯时候父节点获取孩子更小的low即可。相当于走到环了,那么把这个最小的dfn传遍整个环中,也就是整个连通分量中。从而表示u节点或子孙能走回到的最小节点序号。这样的话同一个连通分量中low值是一样的,是最早走回的点号!(看不懂可以画一下图,就理解了)
        

对于无向图的桥:孩子的low值比父亲的dfn值大就说明有桥(这个可怜的孩子不能走非父子边回家了,能不可怜吗??)

void tarjan(int u,int fa){
	dfn[u]=low[u]=++num;//初始化
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v==fa) continue;//不可以走父子边回去
		if(!dfn[v]){//没访问过就递归访问,访问完要更新low
			tarjan(v,u);
			low[u]=min(low[u],low[v]);//获取孩子最小的low值(low是自己或子孙能走回的最小dfn嘛,不是回溯)
			if(low[v]>dfn[u]){//孩子的low值比父亲的dfn值大说明有桥
				cout<

这段代码把“回溯大法”体现的很好。可以看到没访问过就递归访问,返回后,也就是等到孩子们的low值都变正确了,我再去更新自己的low。

另外那一句else其实是留给遇环来处理的,父亲节点根本用不上,你只需要在孩子们遇环后把它们自己“照顾好了”之后,借助它们的结果来优化自己就行了。

        

完整代码 

#include //无向图的桥
using namespace std;
const int maxn=1000+5;
int n,m;
int head[maxn],cnt;
struct node{int to,next;}e[maxn*2];
int low[maxn],dfn[maxn],num;
//dfn[u](时间戳)表示u节点深度优先遍历访问的序号
//low[u](走回点)表示u节点或子孙能走回到的最小节点序号

void add(int u,int v){ e[++cnt]=(node){v,head[u]};head[u]=cnt;}
void tarjan(int u,int fa){
	dfn[u]=low[u]=++num;//初始化
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v==fa) continue;//不可以走父子边回去
		if(!dfn[v]){//没访问过就递归访问,访问完要更新low
			tarjan(v,u);
			low[u]=min(low[u],low[v]);//获取孩子最小的low值(low是自己或子孙能走回的最小dfn嘛,不是回溯)
			if(low[v]>dfn[u]){//孩子的low值比父亲的dfn值大说明有桥
				cout<>n>>m){
		init();
		int u,v;
		while(m--){
			cin>>u>>v;
			add(u,v);
			add(v,u);
		}
		for(int i=1;i<=n;i++){//因为不一定整个图都联通,所以要判断那些点是否走不到
			if(!dfn[i]) tarjan(i,i);//深度优先搜索树的起点就是树根
		}
	}	
}

可以输入数据或自己画图试一下
7 7
1 4
1 2
2 3
3 5
5 7
5 6
6 4

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

        

        

模板2:无向图的割点

x是割点条件:x不是根节点 且搜索树上存在x的一个子节点y,满足low[y]>=dfn[x]

【或者】x是根节点 且搜索树上存在至少两个子节点满足上述条件 (画图可证明,这里不多说了)

#include //无向图的割点
using namespace std;
const int maxn=1000+5;
int n,m,root;
int head[maxn],cnt;
struct node{int to,next;}e[maxn*2];
int low[maxn],dfn[maxn],num;

void add(int u,int v){ e[++cnt]=(node){v,head[u]};head[u]=cnt;}
void tarjan(int u,int fa){
	dfn[u]=low[u]=++num;//初始化
	int count=0;//记录有几个满足条件的子节点
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(v==fa) continue;//没访问过就递归访问,访问完更新low
		if(!dfn[v]){
			tarjan(v,u);
			low[u]=min(low[u],low[v]);//low是自己或子孙能走回的最小dfn
			if(low[v]>=dfn[u]){
				count++;
				if(u!=root||count>1)cout<>n>>m){
		init();
		int u,v;
		while(m--){
			cin>>u>>v;
			add(u,v);
			add(v,u);
		}
		for(int i=1;i<=n;i++){//因为不一定整个图都联通,所以要判断那些点是否走不到
			if(!dfn[i]) {
				root=i;
				tarjan(i,i);
		}
		}
	}	
}

测试数据(供你参考使用)
7 7
1 4
1 2
2 3
3 5
5 7
5 6
6 4

5 3
1 2
2 3
3 5

        

        

        

模板3:有向图的强连通分量

有向图的【强连通分量】:即极大连通子图(任何两个节点都能相互到达)
        

if(low[u]==dfn[u]){//在回溯之前,在low[u]==dfn[u]时,则从栈中不断弹出节点,直到x出栈停止。弹出的节点就是一个连通分量
		int v;
		do{//输出强连通分量(一定要先执行再判断)
			v=s.top();s.pop();
			cout<<"V: "<

前面和无向图基本一样(无非不用跳过走父子边步骤),后面内容就不太一样了。

判断条件:在low[u]==dfn[u]时,则从栈中不断弹出节点,直到x出栈停止。弹出的节点就是一个连通分量。

#include //有向图的强连通分量
using namespace std;
const int maxn=1000+5;
bool ins[maxn];
int n,m;
int head[maxn],cnt;
stack  s;
struct node{int to,next;}e[maxn*2];
int low[maxn],dfn[maxn],num;

void add(int u,int v){ e[++cnt]=(node){v,head[u]};head[u]=cnt;}

void tarjan(int u){
	dfn[u]=low[u]=++num;//dfn访问序号,low使能回溯到的最早的dfn
	ins[u]=1;
	s.push(u);//第一次访问节点时候入栈
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].to;
		if(!dfn[v]){//没访问过就递归访问
			tarjan(v);
			low[u]=min(low[u],low[v]);//获取孩子的最小的low值   
		}
		else if(ins[v]){//已经访问过且在栈中获取dfn号
			low[u]=min(low[u],dfn[v]);
		}
	}
	if(low[u]==dfn[u]){//在回溯之前,在low[u]==dfn[u]时,则从栈中不断弹出节点,直到x出栈停止。弹出的节点就是一个连通分量
		int v;
		do{//输出强连通分量(一定要先执行再判断)
			v=s.top();s.pop();
			cout<<"V: "<>n>>m){
		init();
		int u,v;
		while(m--){
			cin>>u>>v;
			add(u,v);
		}
		for(int i=1;i<=n;i++){//有向图不一定整个图都双向联通,所以要判断那些点是否走不到
			if(!dfn[i]) 	tarjan(i);
		}
	}	
}

参考数据
5 8
1 2
1 3
3 2
3 4
3 5
4 1
4 5
5 1

你可能感兴趣的:(图论,图论,算法,深度优先,数据结构,c++)