(转)Tarjan应用
以下转载自:http://hi.baidu.com/lydrainbowcat/item/f8a5ac223e092b52c28d591c
基本概念:
1.割点:若删掉某点后,原连通图分裂为多个子图,则称该点为割点。
2.割点集合:在一个无向连通图中,如果有一个顶点集合,删除这个顶点集合,以及这个集合中所有顶点相关联的边以后,原图变成多个连通块,就称这个点集为割点集合。
3.点连通度:最小割点集合中的顶点数。
4.割边(桥):删掉它之后,图必然会分裂为两个或两个以上的子图。
5.割边集合:如果有一个边集合,删除这个边集合以后,原图变成多个连通块,就称这个点集为割边集合。
6.边连通度:一个图的边连通度的定义为,最小割边集合中的边数。
7.缩点:把没有割边的连通子图缩为一个点,此时满足任意两点之间都有两条路径可达。
注:求块<>求缩点。缩点后变成一棵k个点k-1条割边连接成的树。而割点可以存在于多个块中。
8.双连通分量:分为点双连通和边双连通。它的标准定义为:点连通度大于1的图称为点双连通图,边连通度大于1的图称为边双连通图。通俗地讲,满足任意两点之间,能通过两条或两条以上没有任何重复边的路到达的图称为双连通图。无向图G的极大双连通子图称为双连通分量。
Tarjan算法的应用论述:
1.求强连通分量、割点、桥、缩点:
对于Tarjan算法中,我们得到了dfn和low两个数组,
low[u]:=min(low[u],dfn[v])——(u,v)为后向边,v不是u的子树;
low[u]:=min(low[u],low[v])——(u,v)为树枝边,v为u的子树;
下边对其进行讨论:
若low[v]>=dfn[u],则u为割点,节点v的子孙和节点u形成一个块。因为这说明v的子孙不能够通过其他边到达u的祖先,这样去掉u之后,图必然分裂为两个子图。这样我们处理点u时,首先递归u的子节点v,然后从v回溯至u后,如果发现上述不等式成立,则找到了一个割点u,并且u和v的子树构成一个块。
void tarjan(int x)
{
v[x]=1,dfn[x]=low[x]=++num;
for(int i=head[x];i;i=next[i])
if(!v[ver[i]])
{
tarjan(ver[i]);
low[x]=min(low[x],low[ver[i]]);
if(dfn[x]<=low[ver[i]]) v[x]++;
}
else low[x]=min(low[x],dfn[ver[i]]);
if((x==1&&v[x]>2)||(x>1&&v[x]>1)) v[x]=2; else v[x]=1;//v[x]=2表示该点为割点,注意其中第一个点要特判
}
若low[v]>dfn[u],则(u,v)为割边。但是实际处理时我们并不这样判断,因为有的图上可能有重边,这样不好处理。我们记录每条边的标号(一条无向边拆成的两条有向边标号相同),记录每个点的父亲到它的边的标号,如果边(u,v)是v的父亲边,就不能用dfn[u]更新low[v]。这样如果遍历完v的所有子节点后,发现low[v]=dfn[v],说明u的父亲边(u,v)为割边。
void tarjan(int x)
{
vis[x]=1;
dfn[x]=low[x]=++num;
for(int i=head[x];i;i=next[i])
if(!vis[ver[i]])
{
p[ver[i]]=edge[i];//记录父亲边
tarjan(ver[i]);
low[x]=min(low[x],low[ver[i]]);
}
else if(p[x]!=edge[i])//不是父亲边才更新
low[x]=min(low[x],dfn[ver[i]]);
if(p[x]&&low[x]==dfn[x]) f[p[x]]=1;//是割边
}
2.求双连通分量以及构造双连通分量:
对于点双连通分支,实际上在求割点的过程中就能顺便把每个点双连通分支求出。建立一个栈,存储当前双连通分支,在搜索图时,每找到一条树枝边或后向边(非横叉边),就把这条边加入栈中。如果遇到某时满足DFS(u)<=Low(v),说明u是一个割点,同时把边从栈顶一个个取出,直到遇到了边(u,v),取出的这些边与其关联的点,组成一个点双连通分支。割点可以属于多个点双连通分支,其余点和每条边只属于且属于一个点双连通分支。
对于边双连通分支,求法更为简单。只需在求出所有的桥以后,把桥边删除,原图变成了多个连通块,则每个连通块就是一个边双连通分支。桥不属于任何一个边双连通分支,其余的边和每个顶点都属于且只属于一个边双连通分支。
一个有桥的连通图,如何把它通过加边变成边双连通图?方法为首先求出所有的桥,然后删除这些桥边,剩下的每个连通块都是一个双连通子图。把每个双连通子图收缩为一个顶点,再把桥边加回来,最后的这个图一定是一棵树,边连通度为1。
统计出树中度为1的节点的个数,即为叶节点的个数,记为leaf。则至少在树上添加(leaf+1)/2条边,就能使树达到边二连通,所以至少添加的边数就是(leaf+1)/2。具体方法为,首先把两个最近公共祖先最远的两个叶节点之间连接一条边,这样可以把这两个点到祖先的路径上所有点收缩到一起,因为一个形成的环一定是双连通的。然后再找两个最近公共祖先最远的两个叶节点,这样一对一对找完,恰好是(leaf+1)/2次,把所有点收缩到了一起。
3.求最近公共祖先(LCA)
在遍历到u时,先tarjan遍历完u的子树,则u和u的子树中的节点的最近公共祖先就是u,并且u和【u的兄弟节点及其子树】的最近公共祖先就是u的父亲。注意到由于我们是按照DFS顺序遍历的,我们可用一个color数组标记,正在访问的染色为1,未访问的标记为0,已经访问到即在【u的子树中的】及【u的已访问的兄弟节点及其子树中的】染色标记为2,这样我们可以通过并查集的不断合并更新,通过getfather实现以上目标。
void tarjan(int x)
{
fa[x]=x,color[x]=1;
int i,y;
for(i=head[x];i;i=next[i])
if(color[y=ver[i]]==0)
{
tarjan(y);
fa[y]=x;
}
for(i=headquery[x];i;i=nextquery[i])
if(color[y=query[i]]==2) ans[i]=get(y);
color[x]=2;
}
以下转自LHQ神犇http://blog.csdn.net/lhq_er/article/details/74942801
http://blog.csdn.net/lhq_er/article/details/74911231
简介
Tarjan作为一位算法大师,发明了许多算法。本篇博文介绍一下Tarjan框架下的求解树上LCA(最近公共祖先)的离线算法,复杂度O(N+Q)。以及求割点,桥的算法,复杂度O(V+E),即dfs 1次所需的时间。
Tarjan_LCA
Problem:
一棵树,n个节点,Q组询问(x,y),求x,y的LCA
思路:
-
最暴力的想法,x,y都向上搜索,因为向上的节点就是x或y的祖先,所以搜到的第一个公共节点就是x和y的LCA,复杂度O(QN);
-
一种简单的优化方法是把关于x的所有询问一起处理,这样x只需要向上搜索1次,优化一下常数;
-
任何题目的方法归根结底都是枚举,我们注意到之前我们是在枚举询问,规模为O(Q),一般Q是O(N^2)级别的,所以我们似乎可以换一下思路,去枚举一下答案,也就是LCA,这个的规模只是O(N)。——————————在观察我们发现u的不同子树中的节点的LCA是u,这样,一个想法就形成了。现在让我们来看一下算法导论中完整版的dfs:
//CLRS 中文版 p350 dfs模板;
dfs(G)
{
for each vertex u in G.v
u.color=WHITE
u.pre=NIL
time=0
for each vertex u in G.v
if u.color==WHITE
dfs_visit(G,u);
}
dfs_visit(G,u)
{
time=time+1
u.d=time
u.color=GRAY
for each v in G:Adj[u]
if v.color==WHITE
v.pre=u
dfs_visit(G,v)
u.color=BLACK
time=time+1
u.f=time
}
请读者仔细阅读这段伪代码,这几乎是在全面的前提下最精简的版本了,少了任何一句话都不行,尤其是其中的“白”“灰”“黑”三种颜色与时间戳的概念,在很多高级算法中是必不可少的,也是很多高级算法创造根源,Tarjan_LCA算法在这个基础上就很容易理解。
来个例子,本例子中,x1 < x2 < x3 < x4 < x5 < x6
画出时间戳可以发现u,v的时间戳不相交,这样我们可以轻易得出以下定理:
- 当出现“灰黑灰”时,x为u向上第一个灰点,则path(u,x)上的点和v的LCA为x。
简单来说就是上面的直觉,x下一颗子树u内节点与另外子树中节点的LCA均为x
这样的性质让我们想到了一种数据结构——并查集
废话不多说,上CLRS伪代码:
LCA(u)
MAKE_SET(u);
FIND_SET(u).ancestor=u;
for each child v of u in T
LCA(v)
UNION(u,v)
FIND_SET(u).ancestor=u
u.color=BLACK
for each node v such that (u,v) in P
if v.color==BLACK
print "The least common ancestor of"
u "and" v "is" FIND_SET(v).ancestor
http://www.cnblogs.com/JVxie/p/4854719.html
这个网站有比较详细的模拟过程,可以看一看加深理解
http://blog.csdn.net/jarily/article/details/8947928
再转一份比较清晰的解释:
*算法思想:
*Tarjan_LCA离线算法;
*Tarjan算法基于dfs的框架,对于新搜到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每个子树进行搜索;
*每搜索完一棵子树,则可确定子树内的LCA询问都已解决,其他的LCA询问的结果必然在这个子树之外;
*这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先;
*之后继续搜索下一棵子树,直到当前结点的所有子树搜完;
*
*这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问;
*如果有一个从当前结点到结点v的询问,且v已经被检查过;
*则由于进行的是dfs,当前结点与v的最近公共祖先一定还没有被检查;
*而这个最近公共祖先的包含v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先;
*
*算法步骤:
*对于每一个结点:
*(1)建立以u为代表元素的集合;
*(2)遍历与u相连的结点v,如果没有被访问过,对于v使用Tarjan_LCA算法,结束后将v的集合并入u的集合;
*(3)对于与u有关的询问(u,v),如果v被访问过,则结果就是v所在集合的代表元素;
习题:
模板题:
1.poj 1330
2.http://acm.zjnu.edu.cn/CLanguage/showproblem?problem_id=1232 交通运输线
算法介绍
Tarjan_割点
- 适用范围:无向图
- 功能:给定无向图G=(V,E),求出一个点集合V’,包含图中所有的割点。
- 时间复杂度:O(N+E),N为图中点数,E为图中边数。
Tarjan_桥
- 适用范围:无向图
- 功能:给定无向图G=(V,E),求出一个边集合E’,包含图中所有的桥。
- 时间复杂度:O(N+E),N为图中点数,E为图中边数。
算法讲解
一些概念:
- 点连通度:去掉最少的点使得图分为若干联通分支。只有点连通度为1的图有割点.
- 割点:若去掉某个节点将会使图分为若干联通分支,则改点称为割点。
- 边连通度:去掉最少的边使得图分为若干联通分支。只有边连通度为1的图有桥。
- 桥:若去掉某条边将会使图分为若干联通分支,则改边称为桥。
现实意义:
通信网络中,用来衡量系统可靠性,连通度越高,可靠性越高。
割点
- 暴力求解,依次删除每一个节点v,用DFS(或者BFS)判断是否连通,再把节点加入图中。若用邻接表(adjacency list),需要做V次DFS,时间复杂度为O(V∗(V+E))。
这个算法复杂度太高,我们需要去改进它,我们想:能否一遍DFS求解? - Tarjan算法
我们不难发现:一个顶点u是割点,当且仅当满足(1)或(2)
(1) u为根节点,且u有多于一个子树。
(2) u为非根节点,u有一个子节点s,且没有任何从s或s的后代节点指向v的真祖先的后向边。
对于根节点我们可以进行特判,那么非根节点我们如何处理呢?
思路:
我们定义DNF[v]为v结点的入时间戳,即根据dfs序给节点进行编号,定义LOW[v]为v及v的子孙所能达到的最小节点的DFN,那么判定v是否是节点就很方便了,只要u有一个儿子v,使得DNF[u]< =LOW[v],则u是割点。
桥:
- 思路和求割点一样,也需要dfn数组和low数组辅助,只是不用判根,(u,v)是桥当且仅当DFN[u]< low[v],因为若u有一个子节点v,v及它的子孙所能到达的节点不超过v,及无法到u以上,那么这条树边就是桥了。
- 需要注意的是重边情况,若有两条边(1,2),(2,1),那么都不是桥但是若只有一条(1,2),则是桥。但是在处理的时候若只按照(v!=fa)判断,这两条边只算了一条,我们需要的是不重复计算同一条边 ,那么如何判断是不是同一条边呢?链式前向星为我们提供了一种方法,因为边存储时同一条边的序号是(1,2),(3,4),……这样下去的,若((i+1)/2==(j+1)/2)则i,j是同一条边,这样就判断出来了。
CODE
割点模板:
// luogu P3388
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100010,MAXM=100010;
int Head[MAXN],Next[MAXM*2],To[MAXM*2];
bool vis[MAXN],cutv[MAXN];
int dfn[MAXN],low[MAXN];
int n,m,tot,tim,root,rootson;
void add_eage(int x,int y)
{
tot++;
Next[tot]=Head[x];
Head[x]=tot;
To[tot]=y;
}
void ReadInfo()
{
scanf("%d%d",&n,&m);
tim=tot=0;
for (int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add_eage(x,y); add_eage(y,x);
}
}
void Tarjan(int u,int pre)
{
dfn[u]=low[u]=++tim;
vis[u]=true;
for (int i=Head[u];i;i=Next[i])
{
int v=To[i];
if (!vis[v])
{
Tarjan(v,u);
low[u]=min(low[u],low[v]);
if (u!=root && dfn[u]<=low[v]) cutv[u]=true;
else if (u==root && ++rootson==2) cutv[u]=true;
}
else if (v!=pre) low[u]=min(low[u],dfn[v]);
}
}
void solve()
{
memset(dfn,-1,sizeof(dfn));
memset(low,-1,sizeof(low));
memset(vis,false,sizeof(vis));
memset(cutv,false,sizeof(cutv));
for (int i=1;i<=n;i++)
if (!vis[i])
{
root=i;
rootson=0;
Tarjan(i,0);
}
}
void Outit()
{
int num=0;
for (int i=1;i<=n;i++)
if (cutv[i]) num++;
printf("%d\n",num);
for (int i=1;i<=n;i++)
if (cutv[i]) printf("%d ",i);
printf("\n");
}
int main()
{
ReadInfo();
solve();
Outit();
return 0;
}
桥模板(可处理重边):
#include<bits/stdc++.h>
using namespace std;
const int MAXN=100010,MAXM=100010;
int Head[MAXN],Next[MAXM*2],To[MAXM*2];
bool vis[MAXN];
int dfn[MAXN],low[MAXN];
int n,m,tot,tim,num_cutedge;
struct Edge{
int u,v;
}cutedge[MAXM];
void add_eage(int x,int y)
{
tot++;
Next[tot]=Head[x];
Head[x]=tot;
To[tot]=y;
}
void ReadInfo()
{
memset(Head,0,sizeof(Head));
scanf("%d%d",&n,&m);
tim=tot=0;
for (int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
add_eage(x,y); add_eage(y,x);
}
}
void Tarjan(int u,int id)
{
dfn[u]=low[u]=++tim;
vis[u]=true;
for (int i=Head[u];i;i=Next[i])
{
int v=To[i];
if (!vis[v])
{
Tarjan(v,i);
low[u]=min(low[u],low[v]);
if (dfn[u]<low[v]) cutedge[++num_cutedge]=(Edge){u,v};
}
else if ((i+1)/2!=(id+1)/2) low[u]=min(low[u],dfn[v]);
}
}
void solve()
{
memset(dfn,-1,sizeof(dfn));
memset(low,-1,sizeof(low));
memset(vis,false,sizeof(vis));
num_cutedge=0;
for (int i=1;i<=n;i++)
if (!vis[i]) Tarjan(i,0);
}
void Outit()
{
printf("the number of the bridges is %d\n",num_cutedge);
for (int i=1;i<=num_cutedge;i++)
printf("%d %d\n",cutedge[i].u,cutedge[i].v);
}
int main()
{
ReadInfo();
solve();
Outit();
return 0;
}