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("");
}
}
相关文章推荐
- 【UVA - 796 Critical Links 】(求桥 Tarjan算法)
- uva 796 Critical Links 割边的数量
- UVA796- Critical Links(无向图的桥)
- UVa 796 - Critical Links
- UVA - 796
- 【日常学习】【模拟双向链表】【疑问】Uva12657 - Boxes in a Line题解
- UVA127—加油学习努力向前
- [kuangbin带你飞]专题九 连通图 Critical Links UVA - 796
- UVA315 && UVA 796 (求割点和桥,模版)
- UVa 1252该类的子集的二进制表示方法还需要学习
- 【日常学习】【二叉树遍历】Uva548 - Tree题解
- UVA - 796 Critical Links (无向图求割边(桥))
- UVA 796 Critical Links
- 【日常学习】【二叉树遍历】Uva548 - Tree题解
- ACM学习历程——UVA540 Team Queue(队列,map:Hash)
- UVA 796 Critical Links 求桥 .
- UVA 796 Critical Links(无向图求桥)
- 【割边 && 桥】UVA - 796 Critical Links
- UVA - 796 Critical Links (割边 tarjan )
- Uva 796 Critical Links 找桥