updata:修正错误
有向图强连通分量
有向图强连通分量就是在一个强连通分量里面,每个点都能到达分量里面其他所有点。(很明显的是每个点只能属于一个分量)
那么,如何求?
Tarjan算法
定义
我们定义一个low数组与一个dfs数组与一个ts(时间戳,不需要过多理解,下文看了就知道功能了)
做法
先说DFS树,Tarjan算法实际上是利用了DFS树和时间戳进行的寻找联通分量的算法。
一个图的DFS树是什么,对一个图进行DFS,每个点只能遍历一遍,走过的边和点构成了一棵树,例如下面这个例子。
时间戳是什么,你可以理解为访问这个点的时间,我们有个(tim)变量,每次访问一个点,就把(tim++),因此不难发现,一个点访问的早,时间戳也早。
好,那么我们进入到DFS的世界中去:
(其中,边权为(1)的边表示不是DFS树中的边,有一条边有两个箭头的,原本是想两条有向边的,但是画图网站直接给我合并了。。。)
这里我们对于每个点开两个变量,一个(dfn),储存时间戳,(low)刚开始储存时间戳,在中间可能更新,具体作用后面讲。
好,在DFS中,我们总是会遇到以下两种情况:
- 这个点访问过了,实际上就是已经有了时间戳。
- 这个点没有被访问过,表现上就是这个点没有时间戳。
第二种情况直接访问即可,因此我们要针对的是第一种情况。
对于(4->2)这条边,表现上是(DFS)树中儿子连向父亲,我们称其为(A)类边,那么代表这个儿子和父亲实际上是一个强连通分量。
对于(2->4)这条边,表现上是(DFS)树中父亲连向已经访问过的儿子,我们称其为(B)类边,那么这个儿子有没有可能跟这个这个父亲同一个分量呢?因为一个分量中的点能互相到达,所以默认是这个儿子来访问爸爸,而不是爸爸访问儿子,所以这个边我们先暂时不理会他。
对于(3->5)这条边,实际表现是一个子树连向另外一个子树,我们称其为(C)类边,这种情况比较复杂,所以我们将在下文的分析后,开始这三条边的分析。
(为了方便下文讨论,我们称第二种情况的边是(D)类边)
对于一个联通分量,两个点必定能互相访问(但是需要注意的是,(x->y)的路径和(y->x)的路径可能有相同的边,比如:(1->2,2->3,3->4,3->1,4->2)),加入这个分量中有一个点最先被访问,我们设他是(fa)吧,好,这个时候(low)派上了用场,如果一个点(dfn==low),那么其是(fa)点,反之不是。
先假如图中只有一个联通分量,那么有以下几个引理:
-
每个点都会被访问到。
证明:如果(x)没有被访问到,假设(fa)到(x)的路径是:(fa->a_1->a_2->...->a_k->x),那么(a_k)是会访问(x)的,什么?(a_k)也没有被访问,看(a_{k-1}),一直看下去,(fa)总被访问了吧。
-
一个分量的DFS树不一定只是一条链。
你问我这个怎么证明?构造法啊。
看,如果先更新了(3),那么(DFS)树就变成了(1)有两个儿子(2,3)。
-
如果(x)有一条边指向已经被访问过的点(y),我们采用(low[x]=min(low[y],low[x]))的更新方式,且如果(x)访问到未访问点(y),在(y)完成(DFS)之后,也执行上述语句,那么除了(fa)点外,每个点都不可能(low==dfn)。
证明:假如一个点,如果一个点,或者其DFS子树中有一个点可以到达(fa),那么这个点绝对不可能是(low==dfn),那么对于一个点(x),其一定存在一条到(fa)的路径,但是为什么其子树没有呢?还记得(2)中的例子吗,说明其路径中有点(y)已经被访问过了,如果假设(y)的(low)不等于(y)的(dfn),因为(y)比(x)先访问,所以(x)也满足要求,而刚开始(DFS)时,第一个访问到已访问点的点,一定满足(low)不等于(dfn)。
但实际上,(low[x]=min(low[x],dfn[y]))也不会错(但是需要注意,(D)类边的更新方式永远都是(low[x]=min(low[y],low[x]))),因为已访问点(y)比(x)先访问,所以也可以满足条件。
但是在实际使用中,是不可能只有一个联通分量的,因此,我们可以把一个强连通分量看成一个点,那么整个图就会变成DAG图,这个方法叫做缩点。
如这个例子,把左边变成了右边。
我们在(DFS)中,我们搜到一个分量中的一个点,上文讲了,称这个点为(fa),那么所有这个分量的点都在其子树中,还记得上面的三条边吗。
(A)类边其实就是更新(low)的途径。
(B)类边毫无作用,因为如果这个儿子跟我是同一个分量,设这条边为(x->z),则(z)在(x)的DFS树上的一个儿子(y)的子树中,那么可以直接通过(y)完成更新,完全没有必要考虑这种情况,但是考虑了也不会错,因此在打代码的过程中可以直接考虑,减少代码量(不难发现,在(min(low[x],dfn[y]))和(min(low[x],low[y]))两种更新模式都不会错)。
(C)类边,上文说了比较复杂,如:(x->y),有可能(x,y)不是同一个分量的,那么(y)已经被先访问了,时间戳也比(x)小,那不就更新了吗?但是(y)所在的分量的(fa)节点是否已经结束(DFS)了呢,如果没有,说明(y)所在分量的(fa)是(x)在DFS树中的父亲,换而言之,(x)可以到达其父亲节点,那么(x)确实不可能是(fa)节点,而且是和(y)是同一个分量的矛盾,所以其(fa)一定已经停止(DFS)了(一个分量的(fa)停止了思考,表示这个分量的点都被找过了)。因此我们可以设置(be)数组,表示这个点所属的分量的(fa)节点是否已经停止了思考,然后只用(be)为(0)的点更新自身即可。
(D)类边,只需要担心(x)在其(DFS)子树中如果有不是跟(x)同个联通块的怎么处理。
需要证明一个东西,一个联通块的除(fa)以外的点的父亲节点都是这个联通块的点,根据引理1,每个点都会被访问,如果这个点被非联通块的点(x)访问了,那么(fa)可以到(x),(x)可以到这个点,那么这个联通块的每个点都可以通过(fa)到(x),(x)可以通过这个点到每个点,那么(x)也是这个联通块的点,矛盾,证毕。
所以如果(x)通过(D)类边访问到了(y),(y)肯定是(fa)节点,假设(y)的(low==dfn),那么(x)也肯定是正确的啦,而(x)的子树中一定存在一个(fa)点,其子树中不存在(fa)点,但是这个(fa)点一定满足要求吗?但是对于这个点而言(ABC)类边都是正确的,所以这个(fa)点也是正确的,证毕。
至于如何处理(be)吗,考虑到一个一个分量在(DFS)中的连续性,我们可以用栈储存,访问时,加入栈,当(fa)点回溯时,把(fa)点以及(fa)点以上的点全部弹出,根据数学归纳法,一定存在一个(fa),其子树的不存在(fa),这样肯定可以,而且访问(fa)之前和访问(fa)之后的栈是一样的,因此可以不断的把底层的分量删除,最终删完整个图。(其实细心的人不难发现,这不是缩点后的DAG的拓扑排序吗,这也是为什么说联通块编号是反着的拓扑序的原因)
low,dfn的相等情况证明
那么为什么(x)的(low)不一定等于分量中(fa)的(dfn)呢?
这还不简单,构造法啊。
如果(2)先访问了(4),那么(4)的(low)并不会等于(1),当然,这并不代表我们的做法是错的,只是单纯的因为(1)到(4)再回到(1)必须经过(4)而已,当然点双需要注意这个东西(后文会讲)。
扩展芝士:
当然,现在我们尝试证明存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过,是(low[x]=dfn[x])的充分不必要条件。(在(min(low[x],low[y]))的更新条件下)
充分性:
这样子的话,设路径为(fa->a_1->a_2->...->a_q->x->b_1->...->b_p->fa),然后(x)访问到了(b_i),发现(b_i)访问过了,就把(x)换成(b_i),最终会换成一个(b_j),使得访问(b_j)时,其子树中的一个点走过(fa)。
不必要性:
也就是证明(low[x]=dfn[x])时,存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过不成立,证明逆否命题是错的。
不存在(fa->x->fa)的一条路径使得路径上没有一个点重复走过时,则(low[x]≠dfn[x])。
事实上:
这张图中,如果(2)优先访问了(3),什么事都没有。
证毕
当然,至于(min(low[x],dfn[y]))的更新方式吗。懒得想
DFS处理问题
需要注意的一件事情是,一次(DFS)不一定可以搞出所有的点,所以需要判断每个点有没有(DFS)过,当然,也可以设置一个超级根节点,把这个点跟所有点连起来。
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int belong[61000],dfn[61000],cnt,low[61000],n,m,id;
struct node
{
int y,next;
}a[210000];int len,last[61000];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}//边目录
int sta[61000],p;//栈
bool v[61000];//所在的分量的fa找完没?其实就是上文的be数组
void dfs(int x)
{
dfn[x]=low[x]=++id;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(dfn[y]==0)
{
dfs(y);//便历
low[x]=min(low[x],low[y]);
}
else if(v[y]==false)low[x]=min(low[x],dfn[y]);//low[x]=min(low[x],low[y]);也不会错
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
do
{
now=sta[p--];
v[now]=true;//找完了
belong[now]=cnt;//所在的分量
}while(now!=x && p>0);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)dfs(i);//遍历
}
printf("%d
",cnt);
return 0;
}
练习
1
这道题目难度有点大,我们做一遍Tarjan算法,然后把每个强连通分量当成一个点,计算每个点的入度与出度,我们需要知道,为什么这些点(我们已经把所有强连通分量缩点了)不在一个强连通分量里面?
比如:
我们可以姑且的认为,一个长得像(1->2->3->4->...)的点叫伪点(非专业术语)
而一个伪点一般有一个点入度为0,一个点出度为0,当然,即使有特殊情况使得某个为0也是没问题的,代表他和其他伪点已经有联系了。
那么,我们只需要把一个伪点没入度的连向没出度的(当然,只有一个分量的话要特判,直接输出0),也就是max(rdcnt,cdcnt)。
虽然很难理解,但是画以下图就知道了。
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
int flog[21000],fa[21000],biao[21000],id,n,m,cnt,t;
struct node
{
int x,y,next;
}a[51000];int last[21000],len,list[21000],top;
bool v[21000];
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
inline int mymin(int x,int y){return x<y?x:y;}
inline int mymax(int x,int y){return x>y?x:y;}
void dfs(int x)
{
fa[x]=biao[x]=++id;
list[++top]=x;v[x]=true;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(biao[y]==0)
{
dfs(y);
fa[x]=mymin(fa[x],fa[y]);
}
else
{
if(v[y]==true)fa[x]=mymin(fa[x],fa[y]);
}
}
if(biao[x]==fa[x])
{
int i=0;cnt++;
while(i!=x)
{
i=list[top--];
flog[i]=cnt;
v[i]=false;
}
}
}
int rd[21000],cd[21000];
int main()
{
//freopen("b.in","r",stdin);
//freopen("1.out","w",stdout);
scanf("%d",&t);
while(t--)
{
memset(fa,0,sizeof(fa));
memset(biao,0,sizeof(biao));cnt=0;len=0;id=0;
memset(last,0,sizeof(last));top=0;
memset(rd,0,sizeof(rd));memset(cd,0,sizeof(cd));
scanf("%d%d",&n,&m);
int ans1=0,ans2=0;
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(biao[i]==0)dfs(i);
}
if(cnt==1)
{
printf("0
");
continue;
}
for(int i=1;i<=m;i++)
{
int tx=flog[a[i].x]/*缩点*/,ty=flog[a[i].y];
if(tx!=ty)
{
rd[ty]++;cd[tx]++;
}
}
for(int i=1;i<=cnt;i++)
{
if(rd[i]==0)ans1++;
if(cd[i]==0)ans2++;
}
printf("%d
",mymax(ans1,ans2));
}
return 0;
}
2
我们先跑一遍二分匹配,然后把原本的边反向建(母牛连向公牛),并且连一条边,公牛连向他匹配的母牛,那么再跑一边强连通,我们就会发现每个分量里面都是公牛->母牛->公牛->母牛...
也就是说每个母牛至少有两个选择,公牛也是,然后我们在找公牛能****(手动打码)的每个母牛,如果母牛跟公牛在同一分量中,那么这个母牛原本的公牛也可以在找另外一头母牛,是不是很厉害?
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<bitset>
using namespace std;
const int cc=4010;
struct node
{
int y,next;
}a[200010];int len,last[cc];
struct trlen
{
int x,y,next;
}map[200010];int tlen,tlast[cc];
int cnt,id,p;
int sta[cc],low[cc],dfn[cc],belong[cc];
int chw[cc],match[cc],n;
bool v[cc];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}
void ins1(int x,int y)
{
tlen++;
map[tlen].x=x;map[tlen].y=y;map[tlen].next=tlast[x];tlast[x]=tlen;
}
bool find(int x)
{
for(int k=tlast[x];k;k=map[k].next)
{
int y=map[k].y;
if(chw[y]!=id)
{
chw[y]=id;
if(match[y]==0 || find(match[y])==true)
{
match[y]=x;
return true;
}
}
}
return false;
}
void dfs(int x)
{
low[x]=dfn[x]=++id;v[x]=true;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(low[y]==0)
{
dfs(y);
low[x]=min(low[x],low[y]);
}
else if(v[y]==true)low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
do
{
now=sta[p--];
v[now]=false;
belong[now]=cnt;
}while(now!=x);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int kkk=0;scanf("%d",&kkk);
for(int j=1;j<=kkk;j++)
{
int x;scanf("%d",&x);
ins1(i,x+n);
}
}
for(int i=1;i<=n;i++)
{
id++;
find(i);
}//二分匹配
for(int i=1;i<=n;i++)ins(match[i+n],i+n);
for(int i=1;i<=tlen;i++)ins(map[i].y,map[i].x);
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i);
}
for(int i=1;i<=n;i++)
{
int jj=belong[i],ansl[cc];
ansl[0]=0;
for(int j=tlast[i];j;j=map[j].next)
{
if(belong[map[j].y]==jj)ansl[++ansl[0]]=map[j].y-n;
}
sort(ansl+1,ansl+1+ansl[0]);
for(int j=1;j<ansl[0];j++)printf("%d ",ansl[j]);
printf("%d
",ansl[ansl[0]]);
}
//输出
return 0;
}
3
这道题比较简单,如果一个强连通分量有边连向其他分量,这个分量都没用了。
#include<cstdio>
#include<cstring>
using namespace std;
inline int mymin(int x,int y){return x<y?x:y;}
int n,m;
int low[21000],dfn[21000],belong[21000],cnt,out[21000],stp;
int sta[21000],tp=0;bool v[21000];
struct node
{
int x,y,next;
}a[21000];int last[21000],len;
int ansl[21000];
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
void dfs(int x)
{
low[x]=dfn[x]=++stp;v[x]=true;sta[++tp]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(low[y]==0)
{
dfs(y);
low[x]=mymin(low[x],low[y]);
}
else if(v[y]==true)low[x]=mymin(low[x],dfn[y]);
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
while(now!=x)
{
now=sta[tp--];
belong[now]=cnt;
v[now]=false;
}
}
}
int main()
{
memset(v,true,sizeof(v));
while(scanf("%d",&n)!=EOF)
{
if(n==0)break;
scanf("%d",&m);
memset(low,0,sizeof(low));stp=0;
memset(last,0,sizeof(last));len=0;
memset(out,0,sizeof(out));
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i);
}
for(int i=1;i<=m;i++)
{
if(belong[a[i].x]!=belong[a[i].y])out[belong[a[i].x]]++;
}
for(int i=1;i<=n;i++)
{
if(out[belong[i]]==0)ansl[++ansl[0]]=i;
}
for(int i=1;i<ansl[0];i++)printf("%d ",ansl[i]);
printf("%d
",ansl[ansl[0]]);ansl[0]=0;
}
return 0;
}
双连通分量
双联通分量就是无向图的强连通分量
UPDATA:由于后面又更新了博客,所以有些内容看起来比较神奇,可能就是前一次的内容和后一次的更新内容卡一块了。
边-双连通分量
桥
如果原本两个点是连通的,截断一条边就使得两个点不联通了,这条边叫桥。
边双
定义
边双连通分量就是分量中不含桥的联通块。
一个点只会属于一个边双,如果属于两个边双,可以把这两个边双合并(因为如果这两个边双间有桥,删掉后,有(x),不会不连通,所以矛盾,不存在桥)。
做法
没错,Tarjan永远的神!!!!
跟强连通一样的更新套路,但是需要注意的一件事情是,不可能存在(C)类边(比较重要的性质),因为如果(x->y),那么理论上来讲,因为这是双向边,所以(y)在(DFS)的时候就可以访问(x)。而且需要注意。需要保存父亲边(需要注意的是,这里保存的是父亲边,因为可能存在重边),不能走过父亲边(但是如果题目中要求重边视为一条,则保存父亲),所以不需要保存(be)数组。
至于判断条件,有两种方法。
-
对于(low[x]==dfn[x])时,其到父亲的边是桥(但是如果(x)是(DFS)树的根则没有父亲),且(x)是(fa)点。
这种跟强连通是比较像的,仔细想想可以发现,栈的证明是一样的。
应该 -
对于(x)在(DFS)树中的一个儿子(y),若(low[y]>dfn[x]),则(x-y)的边是桥,(y)是(fa)节点,需要注意的是,这种写法,栈的弹出设定不应该是:(sta[top]!=x),而应该是(now!=y),因为(y,x)在栈中不一定连续,可能存在(x)的另外一个儿子和(x)是一个边双的。
缩点
缩点都是差不多的,但是无向图的缩点最后会变成树,而不是DAG,后面的点双也是。
dfn,low相等情况证明
需要注意的是,由于边双中,(fa->x->fa)的路径中存在点重复无所谓,所以(min)中可以(dfn,low)都可以,而其(low[x])和(dfn[fa])的等于情况和强连通基本相同(好像就是相同的),所以不做过多分析。(反正不存在(C)类边,随便分析)
DFS处理
由于是无向图,如果图联通,只需要跑一次(DFS),点双边双都一样,不需要像强连通一样用循环判断每个点时候(DFS)了。当然如果图不连通还是要的
代码
代码:
void dfs(int x,int fa)
{
low[x]=dfn[x]=++stp;
sta[++tp]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=fa)
{
if(low[y]==0)
{
dfs(y,x);
low[x]=min(low[x],low[y]);
}
else low[x]=min(low[x],dfn[y]);
}
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
while(now!=x)
{
now=sta[tp--];
belong[now]=cnt;
}
}
}
练习
这次没例题,直接放练习
像上次那样,我们记录每个分量的度(无向边),为0,ans+=2,为1,ans++
然后答案为ans/2+ans%2
#include<cstdio>
#include<cstring>
using namespace std;
inline int mymin(int x,int y){return x<y?x:y;}
int low[6000],dfn[6000],belong[6000],cnt,stp;
int sta[6000],tp;
struct node
{
int x,y,next;
}a[21000];int last[6000],len;
int ax[11000],ay[11000],n,m,io[11000],ans;
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
void dfs(int x,int fa)
{
low[x]=dfn[x]=++stp;
sta[++tp]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=fa)
{
if(low[y]==0)
{
dfs(y,x);
low[x]=mymin(low[x],low[y]);
}
else low[x]=mymin(low[x],dfn[y]);
}
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
while(now!=x)
{
now=sta[tp--];
belong[now]=cnt;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&ax[i],&ay[i]);
ins(ax[i],ay[i]);ins(ay[i],ax[i]);
}
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i,0);
}
if(cnt==1){printf("0
");return 0;}//特判
for(int i=1;i<=m;i++)
{
if(belong[ax[i]]!=belong[ay[i]])io[belong[ax[i]]]++,io[belong[ay[i]]]++;
}
for(int i=1;i<=cnt;i++)
{
if(io[i]==0)ans+=2;
else if(io[i]==1)ans++;
}
printf("%d
",ans/2+ans%2);
return 0;
}
点双连通分量
割点
就是把点割掉后,会导致原本联通块变成多个联通块,那么这个点就是割点。
点双连通分量
点双就是不含割点的连通分量(特别的,两个点一条边也是一个点双)。
点双最麻烦的就是一个点可能属于多个分量,如果一个点属于多个连通分量,此时这个点一定是割点,而割点也必然属于多个联通块,如下图。
我们发现,中间有一个点属于两个连通分量。
性质:
- 点双每两个点存在一个简单环,每三个点(x,y,z),一定存在(x->y->z)的一条简单路径。(应该吧,这个在下文小结给出的扩展资料有证明)
- 我们探究一下边双和点双的关系,不难一个桥左右两个点一定是割点,而一条边左右两个点是割点,但这条边不一定是桥,好,这样的话,一个边双中可能含有很多个割点,只要不含桥即可,因此一个边双可以被拆分成多个点双,而且,点双中一定不存在桥。
- 设(x)属于点双(A),属于边双(B),那么(A∩B=B),证明:如果存在(y)不属于(B)而属于(A),那么(x,y)之间存在简单环(点双的),环上不存在割边,那么(y,x)应该属于同一个边双,因此矛盾,证毕。
点双其实可以看作边的联通关系,因为每条边最多属于一个分量(这个证法就是删去证联通)。
点双依旧不存在(C)类边,且点的在(DFS)树中的连续性也是可以证明的,证法依旧类似。
但是突然发现一个十分重要的事情:如果不存在一条(fa->x->fa)的路径使得路径上点不重复(不包括起终点)的话,那么(x,fa)不可能是一个分量中的(实际上说明这个(fa)不是(x)所在连通分量的(fa))。我们当时证明了,如果我们采用了(min(low[x],low[y]))的更新方式,那么(low[x]=dfn[fa]),这个是很明显的错误,因此我们只能
那么就只能采用(min(low[x],dfn[y]))的更新方法了,但是这个为什么对呢?扩展资料中证明了,两个点之间不存在简单环,则一定存在割点,那么这种方法更新的话,(x)最多就等于(x,fa)中间那个割点的(dfn),因此不用担心此情况,好,那么存在简单环就一定对吗?
好,就证明一个引理:
(x-a_1-a_2-a_3.-..-a_q-y),其中(x)准备(DFS),(x)在DFS树中在(y)的子树中,(a_i(1≤i≤q))没有被访问过,那么在(x)(DFS)完之后,(low[x]≤dfn[y])。假设(a_i)是下一个(DFS)的对象,那么只要(a_i-...-y)成立即可,运用数学归纳法,发现两个点一定成立,则数学归纳法生效。
好,现在证明点双中的除(fa)外每个点的(low)都小于(dfn[x])(需要注意的是,本文的证明的(fa)都不是指父亲节点,而是指联通块(fa)节点,父亲节点我会特地的打中文)。
假设存在简单环:(fa-a_1-a_2-...-a_q-fa),第一个被访问的点是(a_i),那么(a_i)一定等于的(low)一定等于(dfn[fa]),好,(a_i)到达(a_j),那么,(a_j)的(low)也等于(dfn[fa]),但是环上(a_i-a_j)中间的点呢?他们的(low)说不定不等于(dfn[fa]),于是我们继续利用数学归纳法,已知(a_i-a_j)中最先被访问的点是(a_k),那么(a_k)的(low)小于等于(dfn[a_i]),小于(dfn[a_k]),然后把区间切成了:(a_i-a_k,a_k-a_i),最终一定会被切成两个点,中间没有任何点,证毕。
好,然后直接用(min(low[x],dfn[y]))更新方法即可,当然,(D)类边一直不变都是用(low[y])更新的。
但需要注意的是,点双不需要保留父亲边,允许走父亲,反正每个点肯定和其父亲是同一个点双(因为两个点也是点双,至少有个保底)。
什么,判定一个点是不是(fa)节点?如果(x)通过(D)类边到(y),然后(low[y]==dfn[x])那么(x)是(fa)节点,和(y)一个点双。
点双缩点
由于每个割点,属于多个联通块,不能直接缩,因此,我们选择保留割点,删去所有的非割点,如果割点(x)属于联通块(y),那么(x)与(y+n)连一条边即可。
点双的点
也许就有人要问了,MAD,一个点可以属于多个点双,还要栈干什么,反正每个点的(belong)可以有多个?但是在点双缩点的恰恰就需要找出一个点双中的点(但是不用保存到后面继续使用,而是直接用链式前向星缩点建树),这个时候了,我们发现一个点在结束完(DFS)后,就不会作为(fa)节点了,所以需要特殊考虑的就是当前(DFS)的节点(x)。
难道是如果(x)是(fa)节点的话,且和(y)一个分量,就把栈中(x)及(x)以上的全部弹出,然后再把(x)加回队列中?
不,类似边双的问题(x,y)不一定连续,所以应该把(y)和(y)以上的点全部弹出,并算上(x)即可。
点双的边
当然,有的时候需要搞出一个点双中所有的边,但是边只属于一个点双,就比较方便,直接在(DFS)中往栈塞(D)类边,但是(A、B)类边呢?因为双向边,所以(A=B),所以只需要保存比较容易处理的(A)类边即可,然后在弹出的时候判断栈顶是不是(x->)y的(D)类边即可。
不对!!!!因为允许到父亲,所以实际上(D⊂A),所以,几种处理方法:
- 用(v)表示这个边是否用过。
- 不保存(D)类边,但是这样栈弹出的否决条件就比较难处理,因此改进的方法可以是优先到父亲,或者说是到父亲的边不入栈。
当然还有其他方法,不再赘述。
代码
int dfn[61000],cnt,low[61000],id;
int sta[61000],p;
void dfs(int x)
{
dfn[x]=low[x]=++id;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(!dfn[y])
{
dfs(y);
low[x]=min(low[x],low[y]);
if(low[y]==dfn[x])//我和你构成一个点双
{
int now=0;cnt++;
while(now!=y && p>0)now=sta[p--];
//可以在最后处理一下x
}
}
else low[x]=min(low[x],dfn[y]);
}
}
初始化问题
其他自己想,三个分量都差不多,这里只说栈。
其他两个分量一个点只属于一个分量,所以最后栈中一个点不剩,
但是边双最后会剩一个点,(DFS)树的根,所以在初始化的时候别忘了(top=0;)
求割点与桥
割点
我们研究DFS序就会发现,只要一个不是根结点的其中一个儿子的low小于等于(如果可以走父亲,可以直接换成等于)他的dfn,那么这个点就是割点,根节点就是他所在的点双个数大于等于(2),当然具体表现就是(D)类边的个数,因为根节点的(dfn)最小,不可能小于,只能等于。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dfn[61000],low[61000],n,m,id;
struct node
{
int y,next;
}a[210000];int len,last[61000];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}
int ans[130000];
void dfs(int x,bool type)//判断是不是根节点而已
{
bool bk=false;int cnt=0;
dfn[x]=low[x]=++id;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(!dfn[y])
{
cnt++;
dfs(y,0);
low[x]=min(low[x],low[y]);
if(low[y]==dfn[x]/*桥就是>*/)bk=true;
}
else low[x]=min(low[x],dfn[y]);
}
if(type)//根节点特判
{
if(cnt>=2)ans[++ans[0]]=x;
}
else if(bk==true)ans[++ans[0]]=x;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);ins(y,x);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)dfs(i,1);
}
sort(ans+1,ans+ans[0]+1);
printf("%d
",ans[0]);
for(int i=1;i<ans[0];i++)printf("%d ",ans[i]);
if(ans[0])printf("%d
",ans[ans[0]]);
return 0;
}
桥
桥就不打了吧,直接在边双的时候随便乱搞都可以啦。
看个人喜好吧。
小结
又水了一篇博客
补充资料:我博客中的另外一篇博客关于“双联通分量的存在条件的证明(对于算法进阶的补充)”,应该可以加深你对点双的理解。
对于练习中有些题目的证明,强烈建议去网上搜证明,我的这个太不严谨了。(其实就是懒得更新练习了)