一、定义:
关节点(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]
为顶点u
在DFS
树中的深度
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
)
然后就可以判断了,假设u
是v
的双亲,如果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]
最早发现的时间的来记录当前顶点u
在DFS
树在被发现的次序,而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;
}