无向图的连通分支(连通子图): 判断一个无向图是否连通,如果进行dfs或者bfs之后,还有未访问到的顶点,说明不是连通图,否则连通。
求解无向图的所有连通分支: 只需要重复调用dfs或者bfs 就可以解决:遍历顶点,如果v 未访问,则对其进行dfs, 然后标记访问。
关节点(割点): 是图中一个顶点v, 如果删除它以及它关联的边后,得到的新图至少包含两个连通分支。
双连通图: 没有关节点的连通图。
连通无向图的双连通分支(双连通子图,块) : 是图G中一个最大双连通子图。
利用深度优先搜索dfs 可以求解双连通分支,因为dfs过程中,必定要经过关节点,并生成一棵深度优先搜索树。 而图G的连通子图必然是深搜树的一部分。
这张图有4个关节点:1,3,4,7, 将 图分为6个双连通分支。(其实5也是割点)
如果以顶点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) 是深搜过程中对顶点的编号值。
因此,我们在深搜过程中计算出 dfn 值和 low 值,如果发现 u有一个儿子w ,使得 low(w) >= dfn(u), 那么u就是关节点。
求解双连通分量的过程,可以通过深搜完成。 在搜索过程中,如果遇到一个新的边,则压栈,直到找到一个关节点,由于深搜是递归的,在找到一个关节点的同时,必定已经访问完了其子孙节点和其子树的边(包括回退边),而且这些边都在栈中,此时弹出栈中的边直到遇到关节点所在的边即是双连通分支包括的边。
注意在无向图深搜树中,只有两种边:树边(u->v u是v的父亲,v未访问)和回退边(u->v, v是u的祖先,v访问过)。 且无向图的边在邻接表中其实是"双向"的。因此我们要通过一些条件来只使用树边和回退边。
因此对于边u,v dfn[u] < dfn[v] && v 访问过 (即回退边的反向) 或者 dfn[u] > dfn[v], v 是u的父亲(树边的反向),这两种都是已经访问过的边,不需要重复访问。
我对回退边的理解是,它圈定了一个由关节点分割的连通子图的范围。
因为当遇到一个u的子孙 w, 使 low[w] >= dfn[u], 就说明 w 以及w 的子孙都无法访问到 u的祖先,那么去掉u 后,w 以及其子树就被和图的其他部分分割开来,形成了连通子图。这里的割点就是low[w] 能到达的最底层节点,也就是深搜过程中最靠近这个连通子图的关节点。
那么下界其实就是w. 因为设 v是w的孩子, low[v] < dfn[w] , 那么low[w] 肯定 等于 low[v], 从而 low[w] 小于本来的 low[w], 和前提矛盾。如果 low[v] >= dfn[w] , 则 w是一个关节点,那么w自然是一个界限,将刚才的连通子图和新生成的连通子图分开来,也就是前一个连通图的下界。
记忆方法:
//(2)求割边。割边两端点分别在两个连通分支之中,故使用<
//(3)求去除割点u后形成的连通分支数。若u为割点,记subnets[u]为u的子节点数,则去掉u后,图被分成subnets[u]+1个部分(每个子节点的部分和u的祖先的部分),若u为dfs树的根,则分成subnets[u]个部分(根节点没有祖先)。
模板 POJ 1523http://poj.org/problem?id=1523 求割点&去掉割点和连通分量
/*
构建一棵dfs树,序列dfn[i]为深度优先数,表示dfs时访问i节点的序号,low[i]表示从i节点出发能访问到的最小的深度优先数。
当且仅当节点u满足如下两个条件之一时,u为割点:
1.u为dfs树的根,且u至少有两个子节点。
2.u不是dfs树的根,至少存在一个节点v是u的子节点,且low[v]>=dfn[u]。
若u为割点,记subnets[u]为u的子节点数,则去掉u后,图被分成subnets[u]+1个部分(每个子节点的部分和u的祖先的部分),若u为dfs树的根,则分成subnets[u]个部分(根节点没有祖先)。
以上全部算法均在dfs过程中完成。
*/
#define N 1010
struct edge{
int v;
int next;
}e[N*5];
int ecnt;
int head[N];
void init(){
ecnt = 0;
memset(head,-1,sizeof(head));
}
void add(int u,int v){
e[ecnt].v = v;
e[ecnt].next = head[u];
head[u] = ecnt++;
e[ecnt].v = u;
e[ecnt].next = head[v];
head[v] = ecnt++;
}
int low[N],dfn[N];
int cut[N];
int n,m;
int t;
int ans;
int sub[N];
//tarjan求无向图双连通图
void tarjan(int u,int fa){
low[u] = dfn[u] = ++t;
int i;
for(i=head[u];i!=-1;i=e[i].next){
int v = e[i].v;
if(v == fa)continue;
if(!dfn[v]){
tarjan(v,u);
low[u] = min(low[u],low[v]);
if(dfn[u]<=low[v]){//u是割点
if(u!=1)sub[u]++;//假定1为根结点
else ans++;
}
} else low[u] = min(low[u],dfn[v]);//返祖边
}
}
int main(){
int a,b;
int ca=1;
while(1){
scanf("%d",&a);
if(!a)break;
init();
memset(dfn,0,sizeof(dfn));
scanf("%d",&b);
add(a,b);
int node = 0;
node = max(a,b);
while(scanf("%d",&a) && a){
scanf("%d",&b);
add(a,b);
node = max(node,max(a,b));
}
int i,j;
ans=t=0;
memset(sub,0,sizeof(sub));
tarjan(1,1);//假定1为根结点
if(ca>1)puts("");
printf("Network #%d\n",ca++);cout<<ans<<endl;
if(ans>1)sub[1] = ans-1;//根至少有两个儿子才算是割点
bool ok=0;
for(i=1;i<=node;i++){
if(sub[i]){
ok = 1;
printf(" SPF node %d leaves %d subnets\n",i,sub[i]+1);
}
}
if(!ok)puts(" No SPF nodes");
}
return 0;
}
模板:
#define N 1005
vector<int> v[N];
vector<int> lin[N];//记录连通块
int dfn[N],low[N];
bool instack[N],iscut[N];
int num_rt;//特判根结点
int cnt;
int step;
stack<int> sta;
void init(int n){
for(int i=0;i<=n;i++){
instack[i] = 0;
iscut[i] = 0;
dfn[i] = 0;
low[i] = 0;
}
step = 0;
cnt = 0;
while(!sta.empty())sta.pop();
}
void tarjan(int u,int fa){
low[u] = dfn[u] = ++step;
sta.push(u);
instack[u] = 1;
int i;
for(i=0;i<v[u].size();i++){
int to = v[u][i];
if(to==fa)continue;
if(!dfn[to]){
tarjan(to,u);
low[u] = min(low[u],low[to]);
if(low[to]>=dfn[u]){//u是割点
if(fa==-1)num_rt++;//特判根节点
else iscut[u] = 1;
while(1){
int tmp = sta.top();
sta.pop();
iscut[tmp] = 0;
instack[tmp] = 0;
lin[cnt].push_back(tmp);
if(tmp==to)break;
}
lin[cnt].push_back(u);
cnt++;
}
} else if(instack[to]) low[u] = min(low[u],dfn[to]);
}
}