寻找无向图的关节点(Articulation Points)和判断图是否是双连通图(Biconnect Graph)

17 篇文章 0 订阅

一、定义:

关节点(Articulation Poinst)是指,在原本连通的图,在删除关节点及依附的边之后,图将被分割成至少两个连通分量(原本整连通的图变得不连通)。
另外,没有一个关节点的连通图称为双连通图(Biconnect Graph)

二、求解关节点算法:

首先,将无向连通图从某个顶点开始进行DFS遍历,遍历会得到一个DFS树(深度优先生成树)
相信读者已经通过其他地方知道了下面的判断顶点u是否是关节点的原则:

1、如果u是整颗DFS树的根,如果u至少有两个子女,那么u必然是关节点。

2、DFS树中叶节点不是关节点

3、非根顶点u(不整颗DFS树的根)不是关节点的充要条件是:它的任一子孙都可以沿某条路径绕过顶点u达到u的任一祖先w (w!=u),u不属于该路径(这条路径包括回边)
ps:(回边是原本图中有的,在DFS数里没有的)

针对判定原则里的1我们再定义一个变量children用于记录当前顶点的子女数,如果当前顶点是根结点(整棵树的根),再定一个数组parent,parent[u]表示u的双亲是谁。

针对判定原则里的2,3我们再定义:
1、depth[u]为顶点uDFS树中的深度
2、low[u]为顶点u的所有子孙(包括u)能够到达的邻居中的最低(小)深度。

显然,在DFS树中如果顶点w是顶点u的祖先,那么必有depth[w] < depth[u]
因此,在DFS的时候,先让depth[u]等于low[u]等于当前节点的深度,然后再看节点u的子孙里有没有那个子孙v有到u的某个祖先w的回边,有的话更新那个子孙v以及其那个子孙v所有祖先的low[]的值为depth[w](直到那个祖先w)
然后就可以判断了,假设uv的双亲,如果low[u]>=depth[u]那么u的子孙都没一条边(回边)可以绕过u到达u的祖先,所以u是个关节点

维基百科上有个动画:(这个动画有点小问题,右边:D2回溯到D1那就可以判断D1为关节点了,还有D4回溯到D2那里没画,当然这是小问题;左边:D3回溯到D2哪里标绿线才对,标错了)
在这里插入图片描述
想要它慢点,暂停?用windows media player打开就好

看下维基上的伪代码:

GetArticulationPoints(i, d)
    visited[i] = true
    depth[i] = d
    low[i] = d
    childCount = 0
    isArticulation = false
    for each ni in adj[i]
        if not visited[ni]
            parent[ni] = i
            GetArticulationPoints(ni, d + 1)
            childCount = childCount + 1
            if low[ni] >= depth[i]
                isArticulation = true
            low[i] = Min(low[i], low[ni])
        else if ni != parent[i]
            low[i] = Min(low[i], depth[ni])
    if (parent[i] != null and isArticulation) or (parent[i] == null and childCount > 1)
        Output i as articulation point

C++代码:

#include<iostream>
#include<list> 
#include<algorithm>  //for min
#define NIL -1

