您的位置:首页 > 其它

tarjan算法的学习 uva12167,uva315,uva796

2017-10-19 22:20 363 查看

图的强连通&tarjan算法

强连通图:如果有向图G中的任意两个点u,v是互相可到达的,则称图G为强连通图。否则G为非强连通图。

强连通分量:若有向图G为非强连通图,它的子图G' 是强连通图,则称G' 为G的强连通分量。(极大强连通子图)

返图:将有向图G中的所有边的方向逆置,即u->v变为v->u

定理:对于一个有向图G,按照dfs的后序遍历到的点,对图G的返图进行一次dfs,就能得到其中一个强联通分量。首先,对原图dfs能连通这些点,然后对返图也能dfs到的点,是一个强连通分量。

求强连通分量的作用 :

把有向图中具有相同性质的点找出来,形成一个集合(缩点),建立缩图,能够方便地进行其它操作,而且时间效率会大大地提高,原先对多个点的操作可以简化为对它们所属的缩点的操作。 求强连通分量常常用于求拓扑排序之前,因为原图往往有环,无法进行拓扑排序,而求强连通分量后所建立的缩图则是有向无环图,方便进行拓扑排序。 缩点操作在这篇中用过(最小树形图):点击打开链接

以下主要内容:

kosaraju算法求强连通分量

tarjan算法求强连通分量、割点、桥

一、kosaraju算法。

1、基本思路

先对原图进行一次深搜,记录下访问的后序遍历顺序,即每个点在dfs的离开时间,可用栈存储。然后按照离开时间进行第二次深搜,搜返图,在第一次深搜中能到达的点,若在返图的深搜中也能到达,说明这些点是一个强联通分量。

【代码 hdu1269】:

[cpp] view
plain copy

<span style="font-size:18px;">#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <iostream>

#include <algorithm>

#define mset(a,i) memset(a,i,sizeof(a))

using namespace std;

const int MAX=1e5+5;

struct node{

int s,t,next;

}e[MAX<<3];

int head[MAX],head2[MAX],cnt;//存原图,返图

bool vis[MAX];//标记访问的节点

int sta[MAX],top;//记录dfs后序

int Scc[MAX];//可以记录每个节点的分量归属

void add(int u,int v)//建图

{

e[cnt]=node{u,v,head[u]};

head[u]=cnt++;

e[cnt]=node{v,u,head[v]};

head2[v]=cnt++;//返图

}

void init()

{

mset(head,-1);

mset(head2,-1);

cnt=0;

}

void dfs1(int u)

{

vis[u]=1;

for(int i=head[u];~i;i=e[i].next)

{

int t=e[i].t;

if(!vis[t])dfs1(t);

}

sta[top++]=u;//后序

}

void dfs2(int u,int sig)

{

vis[u]=1;

Scc[u]=sig;

for(int i=head2[u];~i;i=e[i].next)

{

int t=e[i].t;

if(!vis[t])dfs2(t,sig);

}

}

int kosaraju(int n)

{

top=0;

mset(vis,0);

for(int i=1;i<=n;i++)

{

if(!vis[i])dfs1(i);

}//第一次dfs跑原图记离开时间

int sig=0;

mset(vis,0);

for(int i=top-1;i>=0;i--)

{

if(!vis[sta[i]])

{

dfs2(sta[i],++sig);//连通分支+1

}

}//第2次dfs跑返图求连通分量数

return sig;

}

int main()

{

int n,m,u,v;

while(cin>>n>>m,n)

{

init();

while(m--)

{

scanf("%d%d",&u,&v);

add(u,v);

}

int ans=kosaraju(n);

if(ans==1)puts("Yes");

else puts("No");

}

return 0;

}</span>

二、tarjan算法。

1、强连通分量(有向图)

详解:http://blog.csdn.net/justlovetao/article/details/6673602

增加两个数组,dfn[]表是dfs到达每个节点的时间。low[]记录当前点或其子树中的点能到达的拥有最小时间戳的点。

若一个点dfs过程中碰到了已访问过的点,说明形成环,dfs回溯回去,回到这个环中时间戳最小的点,

