常规操作:
首先补几个概念:
- 强连通:在一个DAG中,有 a,b 两点,若a可以到达b且b可以到达a,则(a,b)即为强连通
- 强连通图:若在一个DAG中,任意两点都为强连通,则此图为强连通图
- 强连通分量:在一个DAG中,有一个子图,若该子图为强连通图,则该子图叫做强连通分量
再来补几个数组的含义:
- dfn:dfn[i]表示第i个点的时间编号
- low:low[i]表示以i为根的子树(图)下的最小的时间编号 若dfn[i]==low[i]
- 即该点为一个强连通分量子树(图)的根
算法的核心思想:
通过栈来维护强连通,具体细节还要分类而论…
常规的用法:缩点,割点,割边,点双,边双
缩点:
概念:化环为点
原理:通过栈来将环内存下,并在第二次访问到该环的根节点时,依次弹出,进行编号
模板(code):
1.在有向图中:
void tarjan(int x){
dfn[x]=low[x]=++tim;
stk[++top]=x;
vis[x]=1;
SREP(i,0,E[x].size()){
int y=E[x][i];
if(!dfn[y]){
tarjan(y);
chkmin(low[x],low[y]);
}
else if(vis[y]) chkmin(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
tot++;
do{
Id[stk[top]]=tot;
vis[stk[top]]=0;
}while(x!=stk[top--]);
}
}
2.在无向图中:
void tarjan(int x,int f){
dfn[x]=low[x]=++tim;
stk[++top]=x;
vis[x]=1;
bool flag=1;
SREP(i,0,E[x].size()){
int y=E[x][i];
if(y==f && flag){flag=0;continue;}
if(!dfn[y]){
tarjan(y,x);
chkmin(low[x],low[y]);
}
else if(vis[y]) chkmin(low[x],dfn[y]);
}
if(dfn[x]==low[x]){
tot++;
do{
Id[stk[top]]=tot;
vis[stk[top]]=0;
}while(x!=stk[top--]);
}
}
然而一般题目多是无向图,接下来都用无向图了…
割点:
概念:若删掉某点后,原连通图分裂为多个子图,则称该点为割点。
原理:若low[y]>=dfn[x],则x为割点。因为low[y]>=dfn[x],则说明y通过子孙无法到达x的祖先。那么对于原图,去掉x后,必然会分成两个子图。
模板(code):
void tarjan(int x,int f){
dfn[x]=low[x]=++tim;
stk[++top]=x;
bool flag=1;
int child=0;
SREP(i,0,E[x].size()){
int y=E[x][i];
if(y==f && flag){flag=0;continue;}
if(!dfn[y]){
child++;
tarjan(y,x);
chkmin(low[x],low[y]);
if(low[y]>=dfn[x]){
cut[x]=1;
//该点为割点
}
}
else chkmin(low[x],dfn[y]);
}
if(!f && child==1)cut[x]=0;//注意初始点,特判
}
割边(桥):
概念:删掉该边后,原图必然会分裂为多个子图。
原理:若low[y]>dfn[x],则边(x,y)为桥。由割点同理可得。但是由于可能存在重边,需要把一条无向边拆成的两条标号相同的有向边,记录每个点的父亲到它的边的标号,如果边(x,y)是x的后向边,就不能用dfn[u]更新low[v]。这样如果遍历完v的所有子节点后,发现low[y]=dfn[y],说明x的后向边(x,y)为割边。
模板(code):
void tarjan(int x,int f){
dfn[x]=low[x]=++tim;
stk[++top]=x;
bool flag=1;
SREP(i,0,E[x].size()){
int y=E[x][i];
if(y==f && flag){flag=0;continue;}
if(!dfn[y]){
child++;
tarjan(y,x);
chkmin(low[x],low[y]);
if(low[y]>dfn[x]){
mark[x][y]=mark[y][x]=1;
//该边为割边
}
}
else chkmin(low[x],dfn[y]);
}
}
点双(点双连通分量):
概念:在求出所有的割点以后,把所有的割点删除,原图变成了多个连通块。
原理:在求割点的过程中就能把每个点双连通分支求出。通过栈存储双连通分支。在搜索图时,每找到一条后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足dfn[x]<=low(y),说明x是一个割点,同时把边从栈顶一个个取出,直到遇到了边(x,y),取出的这些边与其关联的点,从而组成一个点双,一般再用个容器存一下每个点双。
模板(code):
void tarjan(int x,int f){
dfn[x]=low[x]=++tim;
stk[++top]=x;
vis[x]=1;
bool flag=1;
int child=0;
SREP(i,0,E[x].size()){
int y=E[x][i];
if(y==f && flag){flag=0;continue;}
if(!dfn[y]){
child++;
tarjan(y,x);
chkmin(low[x],low[y]);
if(low[y]>=dfn[x]){
cut[x]=1;
V[++tot].clear();
do{
vis[stk[top]]=0;
Id[stk[top]]=tot;
V[tot].pb(stk[top]);
}while(y!=stk[top--]);
V[tot].pb(x);
solve();//解决该点双上的问题
}
}
else if(vis[y])chkmin(low[x],dfn[y]);
}
if(!f && child==1)cut[x]=0;
}
边双(边双连通分量):
概念:在求出所有的割边以后,把所有的割边删除,原图变成多个连通块。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
原理:与点双同理,在求割边的过程中就能把每条边双连通分支求出,也是通过栈,不过此时自然是存边了,而不是点,之后栈的弹出边界与点双也是同理的,这里就不再赘述。
模板(code):
void tarjan(int x,int fa){
dfn[x]=low[x]=++tim;
LREP(x){
int y=E[i].y;
if(E[i].flag||dfn[y]>=dfn[x])continue;
E[i].flag=E[i^1].flag=1;
stk[++top]=i;
if(!dfn[y]){
tarjan(y,x);
low[x]=min(low[x],low[y]);
if(low[y]>dfn[x]){
tot++;
do{
int k=stk[top];
if(mark[E[k].y]!=tot){
em[++cnt]=make_pair(tot,E[k].y);
mark[E[k].y]=tot;
}
if(mark[E[k^1].y]!=tot){
em[++cnt]=make_pair(tot,E[k^1].y);
mark[E[k^1].y]=tot;
}
Id[k>>1]=tot;
}while(i!=stk[top--]);
}
}
else low[x]=min(low[x],dfn[y]);
}
}
tarjan是一个非常使用且易记忆的算法,将一张有环图转换为DAG或一棵树后,
也就变成了一个十分常规的问题了,然后就可以再运用合理的数据结构或算法将其迎刃而解!