using namespace std;
class Graph
{
	int n;		//图中节点个数
	list<int>* adj;   //邻接表
	void APUtil(int v,bool visited[],int depth[],int low[],
			int parent[],int curDepth,bool AP[]); 
public:
	Graph(int _n);
	void addEdge(int v,int w);
	void AP();			//求出连通图中的关节点 
};
Graph::Graph(int _n)
{
	n = _n;
	adj = new list<int>[_n];
}
void Graph::addEdge(int v,int w)
{
	adj[v].push_back(w);
	adj[w].push_back(v);
}
void Graph::APUtil(int u,bool visited[],int depth[],int low[],
			int parent[],int curDepth,bool AP[]) 
{
	visited[u] = true;				//已访问 
	int children = 0;				//孩子数 
	
	// 初始化u的深度和能到达的深度最小的祖先 
	depth[u] = low[u] = curDepth;	
	
	list<int>::iterator it;
	for( it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		
		if( !visited[v])		//v没被访问过
		{
			parent[v] = u;		//设置v的双亲 
			children++;			//u的子女+1
			APUtil(v, visited, depth, low, parent, curDepth + 1, AP); 
			
			//访问完v后回溯到这里,检查下u的子孙里有没有那个能
			//有绕过u能到达的深度最小的祖先,有的话u从子孙那边 
			//到那个祖先 ,更新low[u] 
			low[u] = min(low[u], low[v]); 
			
			//判断:
			//1、如果u是根结点(整颗生成树的根,其双亲为-1,即NIL) 
			//如果children >  1,则u是关节点
			if( parent[u] == NIL && children > 1)
			{
				AP[u] = true; 
			} 
			
			//2、不是根结点,但是有子女v无法绕过它u到它u的祖先w
			//u是关节点,此时low[v] >= depth[u] 
			if( parent[u] != NIL && low[v] >= depth[u]) 
			{
				AP[u] = true;
			}
		}
		else if( v != parent[u])			 
		{//v访问过了,说明v是当前节点u的祖先,v找到了一条边(回边到v的祖先),
		//这里排除下v是u的双亲(因为v是u的双亲的话处理的就不是回边了,这条边在DFS树)
			low[u] = min(low[u], depth[v]);			//更新low[u]; 
		} 
	} 
}
void Graph::AP()
{
	bool *visited = new bool[n];
	int *parent = new int[n];
	int *low = new int[n];
	int *depth = new int[n];
	bool *AP = new bool[n];
	
	for(int i = 0; i < n; i++)
	{
		AP[i] = false;
		parent[i] = NIL;
		visited[i] = false;
	} 
	for(int i = 0; i < n; i++)
	{
		if(!visited[i])
		{
			APUtil(i,visited,depth,low,parent,0,AP);
		}
	}
	
	for(int i = 0; i < n; i++)
	{
		if(AP[i] == true)
		{
			cout<<i<<"  ";
		}
	}
	cout<<endl;
} 

以动画里图为例测试:

int main()
{
	cout<<"输入图的关节点有:"<<endl;
	Graph g1(15);
	g1.addEdge(0,1);
	g1.addEdge(1,2);
	g1.addEdge(1,6);
	g1.addEdge(2,3);
	g1.addEdge(2,4);
	g1.addEdge(3,4);
	g1.addEdge(4,5);
	g1.addEdge(5,6);
	g1.addEdge(6,7);
	g1.addEdge(1,8);
	g1.addEdge(0,9);
	g1.addEdge(9,10);
	g1.addEdge(10,11);
	g1.addEdge(10,13);
	g1.addEdge(11,12);
	g1.addEdge(12,13);
	g1.AP();
	return 0; 
}

结果在这里插入图片描述

还有一种写法:
不用深度depth[],用disc[u]最早发现的时间的来记录当前顶点uDFS树在被发现的次序,而low[u]来表示顶点u到达的disc值最小的祖先(不是双亲)。