一定有dfn[ u ] == low[ u ],从该点开始将栈中它的后续点依次出栈,即为一个连通分量

【代码 uva12167】:

给定有向图,问需要加几条边可以强连通?先用tarjan算强连通数,然后用缩点法计算连通块之间的入度、出度,缺多少。取max(in, out)

[cpp] view
plain copy

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<iostream>

#include<algorithm>

#include<map>

using namespace std;

const int MAX=202020;

struct node{

int s,t,next;

}e[MAX];

int head[MAX],cnt;

void add(int u,int v)

{

e[cnt]=node{u,v,head[u]};

head[u]=cnt++;

}

int dfn[MAX];//每个节点的访问时间编号

int low[MAX];//每个点能到达的最小编号

int sta[MAX],top;

int Scc[MAX];//每个点所属的分量 序号

int in[MAX],out[MAX];

void tardfs(int u,int &lay,int &sig)

{

low[u]=dfn[u]=lay++;//到达此点的时间

sta[top++]=u;//压入栈

for(int i=head[u];~i;i=e[i].next)

{

int t=e[i].t;

if(dfn[t]==0)

{

tardfs(t,lay,sig);

low[u]=min(low[u],low[t]);

}

else if(!Scc[t])//访问过了

low[u]=min(low[u],dfn[t]);//强连通求法可以用low

}

if(low[u]==dfn[u])//u不能到达任何之前走过的点

{

sig++;

while(1)

{

int j=sta[--top];//出栈

Scc[j]=sig;

if(j==u)break;

}//包含u的连通分量出栈

}

}

int tarjan(int n)

{

int sig=0;//强连通数

int lay=1;//时间戳

top=0;

memset(Scc,0,sizeof(Scc));

memset(dfn,0,sizeof(dfn));//时间戳归0

for(int i=1;i<=n;i++)

{

if(!dfn[i])tardfs(i,lay,sig);

}

return sig;//返回连通数

}

int main()

{

int n,m,u,v,T;

cin>>T;

while(T--)

{

cin>>n>>m;

memset(head,-1,sizeof(head));

cnt=0;

while(m--)

{

scanf("%d%d",&u,&v);

add(u,v);

}

int sig=tarjan(n);

if(sig==1)puts("0");

else

{

memset(in,0,sizeof(in));

memset(out,0,sizeof(out));

for(int i=1;i<=n;i++)//缩点统计度

{

for(int j=head[i];~j;j=e[j].next)

if(Scc[i]!=Scc[e[j].t])

in[Scc[e[j].t]]=out[Scc[i]]=1;

}

int a=0,b=0;

for(int i=1;i<=sig;i++)

{

if(in[i]==0)a++;

if(out[i]==0)b++;//没出度,需要加边

}

cout<<max(a,b)<<endl;

}

}

}

2、割点与割边(割顶&桥)(无向图)

割顶:若去掉一个点和与这个点相连的边后,图不再连通,则这个点是割顶。

基本思路:dfn数组记录时间戳,去更新low数组代表的可到达的祖先最低时间戳。

若满足low[v] >= dfn[u],说明u的子树中v点不能到达u的祖先,因此u点是割点。

桥:去掉这条边,图不再连通。

若满足low[v] > dfn[u],说明u的子树中v点不能到达u及其祖先,因此边<u,v>是割边

例题,uva 315: tarjan算法求割点

割点需要特判根节点是否是割点,方法是统计根节点在dfs时得到的子树数目child,若>1说明root是割点。

【代码uva315】:

[cpp] view
plain copy

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<iostream>

#include<algorithm>

using namespace std;

const int MAX=202020;

struct node{

int s,t,next;

}e[MAX];

int head[MAX],cnt;

void add(int u,int v)

{

e[cnt]=node{u,v,head[u]};

head[u]=cnt++;

}

int dfn[MAX];//每个节点的访问时间编号

int low[MAX];//每个点能到达的最小编号

bool iscut[MAX];//标记割点

void tardfs(int u,int &lay,int fa)

