[点连通度与边连通度]
在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。一个图的点连通度的定义为,最小割点集合中的结点数。
类似的,如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合。一个图的边连通度的定义为,最小割边集合中的边数。
[割点]
一个图有割点,当且仅当这个图的点连通度为1,则割点集合的唯一元素被称为割点(cut point),又叫关节点(articulation point)。切除该点图的连通分量数+1.也就是说原有的一个连通分量经过操作成为两个连通分量,原图不再连通。
(如果有多个割点,则每个割点可以自成一格最小割点集合)
[桥]
一个图有桥,当且仅当这个图的边连通度为1,则割边集合的唯一元素被称为桥(bridge),又叫关节边(articulation edge)。删掉该边图的连通分量数+1,原图不再连通。
[双连通图、割点与桥]
如果一个无向连通图的点连通度大于1,则称该图是点双连通的(point biconnected),简称双连通或重连通。简单来说就是删除任何一结点,不影响图的连通性。图没有割点。
如果一个无向连通图的边连通度大于1,则称该图是边双连通的(edge biconnected),简称双连通或重连通。简单来说就是删除任何一边,不影响图的连通性。图没有桥。
点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的。
[双连通分支]
在图G的所有子图G'中,如果G'是双连通的,则称G'为双连通子图。如果一个双连通子图G'它不是任何一个双连通子图的真子集,则G'为极大双连通子图。双连通分支(biconnected component),或重连通分支,就是图的极大双连通子图。特殊的,点双连通分支又叫做块。
求解方法:
由dfs构造的一棵搜索树中:
1.割点:点u为割点,满足以下两个条件之一
1>.u为树的root && u的孩子≥2个
2>.u不为树的root && 满足DFN[u]<=LOW[v];
其中DFN为dfs过程中节点的编号。LOW为该点引出的边中最多直接或间接连接的最早的祖先。
1>.条件很形象.
2>.当出现DFN[u]<=LOW[v]时,也就是说v点最多达到u,不可能再向上达到u的祖先节点。显然这个时候将u点删除,v所在的一团和u点祖先的一团分割为两个连通分量。所以u为割点。
2.桥:边(u,v)为桥,满足该条件:DFN[u]<LOW[v]。这里照上面的讲解也很形象。当DFN[u]==LOW[v]时,当u->v dfs递归,存在一条v->u的回边,使得LOW[v]=DFN[u];故不为桥。
割和桥的两种判定就像上面的讲述:
void Tarjan( int u,int father )
{
Node *p=ptr[u];
DFN[u]=LOW[v]=++DEP;
while( p )
{
if( DFN[p->v]==0 )
{
Tarjan( p->v,u );
LOW[u]=min( LOW[u],LOW[v] );
//if DFN[u]<LOW[v] 则(u,v)为桥;
}
else if( p->v!=father )
LOW[u]=min( LOW[u],DFN[v] );
p=p->next;
}
/*
if( u is root )
u是割点 <=> u至少有两个孩子
else
u是割点 <=> DFN[u]<=LOW[v]
*/
}
void getV_B()
{
Tarjan(1,0);
int rootSon=0,cutPoint=0;
for( i=2;i<=N;i++ )
{
if( father[i]==1 )
rootSon++;
else if( DFN[father[i]<=LOW[i]] )
cutPoint++;//i为割点
}
if( rootSon>=2 )
cntPoint++;//root点1为割点
for( i=1;i<=N;i++ )
if( DFN[father[i]]<LOW[i] )
//( father[i],i )为桥;
}
3.边双连通分支:
也就是将桥删除后整个图分成的连通块就是边双连通分支。
4.点双连通分支:
对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或反向边,就把这条边加入栈中。如果遇到某时满足dfn(u)<=low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
void Tarjan(int u, int father){
int i,j,k;
low[u] = dfn[u] = nTime ++;
for( i = 0;i < G[u].size() ;i ++ )
{
int v = G[u][i];
if( ! dfn[v])
{ //v没有访问过//树边要入栈
Edges.push_back(Edge2(u,v));
Tarjan(v,u);
low[u] = min(low[u],low[v]);
Edge2 tmp(0,0);
if(dfn[u] <= low[v])
{ //从一条边往下走,走完后发现自己是割点,则栈中的边一定全是和自己在一个双连通分量里面//根节点总是和其下的某些点在同一个双连通分量里面
cout << "Block No: " << ++ nBlockNo << endl;
do
{
tmp = Edges.back();
Edges.pop_back ();
cout << tmp.u << "," <<tmp.v << endl;
}while ( !(tmp.u == u && tmp.v == v) );
}
} // 对应if( ! dfn[v]) {
else if( v != father )
{
//u连到父节点的回边不考虑
low[u] = min(low[u],dfn[v]);
if( dfn[u] > dfn[v])//子孙连接到祖先的回边要入栈,但是子孙连接到自己的边,此处肯定已经入过栈了,不能再入栈
Edges.push_back(Edge2(u,v));
}
} //对应 for( i = 0;i < G[u].size() ;i ++ ) {
}
一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。
统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <stack>
using namespace std;
const int MAXN = 5011;
const int MAXM = 20011;
int now[MAXN], dfn[MAXN], low[MAXN], d[MAXN], color[MAXN];
int pre[MAXM], son[MAXM], vis[MAXM];
stack<int> st;
int n, edgeCnt, dfnCnt, ebccCnt;
void addEdge(int u, int v)
{
vis[edgeCnt] = 0;
pre[edgeCnt] = now[u]; son[edgeCnt] = v; now[u] = edgeCnt ++;
}
void dfs(int u, int fa)
{
dfn[u] = low[u] = ++ dfnCnt;
st.push(u);
for (int p = now[u]; p != -1; p = pre[p])
{
if (vis[p]) continue;
vis[p] = vis[p ^ 1] = true;
int v = son[p];
if (!dfn[v])
{
dfs(v, u);
low[u] = min(low[u], low[v]);
}
else
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u])
{
++ ebccCnt;
while (st.top() != u)
{
color[st.top()] = ebccCnt;
st.pop();
}
color[u] = ebccCnt;
st.pop();
}
}
void init()
{
edgeCnt = dfnCnt = ebccCnt = 0;
memset(dfn, 0, sizeof(dfn));
memset(now, 255, sizeof(now));
memset(color, 255, sizeof(color));
int m;
scanf("%d%d", &n, &m);
for (int i = 0; i < m; ++ i)
{
int u, v;
scanf("%d%d", &u, &v);
addEdge(u, v);
addEdge(v, u);
}
}
void solve()
{
dfs(1, 0);
memset(d, 0, sizeof(d));
for (int i = 1; i <= n; ++ i)
{
for (int p = now[i]; p != -1; p = pre[p])
if (color[i] != color[son[p]])
d[color[i]] ++;
}
int leafCnt = 0;
for (int i = 1; i <= ebccCnt; ++ i)
if (d[i] == 1)
leafCnt ++;
printf("%d\n", (leafCnt + 1) / 2);
}
int main()
{
init();
solve();
return 0;
}