void Graph::APUtil(int u,bool visited[],int disc[],int low[],
			int parent[],int &time,bool AP[]) 
{
	visited[u] = true;				//已访问 
	int children = 0;				//孩子数 
	
	// 初始化u的深度和能到达的深度最小的祖先 
	disc[u] = low[u] = ++time;	
	
	list<int>::iterator it;
	for( it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		
		if( !visited[v])		//v没被访问过
		{
			parent[v] = u;		//设置v的双亲 
			children++;			//u的子女+1
			APUtil(v, visited, disc, low, parent, time, AP); 
			
			//访问完v后回溯到这里,检查下u的子孙里有没有那个能
			//有绕过u能到达的深度最小的祖先,有的话u从子孙那边 
			//到那个祖先 ,更新low[u] 
			low[u] = min(low[u], low[v]); 
			
			//判断:
			//1、如果u是根结点(整颗生成树的根,其双亲为-1,即NIL) 
			//如果children >  1,则u是关节点
			if( parent[u] == NIL && children > 1)
			{
				AP[u] = true; 
			} 
			
			//2、不是根结点,但是有子女v无法绕过它u到它u的祖先w
			//u是关节点,此时low[v] >= disc[u] 
			if( parent[u] != NIL && low[v] >= disc[u]) 
			{
				AP[u] = true;
			}
		}
		else if( v != parent[u])			 
		{//v访问过了,说明v是当前节点u的祖先,v找到了一条边(回边到v的祖先),
		//这里排除下v是u的双亲(因为v是u的双亲的话处理的就不是回边了,这条边在DFS树)
			low[u] = min(low[u], disc[v]);			//更新low[u]; 
		} 
	} 
}
void Graph::AP()
{
	bool *visited = new bool[n];
	int *parent = new int[n];
	int *low = new int[n];
	int *disc = new int[n];
	bool *AP = new bool[n];
	
	for(int i = 0; i < n; i++)
	{
		AP[i] = false;
		parent[i] = NIL;
		visited[i] = false;
	} 
	
	int time = 0;
	for(int i = 0; i < n; i++)
	{
		if(!visited[i])
		{
			APUtil(i,visited,disc,low,parent,time,AP);
		}
	}
	
	for(int i = 0; i < n; i++)
	{
		if(AP[i] == true)
		{
			cout<<i<<"  ";
		}
	}
	cout<<endl;
}

三、判断是否是双连通图(Biconnect)

用上面的求关节点算法,求完有关节点必然不是双连通图,但是如果单纯地只想判断下是不是双连通图,只要找第一个关节点就知道,如果没有,还得看是不是连通图。

bool Graph::isBCUtil(int u,bool visited[],int depth[],int low[],
			int parent[],int curDepth) 
{
	visited[u] = true;				//已访问 
	int children = 0;				//孩子数 
	
	// 初始化u的深度和能到达的深度最小的祖先 
	depth[u] = low[u] = curDepth;	
	
	list<int>::iterator it;
	for( it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		
		if( !visited[v])		//v没被访问过
		{
			parent[v] = u;		//设置v的双亲 
			children++;			//u的子女+1
		
			if( isBCUtil(v, visited, depth, low, parent, curDepth + 1) == true) 
			{
				return true;
			}
			//访问完v后回溯到这里,检查下u的子孙里有没有那个能
			//有绕过u能到达的深度最小的祖先,有的话u从子孙那边 
			//到那个祖先 ,更新low[u] 
			low[u] = min(low[u], low[v]); 
			
			//判断:
			//1、如果u是根结点(整颗生成树的根,其双亲为-1,即NIL) 
			//如果children >  1,则u是关节点
			if( parent[u] == NIL && children > 1)
			{
				return true;
			} 
			
			//2、不是根结点,但是有子女v无法绕过它u到它u的祖先w
			//u是关节点,此时low[v] >= depth[u] 
			if( parent[u] != NIL && low[v] >= depth[u]) 
			{
				return true;
			}
		}
		else if( v != parent[u])			 
		{//v访问过了,说明v是当前节点u的祖先,v找到了一条边(回边到v的祖先),
		//这里排除下v是u的双亲(因为v是u的双亲的话处理的就不是回边了,这条边在DFS树)
			low[u] = min(low[u], depth[v]);			//更新low[u]; 
		} 
	} 
	return false;
}
bool Graph::isBC()
{
	bool *visited = new bool[n];
	int *parent = new int[n];
	int *low = new int[n];
	int *depth = new int[n];
	
	for(int i = 0; i < n; i++)
	{
		parent[i] = NIL;
		visited[i] = false;
	} 
	
	//isBCUtil返回图中有没有关节点 
	if( isBCUtil(0,visited,depth,low,parent,0) == true)
	{
		return false;
	} 
	
	for(int i = 0; i < n; i++ )
	{
		if(visited[i] == false)
		{//DFS后还有节点没被访问过,连通图都不是 
			return false;
		}
	}
	return true;
} 

