重连通分量 (Biconnected Component)
在无向连通图G中,当且仅当删去G中的顶点 v及所有依附于v的所有边后,可将图分割成两个或两个以上的连通分量,则称顶点v为关节点。
没有关节点的连通图叫做重连通图。在重连通图上, 任何一对顶点之间至少存在有两条路径, 在删去某个顶点及与该顶点相关联的边时, 也不破坏图的连通性。
一个连通图G如果不是重连通图,那么它可以包括几个重连通分量。在一个无向连通图G中, 重连通分量可以利用深度优先生成树找到。
算法介绍:
dfn 顶点的深度优先数,标明进行深度优先搜索时各顶点访问的次序。如果在深度优先生成树中,顶点 u 是顶点 v 的祖先, 则有dfn[u]<dfn[v] 。
深度优先生成树的根是关节点的充要条件是它至少有两个子女。
其它顶点 u 是关节点的充要条件是它至少有一个子女 w, 从 w 出发, 不能通过 w、w 的子孙及一条回边所组成的路径到达 u 的祖先。
在图G的每一个顶点上定义一个low值,low[u]是从u或u的子孙出发通过回边可以到达的最低深度优先数。
low[u] = min{ dfn[u],
min{ low[w] | w 是 u 的一个子女 },
min{ dfn[x] | (u, x) 是一条回边 } }
u 是关节点的充要条件是:
u 是具有两个以上子女的生成树的根
u 不是根,但它有一个子女 w,使得
low[w]≥ dfn[u]
这时 w 及其子孙不存在指向顶点u的祖先的回边。
计算dfn与low的算法 (1)
Code
void Graph::DfnLow ( const int x ) { //公有函数:从顶点x开始深度优先搜索
int num = 1; // num是访问计数器
dfn = new int[NumVertices];
low = new int[NumVertices]; //dfn是深度优先数, low是最小祖先访问顺序号
for ( int i = 0; i < NumVertices; i++ ) {
dfn[i] = low[i] = 0; } //给予访问计数器num及dfn[u], low[u]初值
DfnLow ( x, -1 ); //从根x开始
delete [ ] dfn;
delete [ ] low;
}
计算dfn与low的算法 (2)
Code
void Graph::DfnLow ( const int u, const int v ) {
//私有函数:从顶点 u 开始深度优先搜索计算dfn和low。在产生的生成树中 v 是 u 的双亲。
dfn[u] = low[u] = num++;
int w = GetFirstNeighbor (u);
while ( w != -1 ) { //对u所有邻接顶点w循环
if ( dfn[w] == 0 ) { //未访问过, w是u的孩子
DfnLow ( w, u ); //从w递归深度优先搜索
low[u] = min2 ( low[u], low[w] ); //子女w的low[w]先求出, 再求low[u]
}
else if ( w != v ) //w访问过且w不是v,是回边
low[u] = min2 ( low[u], dfn[w] ); //根据回边另一顶点w调整low[u]
w = GetNextNeighbor (u, w); //找顶点u在w后面的下一个邻接顶点
}
}
在算法DfnLow增加一些语句, 可把连通图的边划分到各重连通分量中。
首先, 根据 DfnLow (w, u)的返回, 计算low[w]。
如果low[w]>=dfn[u],则开始计算新的重连通分量。
在算法中利用一个栈, 在遇到一条边时保存它。
可在函数Biconnected中就能输出一个重连通分量的所有的边。
当 n > 1 时输出重连通分量(1)
Code
void Graph::Biconnected ( ) { //公有函数:从顶点0开始深度优先搜索
int num = 1; //访问计数器num
dfn = new int[NumVertices]; //dfn是深度优先数
low = new int[NumVertices]; //low是最小祖先号
for ( int i = 0; i < NumVertices; i++ ) {
dfn[i] = low[i] = 0;
}
DfnLow ( 0, -1 ); //从顶点 0 开始
delete [ ] dfn;
delete [ ] low;
}
当 n > 1 时输出重连通分量(2)
void Graph::Biconnected ( const int u, const int v ) {
//私有函数:计算dfn与low, 根据其重连通分量输出Graph的边。
//在产生的生成树中, v 是 u 的双亲结点, S 是一个初始为空的栈,
//应声明为图的数据成员。
Code
int x, y, w;
dfn[u] = low[u] = num++;
w = GetFirstNeighbor (u); //找顶点u的第一个邻接顶点w
while ( w != - 1 ) {
if ( v != w && dfn[w] < dfn[u] )
S.Push ( (u,w) ); //w不是u的双亲且w先于u被访问, (u,w)进栈
if ( dfn[w] == 0 ) { //未访问过, w是u的孩子
Biconnected (w, u); //从w递归深度优先访问
low[u] = min2 ( low[u], low[w] ); //根据先求出的low[w], 调整low[u]
if ( low[w] >= dfn[u] ) { //无回边, 原来的重连通分量结束
cout << “新重连通分量: ” << endl;
do {
(x, y) = S.Pop ( );
cout << x << "," << y << endl;
} while ( (x, y) 与 (u, w) 不是同一条边 );
} //输出该重连通分量的各边
}
else if ( w != v ) //有回边,计算
low[u] = min2 ( low[u], dfn[w] ); //根据回边另一顶点w调整low[u]
w = GetNextNeighbor (u, w); //找顶点u的邻接顶点w的下一个邻接顶点
}
}
算法 Biconnected 的时间代价是 O(n+e)。其中 n 是该连通图的顶点数,e 是该连通图的边数。
此算法的前提条件是连通图中至少有两个顶点,因为正好有一个顶点的图连一条边也没有。
Practice
PKU 3177
January 2006 Problem 'rpaths' Analysis
by Bruce Merry
The problem can be restated as requiring that one adds the minimum number of edges to make the graph biconnected. The first step is to identify the existing biconnected components; refer to your favourite algorithms textbook to see what a biconnected component is and how to identify one; be aware though that there are two variants of bi-connectivity, depending on whether separate routes must be vertex-disjoint or edge-disjoint; in this case they must be edge-disjoint, so biconnected components are separated by articulation edges (vertex-disjoint biconnectivity is more common and probably what your favourite textbook will discuss, but the algorithms involved are very similar).
We will never need to add edges within a biconnected component, so for the purposes of the problem we can collapse each biconnected component to a single vertex. This will leave the graph as a tree. Each leaf will need a new edge added (since there is currently only one road to its parent), so at least ceil(leaves / 2) edges must be added. It is also possible to show that this number is sufficient (hint: joining the left-most and right-most leaf and re-collapsing the newly created biconnected component will almost always reduce the number of leaves by 2). So it is sufficient to count the leaves; you don't actually need to work out which new paths to add.
Code
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
const int MAXV = 50000;
const int MAXE = 100000;
struct Edge
{
int ID, next;
};
int ID[MAXV];
int Low[MAXV];
int P[MAXV];
int Z[MAXV];
vector<Edge> E[MAXV];
stack<int> S;
int N, R;
int Count;
int DFS(int x, int flag)
{
S.push(x);
Low[x] = ID[x] = Count++;
int leaves = 0;
for (int i = 0; i < E[x].size(); i++)
{
if (E[x][i].ID == flag) continue;
int next = E[x][i].next;
if (ID[next] == -1)
{
P[next] = x;
leaves += DFS(next, E[x][i].ID);
}
if (ID[next] < Low[x]) Low[x] = ID[next];
if (Low[next] < Low[x]) Low[x] = Low[next];
}
if (Low[x] == ID[x])
{
while (S.top() != x)
{
Z[S.top()] = ID[x];
S.pop();
}
Z[x] = ID[x];
S.pop();
if (leaves == 0) return 1;
}
return leaves;
}
int Solve()
{
int leaves = DFS(0, -1);
int root_child = 0;
for (int i = 1; i < N; i++)
{
int p = P[i];
if (Z[p] == 0 && Z[i] == ID[i])
root_child++;
}
if (root_child == 0) return 0;
if (root_child == 1) leaves++;
return (leaves+1)/2;
}
int main()
{
while (scanf("%d%d", &N, &R) == 2)
{
Count = 0;
memset(ID, -1, sizeof(ID));
while (!S.empty()) S.pop();
for (int i = 0; i < N; i++)
E[i].clear();
P[0] = -1;
for (int i = 0; i < R; i++)
{
int x, y;
scanf("%d%d", &x, &y);
x--,y--;
Edge temp;
temp.ID = i, temp.next = y;
E[x].push_back(temp);
temp.next = x;
E[y].push_back(temp);
}
printf("%d\n", Solve());
}
return 0;
}