(转)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

思路:

  1. 最暴力的想法,x,y都向上搜索,因为向上的节点就是x或y的祖先,所以搜到的第一个公共节点就是x和y的LCA,复杂度O(QN);

  2. 一种简单的优化方法是把关于x的所有询问一起处理,这样x只需要向上搜索1次,优化一下常数;

  3. 任何题目的方法归根结底都是枚举,我们注意到之前我们是在枚举询问,规模为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的图有割点.
  2. 割点:若去掉某个节点将会使图分为若干联通分支,则改点称为割点。
  3. 边连通度:去掉最少的边使得图分为若干联通分支。只有边连通度为1的图有桥。
  4. 桥:若去掉某条边将会使图分为若干联通分支,则改边称为桥。

现实意义:

通信网络中,用来衡量系统可靠性,连通度越高,可靠性越高。

割点

  1. 暴力求解,依次删除每一个节点v,用DFS(或者BFS)判断是否连通,再把节点加入图中。若用邻接表(adjacency list),需要做V次DFS,时间复杂度为O(V∗(V+E))。 
    这个算法复杂度太高,我们需要去改进它,我们想:能否一遍DFS求解?
  2. 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是割点。

桥:

  1. 思路和求割点一样,也需要dfn数组和low数组辅助,只是不用判根,(u,v)是桥当且仅当DFN[u]< low[v],因为若u有一个子节点v,v及它的子孙所能到达的节点不超过v,及无法到u以上,那么这条树边就是桥了。
  2. 需要注意的是重边情况,若有两条边(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;
}
posted @ 2017-07-11 22:41  SXia  阅读(284)  评论(0编辑  收藏  举报