{

low[u]=dfn[u]=lay++;//到达此点的时间

int child=0;//子树数目

for(int i=head[u];~i;i=e[i].next)

{

int t=e[i].t;

if(t==fa)continue;

if(dfn[t]==0)

{

tardfs(t,lay,u);

low[u]=min(low[u],low[t]);

child++;

if(u!=1&&low[t]>=dfn[u])//满足子树回不到祖先

iscut[u]=1;

}

else //祖先

low[u]=min(low[u],dfn[t]);//此处是dfn!不是low,因为low可能已被更新,就跨过了割点

}

if(u==1&&child>1)iscut[1]=1;

}

int tarjan(int n)

{

int sig=0;//割点数

int lay=1;//时间戳

memset(iscut,0,sizeof(iscut));

memset(dfn,0,sizeof(dfn));//时间戳归0

for(int i=1;i<=n;i++)

{//若无向图连通,只进入1一次就全部tarjan出来了

if(!dfn[i])tardfs(i,lay,-1);

}

for(int i=1;i<=n;i++)

if(iscut[i])sig++;

return sig;//返回割点数

}

int main()

{

int n,m,q,u,v;

while(cin>>n,n)

{

memset(head,-1,sizeof(head));

cnt=0;

while(scanf("%d",&u),u)

{

do{

scanf("%d",&v);

add(u,v);

add(v,u);

}while(getchar()!='\n');

}

cout<<tarjan(n)<<endl;

}

}

例题,uva796 求桥数并输出

题意是求出桥的数目并输出。

【代码uva796】

[cpp] view
plain copy

#include<stdio.h>

#include<stdlib.h>

#include<string.h>

#include<iostream>

#include<algorithm>

using namespace std;

const int MAX=202020;

struct node{

int s,t,next;

}e[MAX];

int head[MAX],cnt;

void add(int u,int v)

{

e[cnt]=node{u,v,head[u]};

head[u]=cnt++;

}

int dfn[MAX];//每个节点的访问时间编号

int low[MAX];//每个点能到达的最小编号

bool bri[MAX];//标记桥

node ans[MAX];

void tardfs(int u,int &lay,int fa)

{

low[u]=dfn[u]=lay++;//到达此点的时间

for(int i=head[u];~i;i=e[i].next)

{

int t=e[i].t;

if(t==fa)continue;

if(dfn[t]==0)

{

tardfs(t,lay,u);

low[u]=min(low[u],low[t]);

if(low[t]>dfn[u])//满足子树回不到祖先和自己

bri[i]=1;//标记桥

}

else //祖先

low[u]=min(low[u],dfn[t]);//此处是dfn!不是low,因为low可能已被更新,就跨过了割点

}

}

int tarjan(int n)

{

int bridge=0;//桥数

int lay=1;//时间戳

memset(bri,0,sizeof(bri));

memset(dfn,0,sizeof(dfn));//时间戳归0

for(int i=1;i<=n;i++)

{//若无向图连通,只进入1一次就全部tarjan出来了

if(!dfn[i])tardfs(i,lay,-1);

}

for(int i=0;i<cnt;i++)

if(bri[i])bridge++;

return bridge;//返回桥

}

bool cmp(node a,node b)

{

if(a.s==b.s)return a.t<b.t;

return a.s<b.s;

}

int main()

{

int n,m,q,u,v;

while(cin>>n)

{

memset(head,-1,sizeof(head));

cnt=0;

for(int i=1;i<=n;i++)

{

scanf("%d (%d)",&u,&q);u++;

while(q--)

{

scanf("%d",&v);v++;

add(u,v);

add(v,u);

}

}

printf("%d critical links\n",tarjan(n));//桥数

int t=0;

for(int i=0;i<cnt;i++)//对标记的边筛选排序并输出

{

if(bri[i])

{

if(e[i].s<e[i].t)

ans[t++]=node{e[i].s,e[i].t};

else ans[t++]=node{e[i].t,e[i].s};

}

}

sort(ans,ans+t,cmp);

for(int i=0;i<t;i++)

printf("%d - %d\n",ans[i].s-1,ans[i].t-1);

puts("");

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: