最近一口气看见了6道TARJAN,没有一道在比赛的时候打对,感觉自己菜哭了。
tarjan这么基础的算法都没能完全掌握,,然后恶补了几天,感觉以后遇见tarjan题应该不会打不出来了(flag高高立起)
这篇博文会全面介绍tarjan的所有用途(选手自己挖掘的不算,我又不知道。):
1.求有向图的强连通分量
2.求无向图的割顶和桥
3.求无向图的双连通分量
4.判断是否为二分图(其实是dfs的改版,但是思想差不多。)
先讲1:
强连通分量就不予以介绍了,这里只讲怎么求。
tarjan和KOSARAJU不同,他不是用遍历顺序来分离出DFS树中的所有SCC的,而是通过某种手段:
考虑在一个有向连通图中,我们如果能找到一个强连通分量就立即输出的话,我们就能够分离出所有的SCC了,问题就是我们该怎么样找到SCC中的第一个点呢?或者说怎么判断这个点是不是当前SCC的第一个点呢?
其实很简单,我们可以画一个DFS树来显示一下:
在这个图中,我们从节点1开始搜索,设dfn[i]表示访问到节点i时的时间点,low[i]表示节点i及其后代能追溯的最早节点,这个low可能有点难理解,后面会解释:
我们搜的前两个点都很简单,他们都没有反向边连向自己的祖先,所以low[1]=dfn[1]=1,low[2]=dfn[2]=2。
那么问题就出现在第三个点上,所谓反向边就是图中用红色标注出来的边,他把3和1相连,所以low[3]=1,当然3的子孙(如果有的话)的low也是为1.
那么我们为什么要这样干呢?
假设我们从u节点开始遍历,那么如果u的子孙v可以回到u的祖先w,那么u,v,w肯定在一个SCC中,如果u=w则u为SCC中的第一个点否则不是(不管他是或不是,都不影响我们已经存储了这个SCC中的所有节点,即从w-v之间的所有节点),然后把当前存储在栈中的所有元素弹出即可。
标程百度。
2:
这个稍微比上面的简单一些
什么是割顶?就是把这个点去掉以后当前图从连通变为不联通
桥也是一样,只不过桥是边而割顶的点。
怎么求割顶呢?我们仍然需要一个时间戳dfn和low,示例还是用上面的那个图
首先你要知道割顶不可能无祖先且儿子只有一个(去掉了和没去掉一样),这个需要特判。
然后我们就遍历到点2。如果点2为割顶,我们需要满足什么性质呢?设v为点2的子孙如果任意一个v都没有边连到2的祖先(2不算),那么2一定是割点(显然,1和3会断开)。
那么我们如何知道2的子孙有没有边连到2的祖先呢?我们就需要low了,每一次遍历的时候用类似于红色边的边来更新low(low[i]表示i以及其子孙能够追溯到的其祖先的最小的dfn)。
如果u是割点则上述判定条件可以写成low[v]>=dfn[u]
此时有一种特殊状况,如果v的后代只能连回v自己(low[v]>dfn[u]),那么边(u,v)即为桥
代码:
procedure tarjan(x,fa:longint);
var
i,j,child:longint;
v:longint;
begin
inc(time);
inc(t);
dfn[x]:=time;
low[x]:=time;
vis[x]:=1;
stack[t]:=x;
i:=head[x];
while i<>0 do
begin
v:=go[i];
if dfn[v]=0 then
begin
inc(child);
tarjan(v,x);
low[x]:=min(low[x],low[v]);
if low[v]>=dfn[x] then iscut[x]:=true;
end
else
if (dfn[v]<dfn[x])and(v<>fa) then
low[x]:=min(low[x],dfn[v]);//反向边更新
i:=next[i];
end;
if (fa<0)and(child=1) then iscut[x]:=0;
end;
3.双连通分量
对于上面求割顶的算法如果很清晰能够随手打出来的话,双联通分量就没有什么难度了。
每一次如果访问到一个割点,那么当前栈内所有的元素都属于一个双连通分量内。直接弹出并存储即可。
记得是存储边而不是点,这很重要。
代码:
procedure tarjan(x:longint);
var
i,j:longint;
v:longint;
begin
inc(t);
inc(time);
dfn[x]:=time;
low[x]:=time;
stack[t]:=x;
vis[x]:=1;
i:=head[x];
while i<>0 do
begin
if vis1[(i+1)div 2]=0 then
if dfn[go[i]]<>0 then
begin
if vis[go[i]] then
low[x]:=min(low[x],dfn[go[i]]);
end
else begin
vis1[(i+1)div 2]:=true;
tarjan(go[i]);
low[x]:=min(low[x],go[i]);
end;
i:=next[i];
end;
if dfn[x]=low[x] then
begin
inc(cnt);
while stack[t+1]<>x do
begin
block[stack[t]]:=cnt;
vis[stack[t]]:=0;
dec(t);
end;
end;
end;
4.二分图
二分图判定:如果对于无向图G(V,E),可以把节点集分成不相交的两个部分,即X和Y=V-X,使得每条边两个端点一个在X中一个在Y中,则称G为二分图。
另一种等价说法:可以把每个节点着黑白两色,使得每条边的两个端点颜色不同。不难发现,非连通的图是二分图当且仅当它的每个连通分量都为二分图,所以我们只考虑无向连通图。
下面我们用DFS給图进行黑白二着色。首先,总是假设节点0是黑色的,如果你找到了一种让它着白色的方案,只需要把所有节点的颜色反转即可(color[i]=3-color[i])。就能让0着黑色。
代码:
function pd(x:longint):boolean;
var
i,v:longint;
begin
i:=head[x];
while i<>0 do
begin
v:=go[i];
if color[v]=color[x] then exit(false);
if color[v]=0 then
begin
color[v]:=3-color[v];
if not pd(v) then exit(false);
end;
i:=next[i];
end;
exit(true);
end;