各位大佬,你们好,博主又来认真的讲解知识了,求大家不要批判哈,可能会很菜,只求勿喷哈哈。。(这篇文章写的很艰辛,莫名其妙的被吞了一部分文字)
之前讲了有向图强连通分量,那么无向图里就不能单纯的叫强连通分量,而叫双联通分量,hhh,那双连通分量是干哈的呢(充分展现博主东北风范)?
说简单点呢就是有u->v有两条路径,且两条路径上的边不重复。
现在,我们先补充几个知识点,2333】:
点连通度与边连通度:在一个 无向连通图中,如果有一个顶点集合 V,删除顶点集合 V,以及与 V 中顶点相连(至少有一端在 V 中)的所有边后,原图 不连通,就称这个点集 V 为 割点集合。
一个图的 点连通度的定义为:最小割点集合中的顶点数。
类似的,如果有一个边集合,删除这个边集合以后,原图不连通,就称这个点集为 割边集合。
一个图的 边连通度的定义为:最小割边集合中的边数。
双连通图、割点与桥:
如果一个无向连通图的于 点连通度大于 1,则称该图是 点双连通的(point biconnected),简称双连通或重连通。
一个图有 割点,当且仅当这个图的点连通度为 1,则割点集合的唯一元素被称为 割点(cut point),又叫关节点(articulation point)。一个图可能有多个割点。
如果一个无向连通图的于 边连通度大于 1,则称该图是 边双连通的(edge biconnected),简称双连通或重连通。
一个图有桥 桥,当且仅当这个图的边连通度为 1,则割边集合的唯一元素被称为桥 桥(bridge),又叫关节边(articulation edge)。一个图可能有多个桥。
可以看出,点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的,下文中提到的双连通,均既可指点双连通,又可指边双连通。(但这并不意味着它们等价)
双连通分量(分支):在图 G 的所有子图 G'中,如果 G'是双连通的,则称 G'为双连通子图。如果一个双连通子图 G'它不是任何一个双连通子图的真子集,则 G'为极大双连通子图。
双连通分量(biconnected component),或重连通分量,就是图的极大双连通子图。特殊的,点双连通分量又叫做块。
好的好的,补充完知识点,我们可以开心加愉快地来讲算法了
与有向图求强连通分量的 Tarjan 算法类似,只需通过求 DFN 与 LOW 值来得出割点与桥。
对图深度优先搜索(DFS),定义 DFN(u)为 u 在搜索树(以下简称为树)中被遍历到的次序号。定义 Low(u)为 u 或 u 子树中的结点经过最多一条后向边能追溯到的最早的树中结点次序号。
根据定义,则有:
Low(u) = Min{
DFN(u)
DFN(v) (u,v)为后向边(返祖边) 等价于 DFN(v)<DFN(u)且v不为u的父亲结点
Low(v)(u,v)为树枝边(父子边)
}
一个顶点 u 是割点,当且仅当满足(1)或(2):
(1) u 为树根,且 u 有多于一个子树。因为无向图 DFS 搜索树中不存在横叉边,所以若有多个子树,这些子树间不会有边相连。
(2) u 不为树根,且满足存在(u,v)为树枝边(即 u 为 v 在搜索树中的父亲),并使得DFN(u)<=Low(v).(因为删去 u 后 v 以及 v 的子树不能到达 u 的其他子树以及祖先)
一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足 DFN(u)<Low(v).(因为 v 想要到达 u 的父亲必须经过(u,v)这条边,所以删去这条边,图不连通)
实现时,因为有重边的问题,所以需要将一条无向边拆为两条编号一样的有向边,用邻接表进行存储。在判断(u,v)是否为后向边时要注意是树枝边的反向边还是新的一条反向边。
特别注意的是:你需要判断所求“割点”的性质,究竟是不是真正的割点
一个有桥的连通图,如何把它通过加边变成边双连通图?
方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为 1。统计出树中度为 1 的节点的个数,即为叶节点的个数,记为 leaf。则至少在树上添加(leaf+1)/2 条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。
证明:其实博主不会严格证明啊啊(只能参考别人的)
首先在一个边双里的点之间的连边是不会减少桥的数目的。因此先缩点。
考虑我们每次找两个叶子节点连边,但是如果随便找的话,连边之后重缩点可能会出现新的叶子。
[
注意到这种问题只出现在,两个叶子之间的路径上只有至多一条支链。
[
上图即为两条支链,连边后不会出现新叶子。考虑至少有 4 个叶子的情况,选 4 个,设为 ABCD。首先考察 A 到 B 的路径和 C 到 D 的路径,如果有一条路径上其他边至少 2 条,则缩这两个点可以减少两个叶子。假设 A 到 B和 C 到 D 的路径上都只有 1 条其他边(不能没有,否则不连通)。
[
如图所示,AD 路径上至少有两条其他边,于是建一条连接 AD 的边可以删去两个叶子。
边界情况:
叶子=1:不需要边
叶子=2:链,一条边
叶子=3:需要两条边
所以我们按照上述方式选取,可以保证不出现新叶子。
所以结论就是:叶子数=1 答案为 0 否则为(叶子数+1)/2.
下面是一道例题
Redundant Paths (POJ 3177)
题目大意:有 F 个牧场,现在一个牧群经常需要从一个牧场迁移到另一个牧场。奶牛们已经厌烦老是走同一条路,所以有必要再新修几条路,这样它们从一个牧场迁移到另一个牧场时总是可以选择至少两条独立的路。现在 F 个牧场的任何两个牧场之间已经至少有一条路了,奶牛们需要至少有两条。给定现有的 R 条直接连接两个牧场的路,计算至少需要新修多少条直接连接两个牧场的路,使得任何两个牧场之间至少有两条独立的路。两条独立的路是指没有公共边的路,但可以经过同一个中间顶点。1<=F<=5000 ; F-1<=R<=10000
题目解析:题目要求任意两点间至少有两条没有公共边的路,也就是说所要求的图是一张边双连通图。根据上面所提到的,将一张有桥图通过加边变成边双连通图,至少要加(leaf+1)/2 条边。因此对于本题,我们求出所有桥,将桥删去后得出所有的边双连通分量,将它们缩为点后找出叶子数,进而求出答案。
代码如下:
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
int time,n,m,x,y,ans=0,top,i,j,tot=0;
int sure[1000][1000];
int next[400005],to[400005],head[400005],dfn[400005],low[400005],cal[400005];
int in[400005],stack[400005],belong[400005],check[400005],b[400005],vis[400005],come[400005];
void add(int x,int y)
{
tot++;
next[tot]=head[x];
to[tot]=y;
head[x]=tot;
}
void dfs(int u,int fa)
{
in[u]=1;
stack[++top]=u;
low[u]=dfn[u]=++time;
int ok=0;
for(int now=head[u];now;now=next[now])
{
int v=to[now];
if (v==fa&&ok==0)
{
ok=1;
continue;
}
if (dfn[v]==0)
{
dfs(v,u);
low[u]=min(low[u],low[v]);
}
else if (in[v]==1)
{
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u])
{
int hh;
do
{
hh=stack[top--];
belong[hh]=ans;
check[ans]++;
in[hh]=0;
}while(hh!=u);
ans++;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(i=1;i<=n;i++)
{
if (dfn[i]==0)
{
dfs(i,-1);
}
}
int sum=0;
for(int i=1;i<=n;i++)
{
for(int now=head[i];now;now=next[now])
{
if (belong[i]!=belong[to[now]])
{
come[belong[i]]++;
come[belong[to[now]]]++;
}
}
}
/*for(int i=1;i<=n;i++)
{
printf("%d ",belong[i]);
}
printf("\n");
for(int i=0;i<ans;i++)
{
printf("%d ",come[i]);
}*/
for(int i=0;i<ans;i++)
{
if (come[i]==2)
{
sum++;
}
}
printf("%d",(sum+1)/2);
return 0;
}
各位大佬,你们好,博主又来认真的讲解知识了,求大家不要批判哈,可能会很菜,只求勿喷哈哈。。(这篇文章写的很艰辛,莫名其妙的被吞了一部分文字)
之前讲了有向图强连通分量,那么无向图里就不能单纯的叫强连通分量,而叫双联通分量,hhh,那双连通分量是干哈的呢(充分展现博主东北风范)?
说简单点呢就是有u->v有两条路径,且两条路径上的边不重复。
现在,我们先补充几个知识点,2333】:
点连通度与边连通度:在一个 无向连通图中,如果有一个顶点集合 V,删除顶点集合 V,以及与 V 中顶点相连(至少有一端在 V 中)的所有边后,原图 不连通,就称这个点集 V 为 割点集合。
一个图的 点连通度的定义为:最小割点集合中的顶点数。
类似的,如果有一个边集合,删除这个边集合以后,原图不连通,就称这个点集为 割边集合。
一个图的 边连通度的定义为:最小割边集合中的边数。
双连通图、割点与桥:
如果一个无向连通图的于 点连通度大于 1,则称该图是 点双连通的(point biconnected),简称双连通或重连通。
一个图有 割点,当且仅当这个图的点连通度为 1,则割点集合的唯一元素被称为 割点(cut point),又叫关节点(articulation point)。一个图可能有多个割点。
如果一个无向连通图的于 边连通度大于 1,则称该图是 边双连通的(edge biconnected),简称双连通或重连通。
一个图有桥 桥,当且仅当这个图的边连通度为 1,则割边集合的唯一元素被称为桥 桥(bridge),又叫关节边(articulation edge)。一个图可能有多个桥。
可以看出,点双连通与边双连通都可以简称为双连通,它们之间是有着某种联系的,下文中提到的双连通,均既可指点双连通,又可指边双连通。(但这并不意味着它们等价)
双连通分量(分支):在图 G 的所有子图 G'中,如果 G'是双连通的,则称 G'为双连通子图。如果一个双连通子图 G'它不是任何一个双连通子图的真子集,则 G'为极大双连通子图。
双连通分量(biconnected component),或重连通分量,就是图的极大双连通子图。特殊的,点双连通分量又叫做块。
好的好的,补充完知识点,我们可以开心加愉快地来讲算法了
与有向图求强连通分量的 Tarjan 算法类似,只需通过求 DFN 与 LOW 值来得出割点与桥。
对图深度优先搜索(DFS),定义 DFN(u)为 u 在搜索树(以下简称为树)中被遍历到的次序号。定义 Low(u)为 u 或 u 子树中的结点经过最多一条后向边能追溯到的最早的树中结点次序号。
根据定义,则有:
Low(u) = Min{
DFN(u)
DFN(v) (u,v)为后向边(返祖边) 等价于 DFN(v)<DFN(u)且v不为u的父亲结点
Low(v)(u,v)为树枝边(父子边)
}
一个顶点 u 是割点,当且仅当满足(1)或(2):
(1) u 为树根,且 u 有多于一个子树。因为无向图 DFS 搜索树中不存在横叉边,所以若有多个子树,这些子树间不会有边相连。
(2) u 不为树根,且满足存在(u,v)为树枝边(即 u 为 v 在搜索树中的父亲),并使得DFN(u)<=Low(v).(因为删去 u 后 v 以及 v 的子树不能到达 u 的其他子树以及祖先)
一条无向边(u,v)是桥,当且仅当(u,v)为树枝边,且满足 DFN(u)<Low(v).(因为 v 想要到达 u 的父亲必须经过(u,v)这条边,所以删去这条边,图不连通)
实现时,因为有重边的问题,所以需要将一条无向边拆为两条编号一样的有向边,用邻接表进行存储。在判断(u,v)是否为后向边时要注意是树枝边的反向边还是新的一条反向边。
特别注意的是:你需要判断所求“割点”的性质,究竟是不是真正的割点
一个有桥的连通图,如何把它通过加边变成边双连通图?
方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为 1。统计出树中度为 1 的节点的个数,即为叶节点的个数,记为 leaf。则至少在树上添加(leaf+1)/2 条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。
证明:其实博主不会严格证明啊啊(只能参考别人的)
首先在一个边双里的点之间的连边是不会减少桥的数目的。因此先缩点。
考虑我们每次找两个叶子节点连边,但是如果随便找的话,连边之后重缩点可能会出现新的叶子。
[
注意到这种问题只出现在,两个叶子之间的路径上只有至多一条支链。
[
上图即为两条支链,连边后不会出现新叶子。考虑至少有 4 个叶子的情况,选 4 个,设为 ABCD。首先考察 A 到 B 的路径和 C 到 D 的路径,如果有一条路径上其他边至少 2 条,则缩这两个点可以减少两个叶子。假设 A 到 B和 C 到 D 的路径上都只有 1 条其他边(不能没有,否则不连通)。
[
如图所示,AD 路径上至少有两条其他边,于是建一条连接 AD 的边可以删去两个叶子。
边界情况:
叶子=1:不需要边
叶子=2:链,一条边
叶子=3:需要两条边
所以我们按照上述方式选取,可以保证不出现新叶子。
所以结论就是:叶子数=1 答案为 0 否则为(叶子数+1)/2.
下面是一道例题
Redundant Paths (POJ 3177)
题目大意:有 F 个牧场,现在一个牧群经常需要从一个牧场迁移到另一个牧场。奶牛们已经厌烦老是走同一条路,所以有必要再新修几条路,这样它们从一个牧场迁移到另一个牧场时总是可以选择至少两条独立的路。现在 F 个牧场的任何两个牧场之间已经至少有一条路了,奶牛们需要至少有两条。给定现有的 R 条直接连接两个牧场的路,计算至少需要新修多少条直接连接两个牧场的路,使得任何两个牧场之间至少有两条独立的路。两条独立的路是指没有公共边的路,但可以经过同一个中间顶点。1<=F<=5000 ; F-1<=R<=10000
题目解析:题目要求任意两点间至少有两条没有公共边的路,也就是说所要求的图是一张边双连通图。根据上面所提到的,将一张有桥图通过加边变成边双连通图,至少要加(leaf+1)/2 条边。因此对于本题,我们求出所有桥,将桥删去后得出所有的边双连通分量,将它们缩为点后找出叶子数,进而求出答案。
代码如下:
#include<cstring>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
using namespace std;
int time,n,m,x,y,ans=0,top,i,j,tot=0;
int sure[1000][1000];
int next[400005],to[400005],head[400005],dfn[400005],low[400005],cal[400005];
int in[400005],stack[400005],belong[400005],check[400005],b[400005],vis[400005],come[400005];
void add(int x,int y)
{
tot++;
next[tot]=head[x];
to[tot]=y;
head[x]=tot;
}
void dfs(int u,int fa)
{
in[u]=1;
stack[++top]=u;
low[u]=dfn[u]=++time;
int ok=0;
for(int now=head[u];now;now=next[now])
{
int v=to[now];
if (v==fa&&ok==0)
{
ok=1;
continue;
}
if (dfn[v]==0)
{
dfs(v,u);
low[u]=min(low[u],low[v]);
}
else if (in[v]==1)
{
low[u]=min(low[u],dfn[v]);
}
}
if(dfn[u]==low[u])
{
int hh;
do
{
hh=stack[top--];
belong[hh]=ans;
check[ans]++;
in[hh]=0;
}while(hh!=u);
ans++;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for(i=1;i<=n;i++)
{
if (dfn[i]==0)
{
dfs(i,-1);
}
}
int sum=0;
for(int i=1;i<=n;i++)
{
for(int now=head[i];now;now=next[now])
{
if (belong[i]!=belong[to[now]])
{
come[belong[i]]++;
come[belong[to[now]]]++;
}
}
}
/*for(int i=1;i<=n;i++)
{
printf("%d ",belong[i]);
}
printf("\n");
for(int i=0;i<ans;i++)
{
printf("%d ",come[i]);
}*/
for(int i=0;i<ans;i++)
{
if (come[i]==2)
{
sum++;
}
}
printf("%d",(sum+1)/2);
return 0;
}