无向图割边割点算法
而当(u,v)为树边且low[v]>dfn[u]时,表示v节点只能通过该边(u,v)与u连通,那么(u,v)即为割边。
1 void dfs(int u) { 2 //记录dfs遍历次序 3 static int counter = 0; 4 5 //记录节点u的子树数 6 int children = 0; 7 8 ArcNode *p = graph[u].firstArc; 9 visit[u] = 1; 10 11 //初始化dfn与low 12 dfn[u] = low[u] = ++counter; 13 14 for(; p != NULL; p = p->next) { 15 int v = p->adjvex; 16 17 //节点v未被访问,则(u,v)为树边 18 if(!visit[v]) { 19 children++; 20 parent[v] = u; 21 dfs(v); 22 23 low[u] = min(low[u], low[v]); 24 25 //case (1) 26 if(parent[u] == NIL && children > 1) { 27 printf("articulation point: %d\n", u); 28 } 29 30 //case (2) 31 if(parent[u] != NIL && low[v] >= dfn[u]) { 32 printf("articulation point: %d\n", u); 33 } 34 35 //bridge 36 if(low[v] > dfn[u]) { 37 printf("bridge: %d %d\n", u, v); 38 } 39 } 40 41 //节点v已访问,则(u,v)为回边 42 else if(v != parent[u]) { 43 low[u] = min(low[u], dfn[v]); 44 } 45 } 46 }
边双联通分量算法
对于一个无向图的子图,当删除其中任意一个点后,不改变图内点的连通性,这样的子图叫做点的双连通子图。而当子图的边数达到最大时,叫做点的双连通分量。
直观的做法自然先用上周的算法求出所有桥,去掉所有桥之后再做DFS求出每一个连通子图。我们这周要介绍一种更"抽象"的算法,通过在Tarjan算法当中巧妙地用一个栈来统计出每一个组内的节点,其代码如下:
1 void dfs(int u) { 2 //记录dfs遍历次序 3 static int counter = 0; 4 5 //记录节点u的子树数 6 int children = 0; 7 8 ArcNode *p = graph[u].firstArc; 9 visit[u] = 1; 10 11 //初始化dfn与low 12 dfn[u] = low[u] = ++counter; 13 14 //将u加入栈 15 stack[++top] = u; 16 17 for(; p != NULL; p = p->next) { 18 int v = p->adjvex; 19 20 //节点v未被访问,则(u,v)为树边 21 if(!visit[v]) { 22 children++; 23 parent[v] = u; 24 dfs(v); 25 26 low[u] = min(low[u], low[v]); 27 if (low[v] > dfn[u]) { 28 printf("bridge: %d %d\n", u, v); // 该边是桥 29 bridgeCnt++; 30 } 31 } 32 33 //节点v已访问,则(u,v)为回边 34 else if(v != parent[u]) { 35 low[u] = min(low[u], dfn[v]); 36 } 37 } 38 39 if (low[u] == dfn[u]) 40 { 41 // 因为low[u] == dfn[u],对(parent[u],u)来说有dfn[u] > dfn[ parent[u] ],因此low[u] > dfn[ parent[u] ] 42 // 所以(parent[u],u)一定是一个桥,那么此时栈内在u之前入栈的点和u被该桥分割开 43 // 则u和之后入栈的节点属于同一个组 44 将从u到栈顶所有的元素标记为一个组,并弹出这些元素。 45 } 46 }
强连通分量
对于有向图上的2个点a,b,若存在一条从a到b的路径,也存在一条从b到a的路径,那么称a,b是强连通的。 对于有向图上的一个子图,若子图内任意点对(a,b)都满足强连通,则称该子图为强连通子图。 非强连通图有向图的极大强连通子图,称为强连通分量。 特别地,和任何一个点都不强连通的单个点也是一个强连通分量。
1 tarjan(u) 2 { 3 Dfn[u]=Low[u]=++Index // 为节点u设定次序编号和Low初值 4 Stack.push(u) // 将节点u压入栈中 5 for each (u, v) in E // 枚举每一条边 6 if (v is not visted) // 如果节点v未被访问过 7 tarjan(v) // 继续向下找 8 Low[u] = min(Low[u], Low[v]) 9 else if (v in Stack) // 如果节点v还在栈内(很重要,无向图没有这一步) 10 Low[u] = min(Low[u], Dfn[v]) 11 if (Dfn[u] == Low[u]) // 如果节点u是强连通分量的根 12 repeat 13 v = Stack.pop // 将v退栈,为该强连通分量中一个顶点 14 mark v // 标记v,同样通过栈来找连通分量 15 until (u == v) 16 }
scc + 缩点 + topo
点的双连通分量
对于一个无向图的子图,当删除其中任意一个点后,不改变图内点的连通性,这样的子图叫做点的双连通子图。而当子图的边数达到最大时,叫做点的双连通分量。
对于桥的两种情况,它分割个区域数刚好就等于割点数+1;而连通分量内的割点同样也是,每存在一个割点,点的双连通分量就增加一个。
点的双连通分量就等于割点数量加1。
每存在一个割点,就把一个区域一分为二,所以最后的结果也就是统计割点的数量就可以了。而对于分组具体情况,我们仍然采用栈来辅助我们记录
1 void dfs(int u) { 2 //记录dfs遍历次序 3 static int counter = 0; 4 5 //记录节点u的子树数 6 int children = 0; 7 8 ArcNode *p = graph[u].firstArc; 9 visit[u] = 1; 10 11 //初始化dfn与low 12 dfn[u] = low[u] = ++counter; 13 14 for(; p != NULL; p = p->next) { 15 int v = p->adjvex; 16 if(edge(u,v)已经被标记) continue; 17 18 //节点v未被访问,则(u,v)为树边 19 if(!visit[v]) { 20 children++; 21 parent[v] = u; 22 edgeStack[top++] = edge(u,v); // 将边入栈 23 dfs(v); 24 25 low[u] = min(low[u], low[v]); 26 27 //case (1) 28 if(parent[u] == NIL && children > 1) { 29 printf("articulation point: %d\n", u); 30 // mark edge 31 // 将边出栈,直到当前边出栈为止,这些边标记为同一个组 32 do { 33 nowEdge = edgeStack[top]; 34 top--; 35 // 标记nowEdge 36 } while (nowEdge != edge(u,v)) 37 } 38 39 //case (2) 40 if(parent[u] != NIL && low[v] >= dfn[u]) { 41 printf("articulation point: %d\n", u); 42 // mark edge 43 // 将边出栈,直到当前边出栈为止,这些边标记为同一个组 44 do { 45 nowEdge = edgeStack[top]; 46 top--; 47 // 标记nowEdge 48 } while (nowEdge != edge(u,v)) 49 } 50 51 } 52 53 //节点v已访问,则(u,v)为回边 54 else if(v != parent[u]) { 55 edgeStack[top++] = edge(u,v); 56 low[u] = min(low[u], dfn[v]); 57 } 58 } 59 }