常见的(我见过的)强连通分量的三种算法有:1. Kosaraju算法(双DFS)2.Tarjan算法 3.Gabow
http://acm.hdu.edu.cn/showproblem.php?pid=1269
一.Kosaraju算法
算法的核心实现是,首先DFS一遍,得到一个DFS森林,在此过程中得到所有点的拓扑序列(按结束时间由高到低),之后我们建一个反向图,按反拓扑序(结束时间由高到低)进行第二次DFS,则此时得到的每一棵树都是一个强连通分量,这个画个图演示一下比较好理解,严格证明还是参考算法导论340页较好,感性的认识是,假定我们有C1,C2两个强连通分量,而在反拓扑序中C1是在C2前面的,此时说明G中第一遍DFS时先结束了C2,C1才结束的,假设C2中的点是从C1可达的,也就是在第一遍DFS时C2中点的全部时间戳的区间位于C1的区间内,则反向时,这时当将所有边反转时C1就没有边能到C2了(前提是他俩确实是SCC),按照先C1在C2的顺序DFS就行了,这里用到了一些DFS的性质,看算法导论为好。如果它们本来就是两棵树那么饭拓扑序靠前的那棵也不可能有边到后面的。虽然本算法慢,但是它得到了一个连通分量的拓扑序,有时用的上。
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
const int M=100010;
struct node
{
int v,next;
}mp1[M],mp2[M];
int vis1[N],vis2[N];
int tot1,tot2;
int first1[N],first2[N];
int num[N],belong[N];
int cn,ans,n,m;
void init()
{
ans=tot1=tot2=cn=0;
memset(first1,-1,sizeof first1);
memset(first2,-1,sizeof first2);
memset(vis1,0,sizeof vis1);
memset(vis2,0,sizeof vis2);
}
void add(int a,int b)
{
tot1++;
mp1[tot1].v=b;
mp1[tot1].next=first1[a];
first1[a]=tot1;
tot2++;
mp2[tot2].v=a;
mp2[tot2].next=first2[b];
first2[b]=tot2;
}
void dfs1(int v)
{
vis1[v]=1;
for(int i=first1[v];i!=-1;i=mp1[i].next)
if(!vis1[mp1[i].v])
dfs1(mp1[i].v);
num[cn++]=v;
}
void dfs2(int v)
{
vis2[v]=1;
for(int i=first2[v];i!=-1;i=mp2[i].next)
if(!vis2[mp2[i].v])
dfs2(mp2[i].v);
}
int main()
{
int i,j,t,tt=0;
while(~scanf("%d%d",&n,&m),m|n)
{
init();
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
for(i=1;i<=n;i++) if(!vis1[i]) dfs1(i);
for(i=cn-1;i>=0;i--)
if(!vis2[num[i]]) dfs2(num[i]),ans++;
puts(ans==1?"Yes":"No");
}
}
二.Tarjan算法
Tarjan算法时间效率比Kosaraju好,因为算法中只使用了一次DFS,实现的思想是,我对每个点规定3个属性tim[i]和low[i]和vst[i], tim和DFS中的时间戳类似,就是只记录到达时间,low反映的是当前点通过自己的非树边(回退边)或子树中的非树边(回退边),最多能连到离根最近的点(本点须为当前点的直系祖先或当前点),在DFS到此点初始化时令tim[v] = low[v] = ++ times,这样当DFS完一个点的子树之后,发现tim[v]仍然和low[v]相等,就说明这个点和它的子树中的点组成了一个强连通分量了,在这个过程中我们可以用一个栈来辅助,在DFS一个点时就压入这个点,同时令标记inStack[i] = true,这样我们可以通过Stack来判断v点邻接中的已访问过的点是不是v的直系祖先(是直系祖先才能构成环)。在DFS完一个点后,判断tim[v]是否还与low[v]相等,相等的话说明这个点与他的子树构成了一个强连通分量。弹出stack中的点,直到本点,赋予他们同一个强连通分量标号,同时令inStack[v] = false;
我自己利用并查集实现了一下Tarjan算法,过程类似于用并查集实现双连通分量,不过因为是无向图,当前的一条分枝可能还是会指向原来已经DFS过的sbling旁支,这时我们的解决方案是用DFS的双时间标号标记起始和终止时间,根据DFS标号的区间性质就可以判v邻接的一个已访问过的点是否是直系祖先了。并查集的实现过程是一旦son没被访问且low[son] >= tim[father],merge两者。
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
int n,m;
vector<int> mp[N];
int top; // 强连通分量的个数;
int bcnt; // 栈顶;
int index; // 时间戳;
int low[N],dfn[N];
int Stack[N],belong[N],instack[N];
void tarjan(int u)
{
Stack[top++]=u;
instack[u]=1;
low[u]=dfn[u]=++index;
for(int i=0;i<mp[u].size();i++)
{
int v=mp[u][i];
if(!dfn[v]) tarjan(v),low[u]=min(low[v],dfn[u]);
else if(instack[v]) low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
++bcnt;
int v;
do{
v=Stack[--top];
instack[v]=0;
belong[v]=bcnt;
}while(u!=v);
}
}
int main()
{
int i,j,t,tt=0;
while(~scanf("%d%d",&n,&m),m|n)
{
memset(mp,0,sizeof mp);
while(m--)
{
int a,b;
scanf("%d%d",&a,&b);
mp[a].push_back(b);
}
bcnt=top=index=0;
memset(low,0,sizeof low);
memset(dfn,0,sizeof dfn);
for(i=1;i<=n;i++) if(!dfn[i]) tarjan(1);
puts(bcnt==1?"Yes":"No");
}
}
三.Gabow
其实如果说能把Gabow也单列出来作为一种算法,我那个也能算一种了,区别在于我的比Tarjan原版还慢,而Gabow好像快一点,Gabow算法使用两个栈来维护,Stk1和原来的一样,stk2则用来求强连通分量构成子树的根节点,插入过程stk1,stk2和Tarjan中的一样,遇到一个插入一个,而当有一个点的邻接点是他的祖先在栈中我们就弹出stk2中的点直到stk2[top] == 这个邻接的点,这时剩下的这个点很有可能是一个强连通分量的根节点,DFS完一个点后,看stk2[top]是否等于当前节点,若等于同样说明当前点所有子树中点最多回退到当前点,此时构成了一个强连通分量,stk2中弹出这个点,stk1操作与Tarjan中相同,对每个标记强连通分量标号即可。这样的好处是不用频繁修改low的值了,好像能快一点,实现也没比Tarjan复杂,基本一样。
HOJ 2741实测结果
无向连通图求割点和桥
tarjan算法--求无向图的割点和桥
一.基本概念
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)<low(v),也就是v向上翻不到u及其以上的点,那么u--v之间一定能够有1条或者多条边不能删去,因为他们之间有一部分无环,是桥。
如果v能上翻到u那么u--v就是一个环,删除其中一条路径后,能然是连通的。
3.注意点:
1)求桥的时候:因为边是无方向的,所以父亲孩子节点的关系需要自己规定一下,
在tarjan的过程中if(v不是u的父节点) low[u]=min(low[u],dfn[v]);
因为如果v是u的父亲,那么这条无向边就被误认为是环了。
2)找桥的时候:注意看看有没有重边,有重边的边一定不是桥,也要避免误判。
4.也可以先进行tarjan(),求出每一个点的dfn和low,并记录dfs过程中的每个点的父节点,遍历所有点的low,dfn来寻找桥和割点
三:求桥和割点的模板:
#include<iostream> using namespace std; #include<cstdio> #include<cstring> #include<vector> #define N 201 vector<int>G[N]; int n,m,low[N],dfn[N]; bool is_cut[N]; int father[N]; int tim=0; void input() { scanf("%d%d",&n,&m); int a,b; for(int i=1;i<=m;++i) { scanf("%d%d",&a,&b); G[a].push_back(b);/*邻接表储存无向边*/ G[b].push_back(a); } } void Tarjan(int i,int Father) { father[i]=Father;/*记录每一个点的父亲*/ dfn[i]=low[i]=tim++; for(int j=0;j<G[i].size();++j) { int k=G[i][j]; if(dfn[k]==-1) { Tarjan(k,i); low[i]=min(low[i],low[k]); } else if(Father!=k)/*假如k是i的父亲的话,那么这就是无向边中的重边,有重边那么一定不是桥*/ low[i]=min(low[i],low[k]); } } void count() { int rootson=0; Tarjan(1,0); for(int i=2;i<=n;++i) { int v=father[i]; if(v==1) rootson++;/*统计根节点子树的个数,根节点的子树个数>=2,就是割点*/ else{ if(low[i]>=dfn[v])/*割点的条件*/ is_cut[v]=true; } } if(rootson>1) is_cut[1]=true; for(int i=1;i<=n;++i) if(is_cut[i]) printf("%d\n",i); for(int i=1;i<=n;++i) { int v=father[i]; if(v>0&&low[i]>dfn[v])/*桥的条件*/ printf("%d,%d\n",v,i); } } int main() { input(); memset(dfn,-1,sizeof(dfn)); memset(father,0,sizeof(father)); memset(low,-1,sizeof(low)); memset(is_cut,false,sizeof(is_cut)); count(); return 0; }
POJ 1352
- #include<iostream>
- #include<cstdio>
- #include<cstring>
- #include<vector>
- #define maxn 1050
- using namespace std;
- int dfn[maxn],low[maxn]; //dfs序 和子树连接的最小节点
- int vis[maxn];
- vector<int>edge[maxn];
- int child[maxn];
- int num,son;
- void init()
- {
- memset(vis,0,sizeof(vis));
- memset(child,0,sizeof(child));
- vis[1]=1;
- num=0;
- son=0;
- }
- void Tarjan(int u)
- {
- dfn[u]=low[u]=++num;
- vis[u]=1;
- for(int i=0; i<edge[u].size(); i++)
- {
- int v=edge[u][i];
- if(!vis[v])
- {
- Tarjan(v);
- low[u]=min(low[u],low[v]);
- if(dfn[u]<=low[v]) //得到子树
- {
- if(u!=1)
- child[u]++;
- else
- son++;
- }
- }
- else low[u]=min(low[u],dfn[v]);
- }
- }
- int main()
- {
- // freopen("in.txt","r",stdin);
- int a,b;
- int Case=1;
- while(1)
- {
- while(scanf("%d",&a)&&a)
- {
- scanf("%d",&b);
- edge[a].push_back(b);
- edge[b].push_back(a);
- }
- init();
- Tarjan(1);
- // for(int i=1;i<=5;i++)
- // cout<<dfn[i]<<" "<<low[i]<<endl;
- if(Case>1)
- cout<<endl;
- printf("Network #%d\n",Case++);
- int flag=1;
- child[1]=son-1;
- for(int i=1; i<=1000; i++)
- if(child[i]>0)
- {
- flag=0;
- printf(" SPF node %d leaves %d subnets\n",i,child[i]+1);
- }
- if(flag)
- cout<<" No SPF nodes"<<endl;
- for(int i=1; i<=1000; i++)
- edge[i].clear();
- scanf("%d",&a);
- if(a==0)
- break;
- scanf("%d",&b);
- edge[a].push_back(b);
- edge[b].push_back(a);
- }
- return 0;
- }
习题:poj 2186 poj1236 poj 3352