一、割点、割边、双连通分支概念
挂接点(Articulation point)就是割点(Cut Vertex)
桥(Bridge)就是割边(Cut Edge)
割点:v为割点,则去掉v后,图的连通分支增加。
割边:v为割边,则去掉v后,图的连通分支增加。
割点形式化的定义:a是割点当且仅当存在两个点u,v使得u到v的每条路径都会经过a。(去掉a后,u到v没有路径)
边双连通分支:在连通分支中去掉一条边,连通分支仍连通。
点双连通分支:在连通分支中去掉一个点,连通分支仍连通。
我们这里说的是点双连通分支,因为由定义“任何两条边都在一个公共简单回路,且在一个双连通分支中没有割点,因此是点双连通分支。”。
割点应用场景:
给定一个计算机网络,如果有一个计算机v坏掉了,那么是否任何两个计算机都能够仍然连通?
遇到上述问题,是否能够转化成图论问题呢?
其实这个问题就是看坏掉的计算机是否是割点,如果是割点,则一定存在两个计算机u、v,u和v不连通。
双连通图定义:不存在割点。
双连通分支定义:他是极大双连通子图,就是如果G是双连通分支,则不存在G’,G是G‘的子图,且G’也是双连通分支。
二、一些命题的证明
命题:两个双连通分支之间最多有一个公共点。
证明:假设双连通分支C1、C2,且有两个公共点v1、v2,因为双连通分支划分非桥边,所以v1与v2之间至少有两条边,因此假设v1与v2有两条边,如下图:
首先声明,e1、e2可能是个路径,即由多条边组成,但是我们假设e1与e2为边。
e1在C1中,因此e1与C1中任何一条边都在一个简单回路中,同理,e2与C2中任何一条边都在一个简单回路,因为e1与e2在一个回路,所以e1与C2中任何一条边都在一个简单回路,e2与C1中任何一条边都在一个简单回路,所以C1与C2合并。
命题:两个双连通分支之间的一个公共点是割点。
证明:如果公共点不是割点,则将A点去除后,C1与C2仍然连通,因此必然存在u与v使得(u,v)属于E,因此当A存在时,u到A存在路径,A到v存在路径,u和v之间存在一条边,因此(u,v)与C1中任何一条边都在一个简单回路,(u,v)与C2中任何一条边都在一个简单回路,所以C1与C2合并。
命题:割点一定属于至少两个双连通分支。
证明:如果割点属于一个双连通分支,根据双连通分支定义,去掉任何一个点都不会让图不连通,与割点的定义矛盾。
命题:在一个双连通分支中至少要删除两个点才能够使图G不连通。
证明:双连通分支定义。Trivial。
Equivalence relation:边e1和e2是等价关系的当且仅当e1=e2或e1与e2在一个回路中。
Equivalence Class:一个等价类中的两条边都是等价关系。
命题:对于根顶点u,u为割点当且仅当u有至少两个儿子。
首先说明一点,无向图只有树边和反向边。
=>
已知Gn的根是割点,如果Gn中该根s只有一个子女节点t,则去除了根节点后,其子女节点t仍然连接着分支,这与根s为割点条件不符,因此根至少两个子女。
<=
如果Gn中根有至少两个子女,因为无向图只存在树边、反向边,没有交叉边,因此当根去除后,分支之间不能连接,因此根为割点。
命题:对于非根顶点u,u为割点当且仅当u只要存在一个子顶点s,s的后裔(注意是后裔,不是真后裔)没有指向u的真祖先的反向边。
=>
已知Gn中某个非根节点v是割点,如果任何v的子顶点s,s或s的后裔指向v的真祖先的反向边,我们考虑一个分支,假设反向边为(a,b),其中a为v的子顶点,b为v的真祖先,当v删除后,因为v的真祖先连通,v的子顶点之间连通,如果存在{a,b}的边,则v的真祖先和v的子顶点也连通,与条件矛盾。
<=
如果存在一个子顶点s,不存在s或s的后裔指向v的真祖先的反向边,则v的真祖先区域和v的子顶点区域不连通,因此v为割点。
命题:对于非根顶点u为割点,当且仅当存在相邻子顶点,使得 low[v]>=d[u].
证明:
=>
因为u为割点,因此u有一个子顶点v,不存在v或v的真后裔顶点指向u的真祖先的反向边。
因为 low[v] = min { d[v], d[w]},其中对于v的后裔u,(u,w)为反向边。
因为d[w]>=d[u],所以low[v]>=d[u].
<=
因为low[v]>=d[u],所以 low[v] = min { d[v], d[w]}>=d[u],因为d[v]>=d[u],所以d[w]>=d[u],所以w为u的后裔。
所以不存在v,v或v的子顶点存在指向u的真祖先的反向边,因此u为割点。
命题:(u,v)是桥,当且仅当low[v]>d[u].
证明:
=>
已知(u,v)是桥,则此边不在任何简单回路中,不失一般性,先访问u,再访问v,则low[v]=min { d[v], d[w]}, d[w]>d[u],所以low[v]>d[u].
<=
已知low[v]>d[u],所以d[w]>d[u],所以不存在v或v的子顶点,(v,w)为反向边,且w是u的后裔,所以(u,v)不属于简单回路,所以是桥。
命题:双连通分支的任意两条边位于同一个简单回路等价于双连通分支中没有割点。
证明:
如果存在割点u,设与u相邻的点为v1,v2,….vn,则因为u去掉后,应该使得v1…vn中的至少其中一个顶点和u不连通,假设此点为vi,但是根据双连通分支的定义,(u,vi)位于简单回路,因此根据定义u和vi应该仍然连通,所以矛盾。
点将一个图分成了两部分,设从任一部分的任一点出发对图进行DFS遍历,当遍历递归到割点后,
就进入了图的第二部分。又因为每个点只能visit一次,所以第二部分的点不论怎样遍历再也回不
到第一部分了。当所有点(第二部分中)都被访问完后,才回溯到割点,再继续向上回溯。
在DFS的过程中,记录每个点u在DFS树中的标号n1(放在dep[u]中),以及该点所能到达的最小顺序
号n2(存在low[u]中)。注意:这个n2在求取时,是递归进行的,从u的子孙们的low值n2与u的祖先
们的dep值n1(此时u祖先的low值还未求出,dep相当于它的low值)中挑出最小的。这就给了u的儿子
们low值比u还小的机会。然而,如果u是割点,那么u孩子们的low值就决然 >= u 了。这也就成了
判断u是割点的方法。
至于割边,可以再判断u是否为割点的同时,顺便判断<u,u儿子i>是否为割边。只要满足low[i]>u
就行了。
另外,对于dfs起点就是一个割点的情况:如果不是割点,那它必然只有一个儿子(其他连接都被
dfs回溯掉了)。它必须是割点,才能保证它的几个儿子不被dfs回溯掉。
注意:想要记录割点u切去后增加的连通分量数目。不能简单的记录u的孩子数,而必须在判断u为
割点成立的地方进行统计。即有多少个证明了u是割点的孩子,它们就是u在切除后生成的新连通
分量。割点u的某些孩子不一定能证明u是割点,因为它可能与比u小的某个点相连,从而使自己的
low值比u还小,这与具体的dfs树有关,即遍历的顺序有关。 可见末尾的一组数据,对同一个图
的描述,由于建树的方式不同,导致3的儿子4,不能证明3是割点。从而使3的孩子数(3) != 3造就
的连通分量数2(删除3后,两棵子树成为连通分量)。
针对这点再强调一点:对根节点删掉自己后,就只剩新生的联通分量了。不同于枝节点,还有旧的
连通分量在。
汇总如下:
如果根结点有大于1个的儿子,那么根结点是割点。(这是防止)
如果对于点u的某个儿子v,有low[v] >= dep[u],那么u就是一个割点。
如果对于点u的某个儿子v,有low[v] > dep[u],那么(u,v)是一条割边。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
const int WHITE = 0,GREY = 1,BLACK = 2; //标记值
int map[N][N];
int col[N],low[N],dep[N];//color
int n,m;
bool tag[N];//标记点i是否割点
//求0-1图的割点
void dfs(int now,int father,int depth){
col[now] = GREY;
dep[now] = depth;
int child = 0;
for(int i=1;i<=n;i++){
if(map[now][i]==0)continue;
if(i != father && col[i] == GREY)
low[now] = min(low[now], dep[i]);//low需要被初始化成大数
if(col[i] == WHITE){
dfs(i, now, depth+1);
child = child + 1;
low[now] = min(low[now], low[i]);
if((now==1&&child>1)||(now!=1&&low[i]>=dep[now])) //判割点
tag[now] = 1;//注意:需要记录该割点增加几个联通分量的操作需要在这里cnt++
}
}
col[now] = BLACK;
}
void init(){
memset(map,0,sizeof(map));
memset(col,0,sizeof(col));
memset(tag,0,sizeof(tag));
for(int i=0;i<=n;i++)low[i]=INT_MAX; //low应该被初始化成大值
}
int main(){
int a,b;
cin>>n>>m;
init();
for(int i=0;i<m;i++){
cin>>a>>b;
map[a][b] = map[b][a] = 1;//无向图
}
dfs(1,1,0);//dfs(root,root,0)的形式调用就行了
int ans=0;
for(int i=1;i<=n;i++)
if(tag[i])cout<<i<<‘ ‘;
cout<<endl;
system(“pause”);
return 0;
}
另外一种写法:
#include <iostream>
using namespace std;
const int MAXN = 1010;
int map[MAXN][MAXN],bridge[MAXN][MAXN];
int color[MAXN],d[MAXN],anc[MAXN],cut[MAXN],visited[MAXN];
int node_for_tree[MAXN],map_for_tree[MAXN][MAXN];
int N,M,bridgeN,index;
void DFS(int child,int father,int deep)
{ //在遍历时father先于child,且两者有连边 int i;
color[child] = 1;//遍历过可是未找到它所属的强连通分量,标志为灰色
d[child] = deep;//标志找到该节点时是第几层
anc[child] = deep;#anc[child]等价low[child];
int nodeN = 0;//标志该强联通分量的节点个数
for( i = 1 ; i <= N ; i ++ )
{ //枚举所有节点
if( map[i][child] && i != father && color[i] == 1 )//i已编历,存在回边
anc[child] = min(anc[child],d[i]);//保存连通分量里度数最小的
if( map[i][child] && !color[i] )//有连边且未遍历
{
DFS(i,child,deep+1);//递归搜索下一层
nodeN ++;//将新节点加入连通分量中
anc[child] = min(anc[child],anc[i]); //如果child是根且不知一个顶点在该连通分量中or child非根且没有回边可到达father的祖先
if(( child == 1 && nodeN > 1 ) || (child != 1 && anc[i] >= d[child] ))
cut[child] = 1;//标志为割点
if( anc[i] > d[child] )//说明只能又child到i了,没有回边
{
bridge[child][i] = bridge[i][child] = 1;//标记为桥 bridgeN ++;
}
}
}
color[child] = 2;//遍历完时赋为黑色
}
void tight(int j)
{
visited[j] = 1;
node_for_tree[j] = index;
int i;
for( i = 1 ; i <= N ; i ++ )
{
if( !visited[i] && map[j][i])
if( !bridge[j][i] )//非桥 tight(i);
}
}
int main()
{
int i,j,a,b;
char str[20]; //freopen(“3352.txt”,”r”,stdin); //
while( gets(str) != NULL )
while( scanf(“%d %d”,&N,&M) != EOF )
{
//printf(“Output for “);
//puts(str);
memset(map,0,sizeof(map));
memset(color,0,sizeof(color));
memset(d,0,sizeof(d));
memset(anc,0,sizeof(anc));
memset(cut,0,sizeof(cut));
memset(visited,0,sizeof(visited));
memset(node_for_tree,0,sizeof(node_for_tree));
memset(map_for_tree,0,sizeof(map_for_tree));
memset(bridge,0,sizeof(bridge));
//scanf(” %d %d”,&N,&M);
for( i = 0 ; i < M ; i ++ )
{
scanf(“%d %d”,&a,&b);
map[a][b] = 1;//图是双向的
map[b][a] = 1;
}
//getchar();
bridgeN = 0;
DFS(1,1,0);//找出关键边,即桥;为缩图成树做准备
index = 1;
bool flag = true;
while( flag )
{
flag = false;
for( i = 1 ; i <= N ; i ++ )//找到第一个没有访问的顶点
{
if( !visited[i] )
{
flag = true; break;
}
}
tight(i);//将i所在的连通分量绑定为树的一个节点
index ++;
}
for( i = 1 ; i <= N ; i ++ )
for( j = 1 ; j <= N ; j ++ )
if( bridge[i][j] )//找到桥边
{
int t1 = node_for_tree[i];//找到桥两个端点在树中的编号
int t2 = node_for_tree[j];
map_for_tree[t1][t2] = 1;
map_for_tree[t2][t1] = 1;
}
int temp,leaf_num = 0;
for( i = 1 ; i < index ; i ++ )//树节点数总共为index
{
temp = 0;
for( j = 1 ; j < index ; j ++ )
{
if( map_for_tree[i][j] && i != j )//有通路且不是环
temp ++;
}
if( temp == 1 )//说明找到叶子节点了,度数为一
leaf_num ++;
}
printf(“%d\n”,(leaf_num+1)/2);
//getchar();
}
return 0;
}
第三种写法:
#include<iostream>
#include<stdio.h>
#include<string.h>
using namespace std;
const int MAX=1005;
struct Edge
{
int s,e;
}E[10000];
int map[MAX][MAX];
int ancentor[MAX];
int colour[MAX];
int cut[MAX];
int brige[MAX][MAX];
int D[MAX];
int prin[MAX];
int degree[MAX];
int n,m,tol,index,cnt;
void init()
{
memset(degree,0,sizeof(degree));
memset(brige,0,sizeof(brige));
memset(map,0,sizeof(map));
memset(cut,0,sizeof(cut));
memset(colour,0,sizeof(colour));
}
void dfs(int node,int father,int deep)
{
colour[node]=1;
D[node]=deep,tol=0;
ancentor[node]=D[node];
for(int i=1;i<=n;i++)
{
if(map[node][i]&&colour[i]==1&&i!=father)
ancentor[node]=min(ancentor[node],D[i]);
if(map[node][i]&&colour[i]==0)
{
dfs(i,node,deep+1);
tol++;
ancentor[node]=min(ancentor[node],ancentor[i]);
if((node==1&&tol>1)||(node!=1&&ancentor[i]>=D[node]))
cut[node]=1;
if(ancentor[i]>D[node]) brige[node][i]=1;
}
}
colour[node]=2;
}
void Set_colour(int node)
{
for(int i=1;i<=n;i++)
{
if(!prin[i]&&map[node][i])
{
if(brige[node][i]) prin[i]=index++;
else prin[i]=prin[node];
Set_colour(i);
}
}
}
int main()
{
scanf(“%d%d”,&n,&m);
int x,y;
init();
for(int i=0;i<m;i++)
{
scanf(“%d%d”,&x,&y);
E[i].s=x;
E[i].e=y;
map[x][y]=1;
map[y][x]=1;
}
dfs(1,-1,1);
index=2,prin[1]=1;
Set_colour(1);
for(int i=0;i<m;i++)
{
int s=prin[E[i].s];
int e=prin[E[i].e];
if(s!=e)
{
degree[s]++;
degree[e]++;
}
}
cnt=0;
for(int i=1;i<index;i++)
if(degree[i]==1) cnt++;
printf(“%d/n”,(cnt+1)/2);
return 0;
}