============================================================================================================================================================================================================================================================================

全部代码:
#include<iostream>
#include<list> 
#include<algorithm>  //for min
#define NIL -1

using namespace std;
class Graph
{
	int n;		//图中节点个数
	list<int>* adj;   //邻接表
	void APUtil(int v, bool visited[], int depth[], int low[],
			int parent[], int curDepth, bool AP[]); 
	bool isBCUtil(int v, bool visited[], int depth[], int low[],
			int parent[], int curDepth);
public:
	Graph(int _n);
	void addEdge(int v,int w);
	void AP();			//求出连通图中的关节点 
	bool isBC(); 
};

Graph::Graph(int _n)
{
	n = _n;
	adj = new list<int>[_n];
}
void Graph::addEdge(int v,int w)
{
	adj[v].push_back(w);
	adj[w].push_back(v);
}
void Graph::APUtil(int u,bool visited[],int depth[],int low[],
			int parent[],int curDepth,bool AP[]) 
{
	visited[u] = true;				//已访问 
	int children = 0;				//孩子数 
	
	// 初始化u的深度和能到达的深度最小的祖先 
	depth[u] = low[u] = curDepth;	
	
	list<int>::iterator it;
	for( it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		
		if( !visited[v])		//v没被访问过
		{
			parent[v] = u;		//设置v的双亲 
			children++;			//u的子女+1
			APUtil(v, visited, depth, low, parent, curDepth + 1, AP); 
			
			//访问完v后回溯到这里,检查下u的子孙里有没有那个能
			//有绕过u能到达的深度最小的祖先,有的话u从子孙那边 
			//到那个祖先 ,更新low[u] 
			low[u] = min(low[u], low[v]); 
			
			//判断:
			//1、如果u是根结点(整颗生成树的根,其双亲为-1,即NIL) 
			//如果children >  1,则u是关节点
			if( parent[u] == NIL && children > 1)
			{
				AP[u] = true; 
			} 
			
			//2、不是根结点,但是有子女v无法绕过它u到它u的祖先w
			//u是关节点,此时low[v] >= depth[u] 
			if( parent[u] != NIL && low[v] >= depth[u]) 
			{
				AP[u] = true;
			}
		}
		else if( v != parent[u])			 
		{//v访问过了,说明v是当前节点u的祖先,v找到了一条边(回边到v的祖先),
		//这里排除下v是u的双亲(因为v是u的双亲的话处理的就不是回边了,这条边在DFS树)
			low[u] = min(low[u], depth[v]);			//更新low[u]; 
		} 
	} 
}
void Graph::AP()
{
	bool *visited = new bool[n];
	int *parent = new int[n];
	int *low = new int[n];
	int *depth = new int[n];
	bool *AP = new bool[n];
	
	for(int i = 0; i < n; i++)
	{
		AP[i] = false;
		parent[i] = NIL;
		visited[i] = false;
	} 
	for(int i = 0; i < n; i++)
	{
		if(!visited[i])
		{
			APUtil(i,visited,depth,low,parent,0,AP);
		}
	}
	
	for(int i = 0; i < n; i++)
	{
		if(AP[i] == true)
		{
			cout<<i<<"  ";
		}
	}
	cout<<endl;
} 
bool Graph::isBCUtil(int u,bool visited[],int depth[],int low[],
			int parent[],int curDepth) 
{
	visited[u] = true;				//已访问 
	int children = 0;				//孩子数 
	
	// 初始化u的深度和能到达的深度最小的祖先 
	depth[u] = low[u] = curDepth;	
	
	list<int>::iterator it;
	for( it = adj[u].begin(); it != adj[u].end(); it++)
	{
		int v = *it;
		
		if( !visited[v])		//v没被访问过
		{
			parent[v] = u;		//设置v的双亲 
			children++;			//u的子女+1
		
			if( isBCUtil(v, visited, depth, low, parent, curDepth + 1) == true) 
			{
				return true;
			}
			//访问完v后回溯到这里,检查下u的子孙里有没有那个能
			//有绕过u能到达的深度最小的祖先,有的话u从子孙那边 
			//到那个祖先 ,更新low[u] 
			low[u] = min(low[u], low[v]); 
			
			//判断:
			//1、如果u是根结点(整颗生成树的根,其双亲为-1,即NIL) 
			//如果children >  1,则u是关节点
			if( parent[u] == NIL && children > 1)
			{
				return true;
			} 
			
			//2、不是根结点,但是有子女v无法绕过它u到它u的祖先w
			//u是关节点,此时low[v] >= depth[u] 
			if( parent[u] != NIL && low[v] >= depth[u]) 
			{
				return true;
			}
		}
		else if( v != parent[u])			 
		{//v访问过了,说明v是当前节点u的祖先,v找到了一条边(回边到v的祖先),
		//这里排除下v是u的双亲(因为v是u的双亲的话处理的就不是回边了,这条边在DFS树)
			low[u] = min(low[u], depth[v]);			//更新low[u]; 
		} 
	} 
	return false;
}
bool Graph::isBC()
{
	bool *visited = new bool[n];
	int *parent = new int[n];
	int *low = new int[n];
	int *depth = new int[n];
	
	for(int i = 0; i < n; i++)
	{
		parent[i] = NIL;
		visited[i] = false;
	} 
	
	//isBCUtil返回图中有没有关节点 
	if( isBCUtil(0,visited,depth,low,parent,0) == true)
	{
		return false;
	} 
	
	for(int i = 0; i < n; i++ )
	{
		if(visited[i] == false)
		{//DFS后还有节点没被访问过,连通图都不是 
			return false;
		}
	}
	return true;
} 
int main()
{
	cout<<"输入图的关节点有:"<<endl;
	Graph g1(15);
	g1.addEdge(0,1);
	g1.addEdge(1,2);
	g1.addEdge(1,6);
	g1.addEdge(2,3);
	g1.addEdge(2,4);
	g1.addEdge(3,4);
	g1.addEdge(4,5);
	g1.addEdge(5,6);
	g1.addEdge(6,7);
	g1.addEdge(1,8);
	g1.addEdge(0,9);
	g1.addEdge(9,10);
	g1.addEdge(10,11);
	g1.addEdge(10,13);
	g1.addEdge(11,12);
	g1.addEdge(12,13);
	g1.AP();
	cout<<"g1是不是双连通图:"<<endl;
	cout<<(g1.isBC() ? "是":"不是")<<endl;
	
	cout<<"g2是不是双连通图:"<<endl; 
	Graph g2(2);
	g2.addEdge(0,1);
	cout<<(g2.isBC() ? "是":"不是")<<endl; 
	
	cout<<"g3是不是双连通图:"<<endl; 
	Graph g3(4);
	g3.addEdge(0,1);
	g3.addEdge(1,2);
	g3.addEdge(1,3); 
	cout<<(g3.isBC() ? "是":"不是")<<endl; 
	
	cout<<"g4是不是双连通图:"<<endl;
	Graph g4(5); 
    g4.addEdge(1, 0); 
    g4.addEdge(0, 2); 
    g4.addEdge(2, 1); 
    g4.addEdge(0, 3); 
    g4.addEdge(3, 4); 
    cout<<(g4.isBC() ? "是":"不是")<<endl; 
  
  	cout<<"g5是不是双连通图:"<<endl;
    Graph g5(3); 
    g5.addEdge(0, 1); 
    g5.addEdge(1, 2); 
    g5.addEdge(2, 0); 
    cout<<(g5.isBC() ? "是":"不是")<<endl; 
	return 0; 
}

在这里插入图片描述

  • 17
    点赞
  • 62
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值