Tarjan三大算法

Robert Endre Tarjan是一个美国计算机学家,他传奇的一生中发明了无数算法,统称为Tarjan算法。其中最著名的有三个,分别用来求解
1)有向图的强连通分量
2) 无向图的双联通分量
3) 最近公共祖先问题

一:有向图的强连通分量

算法介绍(摘自百度百科)
如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
下图中,子图{1,2,3,4}为一个强连通分量,因为顶点1,2,3,4两两可达。{5},{6}也分别是两个强连通分量。
Tarjan算法是用来求有向图的强连通分量的。求有向图的强连通分量的Tarjan算法是以其发明者Robert Tarjan命名的。Robert Tarjan还发明了求双连通分量的Tarjan算法。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
定义DFN(u)为节点u搜索的次序编号(时间戳),Low(u)为u或u的子树能够追溯到的最早的栈中节点的次序号。
当DFN(u)=Low(u)时,以u为根的搜索子树上所有节点是一个强连通分量。
接下来是对算法流程的演示。
从节点1开始DFS,把遍历到的节点加入栈中。搜索到节点u=6时,DFN[6]=LOW[6],找到了一个强连通分量。退栈到u=v为止,{6}为一个强连通分量。
返回节点5,发现DFN[5]=LOW[5],退栈后{5}为一个强连通分量。
返回节点3,继续搜索到节点4,把4加入堆栈。发现节点4向节点1有后向边,节点1还在栈中,所以LOW[4]=1。节点6已经出栈,(4,6)是横叉边,返回3,(3,4)为树枝边,所以LOW[3]=LOW[4]=1。
继续回到节点1,最后访问节点2。访问边(2,4),4还在栈中,所以LOW[2]=DFN[4]=5。返回1后,发现DFN[1]=LOW[1],把栈中节点全部取出,组成一个连通分量{1,3,4,2}。
至此,算法结束。经过该算法,求出了图中全部的三个强连通分量{1,3,4,2},{5},{6}。
可以发现,运行Tarjan算法的过程中,每个顶点都被访问了一次,且只进出了一次堆栈,每条边也只被访问了一次,所以该算法的时间复杂度为O(N+M)。

二:无向图的双联通分量

一):无向图的割点和桥

一.基本概念
1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥无向连通图中,如果删除某边后,图变成不连通,则称该边为桥。
2.割点:无向连通图中,如果删除某点后,图变成不连通,则称该点为割点。
二:tarjan算法在求桥和割点中的应用
1.割点:1)当前节点为树根的时候,条件是“要有多余一棵子树”(如果这有一颗子树,去掉这个点也没有影响,如果有两颗子树,去掉这点,两颗子树就不连通了。)
2)当前节点U不是树根的时候,条件是“low[v]>=dfn[u]”,也就是在u之后遍历的点,能够向上翻,最多到u,如果能翻到u的上方,那就有环了,去掉u之后,图仍然连通。
保证v向上最多翻到u才可以
2.桥:若是一条无向边(u,v)是桥,
1)当且仅当无向边(u,v)是树枝边的时候,需要满足dfn(u)

无向图的割点和桥模板代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;

