轉自:http://blog.163.com/zjut_nizhenyang/blog/static/16957002920110385557289/
無向圖的連通分支(連通子圖): 判斷一個無向圖是否連通,如果進行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稱爲割點當且僅當它有不止一個兒子節點