无向图的连通分支(连通子图): 判断一个无向图是否连通,如果进行dfs或者bfs之后,还有未访问到的顶点,说明不是连通图,否则连通。
求解无向图的所有连通分支: 只需要重复调用dfs或者bfs 就可以解决:遍历顶点,如果v 未访问,则对其进行dfs, 然后标记访问。过程如下:
void dfs(int v){
node_pointer w;
visited[v] = TRUE;
for(w = graph[v]; w; w = w->link) {
if(!visited[w->vertex])
dfs(w->vertex);
}
}
void connect(){
int i;
for(i = 0; i < n; i++)
if(!visited[i]) {
dfs(i);
}
}
关节点(割点): 是图中一个顶点v, 如果删除它以及它关联的边后,得到的新图至少包含两个连通分支。
双连通图: 没有关节点的连通图。
连通无向图的双连通分支(双连通子图,块) : 是图G中一个最大双连通子图。
利用深度优先搜索dfs 可以求解双连通分支,因为dfs过程中,必定要经过关节点,并生成一棵深度优先搜索树。 而图G的连通子图必然是深搜树的一部分。
这张图很难看。 它有4个关节点:1,3,4,7, 将 图分为6个双连通分支。
如果以顶点3开始深搜,得到如下一棵树:
3是树根, 红色标号是 深度搜索访问节点的顺序, 红色边是图中深度搜索没有访问到的边(因为有的顶点可以多个边到达,深搜只要通过一个边到达顶点,就不再访问该顶点了),称作非树边,也就是树中没有的。 黑色的边是树边。
如果两个顶点u,v ,其中u是v的祖先或者v是u的祖先,那么非树边(u,v)叫做回退边。在深搜树中,所有的非树边都是回退边。 无向图的深搜树是一棵开放树,如果在其中添加一条回退边,就会形成环,该环路或扩大连通分量的范围,或者导致新的连通分量产生。
通过这个过程,可以发现一条规律:当v是树根,如果它有2个或者更多儿子,那么它是一个关节点。
当v不是树根,当且仅当它有至少一个儿子w, 且从w出发,不能通过w的后代顶点组成的路径和一条回退边到底u 的任意一个祖先顶点,此时v 是一个关节点。 其道理很明显,如果树根包含多个儿子,那么把根节点去掉,整棵树自然被分成多个不相干的部分,图也就断开了。如果v是非根顶点,如果其子树中的节点均没有指向v祖先的回边,那么去掉v以后,将会把v及其子树与图的其他部分分割开来,v自然是关节点。
例如顶点5,它的儿子只有6,而6 能到达的最低层顶点是5(通过 6->7->5), 无法访问到5的祖先顶点,因此5是一个关节点。
基于这样的规律,我们给每个顶点定义一个low值,low(u) 表示从u出发,经过一条其后代组成的路径和回退边,所能到达的最小深度的顶点的编号。( 如果这个编号大于等于u的编号,就说明它的后代无法到达比u深度更浅的顶点,即无法到达u的祖先,那么u就是个关节点)
low(u) = min{ dfn(u), min{ low(w) | w是u的儿子}, min{dfn(w), | (u,w) 是一条回退边} }
dfn(u) 是深搜过程中对顶点的编号值。
计算过程如下:
void dfnlow(int u, int v) {
node_pointer ptr;
int w;
dfn[u] = low[u] = num++;
for(ptr = graph[u]; ptr; ptr = ptr->link) {
w = ptr->vertex;
if(dfn[w] < 0) {
dfnlow(w, u);
low[u] = MIN(low[u], low[w]);
} else if( w != v)
low[u] = MIN(low[u], dfn[w]);
}
}
因此,我们在深搜过程中计算出 dfn 值和 low 值,如果发现 u有一个儿子w ,使得 low(w) >= dfn(u), 那么u就是关节点。
求解双连通分量的过程,可以通过深搜完成。 在搜索过程中,如果遇到一个新的边,则压栈,直到找到一个关节点,由于深搜是递归的,在找到一个关节点的同时,必定已经访问完了其子孙节点和其子树的边(包括回退边),而且这些边都在栈中,此时弹出栈中的边直到遇到关节点所在的边即是双连通分支包括的边。
完整代码:
#include <stdio.h>
#define MAX_VERTICES 50
#define true 1
#define false 0
#define MIN(x,y) ((x) < (y) ? (x) : (y))
typedef struct node *node_pointer;
struct node {
int vertex;
struct node *link;
};
node_pointer graph[MAX_VERTICES];
int n = 0;
int dfn[MAX_VERTICES];
int low[MAX_VERTICES];
typedef struct {
int v;
int w;
}edge;
edge edges[100];
int top = 0;
int num = 0;
void printG() {
int i;
node_pointer e;
for(i=0;i<=n;i++) {
printf("[%d]",i);
for(e=graph[i];e;e=e->link)
printf(" (%d)->",e->vertex);
printf("\n");
}
}
void printDfnLow() {
int i = 0;
while(i<=n) {
printf("[%d]: dfn:%d low:%d\n", i, dfn[i], low[i]);
++i;
}
}
void addEdge(int v, int w) {
node_pointer e = (node_pointer)malloc(sizeof(struct node));
e->vertex = w;
e->link = graph[v];
graph[v] = e;
}
//无向图中一条边在邻接表中对应两个节点,1->2,2->1
void addREdge(int v,int w){
addEdge(v,w);
addEdge(w,v);
}
void init() {
int i = 0;
n = 9; //0 to n
while(i<=n) {
graph[i] = 0;
dfn[i] = low[i] = -1;
i++;
}
num = 0;
addREdge(3,5);
addREdge(5,7);
addREdge(5,6);
addREdge(6,7);
addREdge(7,9);
addREdge(7,8);
addREdge(0,1);
addREdge(1,2);
addREdge(1,3);
addREdge(2,4);
addREdge(4,3);
}
void dfnlow(int u, int v) {
node_pointer ptr;
int w;
dfn[u] = low[u] = num++;
for(ptr = graph[u]; ptr; ptr = ptr->link) {
w = ptr->vertex;
if(dfn[w] < 0) {
dfnlow(w, u);
low[u] = MIN(low[u], low[w]);
} else if( w != v)
low[u] = MIN(low[u], dfn[w]);
}
}
void bicon(int u, int v) {
node_pointer ptr;
int w;
edge e;
dfn[u] = low[u] = num++;
for(ptr = graph[u]; ptr; ptr = ptr->link) {
w = ptr->vertex;
if(v!=w && dfn[w] < dfn[u]) { //v!=w to avoid 1->2 2->1 in undirected graph
// dfn[w] < dfn[u] to avoid visited vertex who is decendant of u
edges[top].v = u; // 新边压栈,v!=w是防止重复计算无向图中同一条边
//dfn[w]<dfn[u] 是防止重复计算回退边,因为dfs过程中,
//遇到的顶点只有两种情况,dfn[w]=-1新点, dfn[w]<dfn[u]
//u,w 是回退边。二者的共同点是 dfn[w] < dfn[u],这两种
//边包括了G的所有边,因此对其他边的访问是重复的。
edges[top].w = w;
++top;
if(dfn[w]< 0) { //如果是新顶点(未访问过)
bicon(w,u); //递归计算
low[u] = MIN(low[u], low[w]);// 更新当前u的low
if(low[w] >= dfn[u]) { //如果发现u的孩子w 满足条件,说明u是关节点
printf("New biconnected component:\n");
do{
e = edges[--top]; //此时栈中是上面的bicon压入的访问过的边,
//即该关节点下的子树边和回退边
printf("<%d,%d>", e.v, e.w);
}while( !(e.v == u && e.w == w));
printf("\n");
}
} else if (w!=v){
low[u] = MIN(low[u], dfn[w]);
}
}
}
}
int main(){
init();
printG();
//dfnlow(3,-1);
bicon(3,-1);
printDfnLow();
getchar();
}
注意在无向图深搜树中,只有两种边:树边(u->v u是v的父亲,v未访问)和回退边(u->v, v是u的祖先,v访问过)。 且无向图的边在邻接表中其实是"双向"的。因此我们要通过一些条件来只使用树边和回退边。
因此对于边u,v dfn[u] < dfn[v] && v 访问过 (即回退边的反向) 或者 dfn[u] > dfn[v], v 是u的父亲(树边的反向),这两种都是已经访问过的边,不需要重复访问。
个人理解:
求双连通分量,关键在于求关键点(即割点),每一个割点就像刀一样的,可以把一个无向图划分成双连通分量,在割点的两旁都是双连通分量,这点我们可以用递归+堆栈来实现,而求割点又是基于两个事实(吴文虎的图论书上有)
对一个给定无向图,实施dfs搜索得到的所有顶点的dfn值及dfs树(或森林)后
1:如果U不是根,U成为割点当且仅当存在U的某一个儿子顶点S,从S或S的后代点到U的祖先点之间不存在后向边(即LOW(S)>=dfn(U)时,U为割点)
2:如果U是根,则U称为割点当且仅当它有不止一个儿子节点