const int MAXN = 110;
const int WHITE = 0,GREY = 1,BLACK = 2;
int map[MAXN][MAXN];
int DFN[MAXN],low[MAXN],col[MAXN];
bool vis[MAXN];
int target[MAXN];
int n,ans;
void tarjan(int now, int father, int depth){
    DFN[now] = depth;
    col[now] = GREY;
    int child = 0;
    for(int i = 1; i <= n; i++){
        if(map[now][i] == 0)continue;

        if( i != father && col[i] == GREY){// i is u'grandfather
            low[now] = min(low[now],DFN[i]);
        }
        if(col[i] == WHITE ){// i is u'child
            tarjan(i,now,depth+1);
            child++;
            low[now] = min(low[i],low[now]);
            if((now == 1 && child > 1) || (now != 1 && low[i] >= DFN[now])){//is ge dian
                target[now] = 1;
                //ans++;  //注意:需要记录该割点增加几个联通分量的操作需要在这里ans++
            }
            /*
            if(low[i] > DEN[now]){
                target[now][i] = 1;//now->i is ge edge;
            }
            */
        }
    }
    col[now] = BLACK;
}
int main(){
    while(scanf("%d",&n)&&n){
        int u,v;
        memset(map,0,sizeof(map));
        memset(col,0,sizeof(col));
        memset(target,0,sizeof(target));
        for(int i = 1; i <= n; i++){
            low[i] = 99999999;//low[i] should initilized INT_MAX;
        }
        while(scanf("%d",&u) && u){
            while(getchar() != '\n'){
                scanf("%d",&v);
                map[u][v] = map[v][u] = 1;
            }
        }
        tarjan(1,1,0);
        ans = 0;
        int cnt = 0;
        for(int i = 1; i <= n; i++){
            if(target[i] == 1) {
                    cnt++;
            }
        }
        cout<<cnt<<endl;
    }
}
/*

http://poj.org/problem?id=1144


Sample Input

5
5 1 2 3 4
0
6
2 1 3
5 4 6 2
0
0
Sample Output

1
2

*/

二):求解无向图的双连通分量

定义:
对于一个连通图,如果任意两点至少存在两条点不重复路径,则称这个图为点双连通的(简称双连通);如果任意两点至少存在两条边不重复路径,则称该图为边双连通的。点双连通图的定义等价于任意两条边都同在一个简单环中,而边双连通图的定义等价于任意一条边至少在一个简单环中。对一个无向图,点双连通的极大子图称为点双连通分量(简称双连通分量),边双连通的极大子图称为边双连通分量。这篇博客就是总结一下求解无向图点双连通分量与边双连通分量的方法。

算法:

Tarjan算法:

  1. 对图进行先深搜索,计算每一个结点v的先深标号dfn[v]。
  2. 计算所有结点v的low[v]是在先深生成树上按照后根遍历的顺序进行的。因此,当访问结点v时它的每个儿子y的low[y]已经计算完毕,这时low[v]取下面三值中最小者:

(1) dfn[v];

(2) dfn[w], 凡是有回退边(v, w)的任何结点w;

(3) low[y],对v的任何儿子y.

[1]
边双连通分量:

若一个无向图中的去掉任意一条边都不会改变此图的连通性,即不存在桥,则称作边双连通图。一个无向图中的每一个极大边双连通子图称作此无向图的边双连通分量。

连接两个边双连通分量的边即是桥。

点双连通分量:

若一个无向图中的去掉任意一个节点都不会改变此图的连通性,即不存在割点,则称作点双连通图。一个无向图中的每一个极大点双连通子图称作此无向图的点双连通分量。

注意一个割点属于多个点双连通分量。

折叠为什么点连通分量必须存边

这是初学者常见的问题,证明如下:

首先要明确边双连通分量和点双连通分量的区别与联系

1.二者都是基于无向图

2.边双连通分量是删边后还连通,而后者是删点

3.点双连通分量一定是边双连通分量(除两点一线的特殊情况),反之不一定

4.点双连通分量可以有公共点,而边双连通分量不能有公共边

由于4,显然,求解边双连通分量只需先一遍dfs求桥,在一遍dfs求点(不经过桥即可)

但如果求点双连通分量,就要更复杂:

1.如果存边

根据dfs的性质,每条边都有且只有一次入栈,而由于性质3和性质4,点双连通分量没有公共边,所以弹出这个点双连通分量里的所有边就一定包含这里面的所有点,而且一定不含其他点双连通分量的边。因此求解时只需弹出这个点双连通分量里的所有边,并记录这些边的点即可(要判重,一个点可出现多次),正确。

2.如果存点

根据dfs的性质,每个点同样有且只有一次入栈。但注意,由于性质4,你将一个点出栈后,还可能有别的点双连通分量包含它,错误。

