首页 > 代码库 > Tarjan三把刀
Tarjan三把刀
搞过OI的对tarjan这个人大概都不陌生。这个人发明了很多神奇的算法,在OI届广被采用。
他最广泛采用的三个算法都是和$dfn$,$low$相关的。
有向图求强连通分量
其实说直白点,就是缩点。用得比较多的就是把一个有向有环图变为一个DAG。然后利用DAG的一些神奇的性质求解一些常见的问题。
核心代码如下:
void tarjan(int node){ vis[node]=1;dfn[node]=low[node]=++dfs_clock; stack[++top]=node; for(int i=LINK[node];i;i=e[i].next) if(!dfn[e[i].y]){ tarjan(e[i].y); low[node]=min(low[node],low[e[i].y]); }else if(vis[e[i].y]) low[node]=min(low[node],dfn[e[i].y]); if(dfn[node]==low[node]){ group_num++; int tmp; do{ tmp=stack[top--]; vis[tmp]=0; group[tmp]=group_num; }while(tmp!=node); }}
每次执行完后,$group$值相同的就是强连通分量里的。同时$group_num$也代表这里有几个强连通分量。
需要注意的是,这个只适用于有向图的缩点建图。如果以这个为模板特判父亲然后妄想把无向图缩成一棵树就是大错特错,至于有向图的缩点请参考下面的点双联通分量
无向图求割点/割边
求割点和割边其实无差,如果一个点存在一条边为割边,那么这个点一定是割点.
相应的,对于点$node$的儿子$son$,如果$dfn[node] \leq low[son] $,那么$edge_{(node,son)}$就是一条割边,而$node$也就是相应的割点。
特殊的,如果$node$为$root$,且只有一个儿子,也满足上述条件,但显然$node$不是割点,所有需要特判。
void tarjan(int node,int father){ vis[node]=1;dfn[node]=low[node]=++dfs_clock; for(int i=LINK[node];i;i=e[i].next)if(e[i].y!=father){ if(!dfn[e[i].y]){ outdu[node]++; tarjan(e[i].y,node); cmin(low[node],low[e[i].y]); if(low[e[i].y]>=dfn[node])OK[node]=1; }else if(vis[e[i].y]) cmin(low[node],dfn[e[i].y]); } if(outdu[node]==1&&node==1)OK[node]=0;}
无向图求点双联通分量
这一块相对来说比较复杂,可以去做一道题。什么是点双联通分量?直白点说就是从一个点到另一个点的一个"圈"。
之前说到了用求强连通分量的方法不能把一个图缩成树来搞,可以参考一下这张图。
如果只用那种方法,这整张图都会被缩成一个点,但是显然这个图应该是两个点双联通分量。
可以注意到,连接着两个点双联通分量的点是一个割点,因此可以借用之前思路来求解求解点双联通分量。
void tarjan(int node){ dfn[node]=low[node]=++dfs_clock; stack[++top]=node;int son=0; Auto(i,node)if(!dfn[e[i].y]){ tarjan(e[i].y); cmin(low[node],low[e[i].y]); if(low[e[i].y]>=dfn[node]){ int tmp;group_num++; do{ tmp=stack[top--]; group[group_num].pb(tmp); }while(tmp!=e[i].y); group[group_num].pb(node); } }else cmin(low[node],dfn[e[i].y]);}
乍一看好像和求强连通分量没什么区别,确实。这个只是把我们称之为缩点的过程移到了循环里。
在建树的时候也跟第一个的构建新图不是很一样。对于每个点双联通分量,新建一个点,这个点向点双联通分量里的每个点连边。最终一定能构成一棵树,而这个树上任意两点之间的路径上所包含的点(除虚点)都是在原图上必经的点。
Tarjan三把刀