Tarjan是个hin牛的人,他发明了缩点算法,求割点和桥balabala.................
咳咳回到正题,缩点有什么用呢?
众所周知,图论里面有个好东西叫DAG,有向无环图。但是duliu出题人不一定会给你DAG,为了完成一系列的操作然后A掉duliu题,我们需要把图变成DAG,tarjan缩点就是用来干这个的。
注意tarjan缩点只在有向图里。
why?
无向图只要不是个链就肯定会有环,缩点缩着缩着就成了一个大点qwq。
先来说几个名词:
强连通:可以互相到达的几个点,我们称这几个点强连通
强连通子图:一个图里面几个强连通的点构成的子图
强连通分量:是一个图里面极大强连通子图
注意强连通分量不唯一
举个例子
一张被用烂了的图:
这里{1,2,3,4},{5},{6}是强连通分量
我们可以弄出一颗dfs树来
从1开始叭
我们注意到这棵dfs树长的非常丑,既有孙子节点指向祖先节点的边,又有一些奇奇怪怪的边
我们把上面红色的边叫做返祖边,蓝色的边叫做横叉边。
横叉边的处理比较特殊,先不管它,先来看返祖边
有返祖边,就一定会有强连通分量
那我们可以记录当前点和它的子树通过返祖边可以到达的辈分最大的祖宗,也就是在dfs树上深度最浅那个祖宗,代表它是以那个祖宗为根的强连通分量里面的一部分
我们设dfn[i]为i是第几个被dfs到的点,也就是时间戳,low[i]就是记录i以及i的子树能到达的最浅的的祖宗的dfn。
我们可以用栈记录下来遍历到过的点,并用vis数组标记点已经在栈里面了
计算low:
我们遍历当前点u的所有出边,如果当前出边的终点v没有遍历过(dfn为0),就对v进行tarjan操作(就像dfs一样),low[u]=min{low[u],low[v]}
如果v被遍历过,而且在栈里面,那就说明v是u的一个祖先,low[u]=min{low[u],dfn[v]}
当我们回溯回u的时候,low[u]已经不会再改变了,如果这时dfn[u]=low[u],就说明u是一个强连通分量的“根”。这时把u和它的子树上的节点弹出栈,并将它们染成同种颜色,一个强连通分量就搞定了。
tarjan缩点代码:
int co[10009],sum,top,all[10009],stac[10009]; int dfn[10009],low[10009],tim; bool ins[10009]; void tarjan(int u) { tim++;//tim就是时间戳 dfn[u]=tim; low[u]=dfn[u]; stac[++top]=u; ins[u]=1; for(int e=head[u];e;e=edge[e].nxt) { int v=edge[e].to; if(!dfn[v]) { tarjan(v); low[u]=min(low[u],low[v]); } else if(ins[v]) { low[u]=min(low[u],dfn[v]); } } if(low[u]==dfn[u]) { ++sum; do { ins[stac[top]]=0; co[stac[top]]=sum; top--; }while(stac[top+1]!=u);//因为上面有个top--,所以在这里要加回去 } }
那我们缩完点,以前邻接表中边的表示方法在新图里就不再适用了,所以我们要重新建图
我们遍历以前的邻接表里所有的边,如果一条边的起点和终点不在同一个强连通分量里面,就在新图上建边
代码:
void add(int fr,int to) { cnt++; edge[cnt].fr=fr; edge[cnt].to=to; edge[cnt].nxt=head[fr]; head[fr]=cnt; } void add2(int fr,int to) { cnt++; edge2[cnt].fr=fr; edge2[cnt].to=to; edge2[cnt].nxt=head[fr]; head[fr]=cnt; } int main() { n=read();m=read(); for(int i=1;i<=m;i++) { int a=read(),b=read(); add(a,b); } for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i); int num=cnt;cnt=0; memset(head,0,sizeof(head)); for(int i=1;i<=num;i++) { int u=edge[i].fr; int v=edge[i].to; if(co[u]!=co[v])//如果这两个点不在同一个强连通分量里面,就连边 { add2(co[u],co[v]); } } // 接下来的操作 }