无向图的边双联通分量模板:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <cstring>
using namespace std;
int n,m;
const int MAXN = 1004;
struct Edge{
    int next,to;
} edge[MAXN*5];
int head[MAXN];
int DFN[MAXN],low[MAXN],belong[MAXN],du[MAXN];

int ecnt = 1,ji,ans,cntt;//cntt表示边双连通分量的个数
stack<int>s;
void add(int u,int v){
    edge[ecnt].to = v;
    edge[ecnt].next = head[u];
    head[u] = ecnt++;
}
void tarjan(int x,int fa){
    low[x] = DFN[x] = ++ji;
    s.push(x);
    for(int i = head[x]; i; i = edge[i].next){
        int to = edge[i].to;
        if(to != fa){
            if(DFN[to] == -1){
                tarjan(to,x);
                low[x] = min(low[to],low[x]);
            }else{
                low[x] = min(low[x],DFN[to]);
            }
        }
    }
    if(low[x] > DFN[fa]){
        cntt++;
        while(1){
            int temp = s.top();
            s.pop();
            belong[temp] = cntt;
            if(temp == x)break;
        }
    }
}
int main(){
    scanf("%d %d",&n,&m);
    for(int i = 1; i <= n; i++){
        DFN[i] = -1;
    }
    for(int i = 1; i <= m; i++){
        int u,v;
        scanf("%d %d",&u,&v);
        add(u,v);
        add(v,u);
    }
    tarjan(1,0);
    for(int i = 1; i <= n; i++){
        for(int j = head[i]; j; j = edge[j].next){
            int to = edge[j].to;
            if(belong[to] != belong[i]){
                du[belong[to]]++;
                du[belong[i]]++;
            }
        }
    }
    for(int i = 1; i <= n; i++){
        du[i] /=2;
    }
    for(int i = 1; i <= n; i++){
        if(du[i] == 1){
            ans++;
        }
    }
    ans = (ans+1)/2;
    cout<<ans<<endl;
}
/*

poj 3352

Sample Input

Sample Input 1
10 12
1 2
1 3
1 4
2 5
2 6
5 6
3 7
3 8
7 8
4 9
4 10
9 10

Sample Input 2
3 3
1 2
2 3
1 3
Sample Output

Output for Sample Input 1
2

Output for Sample Input 2
0
*/
无向图的点双联通分量模板:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <stack>
#include <cstring>
#include <set>
using namespace std;
int n,m;
const int MAXN = 201000;//数组要够大....否则会WA
struct Edge{
    int next,to;
} edge[MAXN*2];
int head[MAXN];
int DFN[MAXN],low[MAXN],fa[MAXN];

int ecnt = 1,ji,ans1,ans2,hea;//ans1记录桥的个数,ans2记录点双中边数
struct Node{
    int u,v;
} stk[MAXN];
void add(int u,int v){
    edge[ecnt].to = v;
    edge[ecnt].next = head[u];
    head[u] = ecnt++;
}
void init(){
    memset(edge,0,sizeof(edge));
    ecnt = 1;
    ji = hea = ans1 = ans2 = 0;
    for(int i = 1; i <= n+10; i++){
        fa[i] = head[i] = low[i] = 0;
        DFN[i] = -1;
    }
}
void tarjan(int x){
    DFN[x] = low[x] = ++ji;
    for(int i = head[x]; i; i = edge[i].next){
        int to = edge[i].to;
        if(to != fa[x]){
           Node e;
           e.u = x;
           e.v = to;
           if(DFN[to] == -1){
              stk[++hea] = e;
              fa[to] = x;
              tarjan(to);
              low[x] = min(low[x],low[to]);
              if(low[to] >= DFN[x]){//找到割点
                int num(0);
                Node temp;
                set<int >s;
                while(1){
                    temp = stk[hea--];
                    num++;
                    s.insert(temp.u);
                    s.insert(temp.v);
                    if(temp.u == x && temp.v == to)break;
                }
                if(s.size() < num)ans2+=num;
              }
                if(low[to] > DFN[x])ans1++;//找到桥
           }
           else{
                if(DFN[to] < DFN[x])stk[++hea] = e;
                low[x] = min(low[x],DFN[to]);
           }
        }
    }
}

int main(){
    while(~scanf("%d %d",&n,&m)){
        if(n == 0&&m==0)break;
        init();
        for(int i = 1; i <= m; i++){
            int u,v;
            scanf("%d %d",&u,&v);
            u++,v++;
            add(u,v);
            add(v,u);
        }
        for(int i = 1; i <= n ; i++){
            if(DFN[i] == -1){
                tarjan(i);
            }
        }
        printf("%d %d\n",ans1,ans2);
    }
    return 0;
}
/*
http://acm.hdu.edu.cn/showproblem.php?pid=3394

Sample Input
8 10
0 1
1 2
2 3
3 0
3 4
4 5
5 6
6 7
7 4
5 7
0 0


Sample Output
1 5

*/

三:最近公共祖先(LCA)问题

引用块内容

LCA的tarjan算法模板:
#include<cstdio>  
#include<iostream>  
#include<cstring>  
#include<cmath>  
#include<algorithm>  
using namespace std;  
const int N = 40000+10;  
const int M = 220;  
int head[N],head1[N],dis[N],LCA[N],father[N];  
bool vis[N];  
int n,m,cnt;  
struct Edge{  
    int from,to;  
    int next;  
    int val;  
};  
struct Edge1{  
    int u,v;  
    int num;  
    int next;  
};  
Edge edge[N*2];  
Edge1 edge1[M*2];  
void add_edge(int u,int v,int val){  
    edge[cnt].from=u;  
    edge[cnt].to=v;  
    edge[cnt].val=val;  
    edge[cnt].next=head[u];  
    head[u]=cnt++;  
}  
void add_ans(int u,int v,int num){  
    edge1[cnt].u=u;  
    edge1[cnt].v=v;  
    edge1[cnt].num=num;  
    edge1[cnt].next=head1[u];  
    head1[u]=cnt++;  
}   
int find(int x){  
    //return x==father[x]?x:find(father[x]);   
    int r=x;  
    while(r!=father[r]){  
        r=father[r];  
    }  
    return r;  
//  if(x!=father[x]){  
//      x=father[x];  
//  }  
//  return father[x];  
}  
void tarjan(int k){  
    vis[k]=1;  
    father[k]=k;  
    for(int i=head1[k];i!=-1;i=edge1[i].next){  
        int v=edge1[i].v;  
        if(vis[v]){  
            LCA[edge1[i].num]=find(v);  
        }  
    }  
    for(int i=head[k];i!=-1;i=edge[i].next){  
        int to=edge[i].to;  
        if(!vis[to]){  
            dis[to]=dis[k]+edge[i].val;  
            tarjan(to);  
            father[to]=k;  
        }  
    }  
}  
int main(){  
    int T;  
    scanf("%d",&T);  
    while(T--){  
        scanf("%d%d",&n,&m);  
        int a,b,c;  
        cnt=0;  
        memset(head,-1,sizeof(head));  
        memset(dis,0,sizeof(dis));   
        for(int i=0;i<n-1;i++){  
            scanf("%d%d%d",&a,&b,&c);  
            add_edge(a,b,c);  
            add_edge(b,a,c);  
        }  
        dis[1]=0;  
        cnt=0;  
        memset(head1,-1,sizeof(head1));  
        for(int i=1;i<=m;i++){  
            scanf("%d%d",&a,&b);  
            add_ans(a,b,i);  
            add_ans(b,a,i);  
        }  
        memset(vis,0,sizeof(vis));  
        tarjan(1);  
        for(int i=1;i<=m*2;i+=2){  
            a=edge1[i].u;  
            b=edge1[i].v;  
            c=edge1[i].num;  
            printf("%d\n",dis[a]+dis[b]-2*dis[LCA[c]]);  
        }  
    }  
    return 0;  
}

猜你喜欢

转载自blog.csdn.net/jal517486222/article/details/79332008
今日推荐