tarjan找强连通分量
有向图强连通分量
在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量。
tarjan找强连通算法
算法思想
首先,一个强连通分量形成一个环,或者是一个单点。
如何知道这个环上的某个点呢?即让这个点在dfs中被遍历到两次即可。
我们知道,图可以用dfs遍历,且dfs采用了“栈”的思想,可以用栈实现对强连通分量上的点的保存。一个点及其子孙中存在极大强连通分量,当且仅当该点是其与子孙各点中dfs序最小的那个,否则,若有一个子孙在它之前被遍历到,则能构成一个更大的强连通分量。
具体实现
变量定义
dfn[N]:某个点的dfs序
low[N]:某个点以及其子孙中dfs序最小的值
color[N]:某个点所属的强连通分量的颜色
st[N]:栈,用于储存可能构成强连通分量的点
vis[N]:某个点是否在栈中
代码
void tarjan(int u)
{
dfn[u]=low[u]=++deep;
vis[u]=1;
st[++top]=u;
int sz=g[u].size();
for(int i=0;i<sz;i++)
{
int v=g[u][i];
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[v],low[u]);
}
else
{
if(vis[v])
low[u]=min(low[v],low[u]);
}
}
if(low[u]==dfn[u])
{
vis[u]=0;
color[u]=++tot;
while(st[top]!=u)
{
vis[st[top]]=0;
color[st[top]]=tot;
top--;
}
top--;
}
}
例题:[USACO06JAN]牛的舞会The Cow Prom
给你n个点,m条边,求图中所有大小大于1的强连通分量的个数
输入样例#1:
5 4
2 4
3 5
1 2
4 1
输出样例#1:
1
题解:数出强连通分量的个数,给每个强连通分量染色后,统计每种颜色的数量,输出颜色个数大于1的个数。
#include<cstdio>
#include<vector>
using namespace std;
const int maxn=10010;
int n,m;
int vis[maxn],dfn[maxn],low[maxn],color[maxn],t[maxn];
int deep;
int st[maxn],top;
int tot;
vector<int> e[maxn];
void tarjan(int u)
{
dfn[u]=++deep;
low[u]=deep;
vis[u]=1;
st[++top]=u;
int sz=e[u].size();
for(int i=0;i<sz;i++)
{
int v=e[u][i];
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])
low[u]=min(low[u],low[v]);
}
if(dfn[u]==low[u])
{
color[u]=++tot;
vis[u]=0;
while(st[top]!=u)
{
color[st[top]]=tot;
vis[st[top--]]=0;
}
top--;
}
}
int ans;
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
e[x].push_back(y);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
t[color[i]]++;
for(int i=1;i<=tot;i++)
if(t[i]>1) ans++;
printf("%d\n",ans);
return 0;
}
例题:51nod 1456 小K的技术
给n个点,m个点对(ai,bi),最初图上无边,要求连最少的边,使得满足这m个点对间ai到bi有路径相连。规定a到b有路,且b到c有路时,a到c也有路。输出最小连边数。
输入样例
4 5
1 2
1 3
1 4
2 3
2 4
输出样例
3
题解:不妨将这m对点当作m条边,建图后,当一个连通块内存在强连通分量时,该连通块内所需边数为连通块点数,若不存在强连通分量,则该连通块所需的边数为连通块点数-1.(可画图验证)
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m;
int low[maxn],dfn[maxn],tot,deep,st[maxn],top,vis[maxn],num[maxn],color[maxn],t[maxn],tag[maxn],mark[maxn];
vector<int> e[maxn];
int b;
int ans;
int fa[maxn];
int _find(int x)
{
if(fa[x]==x) return x;
return fa[x]=_find(fa[x]);
}
void unite(int x,int y)
{
int fax=_find(x),fay=_find(y);
if(fax==fay)return ;
fa[fax]=fay;
}
void tarjan(int u)
{
low[u]=dfn[u]=++deep;
vis[u]=1;
st[++top]=u;
int sz=e[u].size();
for(int i=0;i<sz;i++)
{
int v=e[u][i];
unite(u,v);
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
{
if(vis[v])
low[u]=min(low[u],low[v]);
}
}
if(low[u]==dfn[u])
{
color[u]=++tot;
vis[u]=0;
while(st[top]!=u)
{
color[st[top]]=tot;
vis[st[top--]]=0;
sz++;
}
top--;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
e[x].push_back(y);
}
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=n;i++) if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++) t[color[i]]++;
for(int i=1;i<=n;i++)
{
int fai=_find(i);
if(t[color[i]]>1) tag[fai]++;
num[fai]++;
}
for(int i=1;i<=n;i++)
{
int fai=_find(i);
if(!mark[fai])
{
mark[fai]=1;
if(tag[fai]) ans+=num[fai];
else ans+=num[fai]-1;
}
}
printf("%d\n",ans);
return 0;
}
tarjan缩点
缩点是图论中常用的技巧,当路径上贡献具有传导性时,可以将一个强连通分量缩成一个新点,因为一个强连通分量内的点可以互相到达。强连通分量内的点的个数可以通过染色记录,具有同一种颜色的点的个数即为该强连通分量内点的个数。
例题:poj2186 Popular Cows
告诉你有n头牛,m个崇拜关系,并且崇拜具有传递性,如果a崇拜b,b崇拜c,则a崇拜c,求最后有几头牛被所有牛崇拜。
Sample Input
3 3
1 2
2 1
2 3
Sample Output
1
题解:
先考虑整张图无环时(DAG)的情况,当存在一头牛被所有牛崇拜时,只需考虑出度为0的点的个数(出度为0的点的个数一定大于等于1),若为1,则有解,若大于1,则无解;
若图中有环,则用tarjan将图中强连通分量找出后,对于点u相连的点v,若他们的颜色相同,说明他们处于同一个强连通分量,视作u,v在一个超级点中,不统计在u的出度内;若他们的颜色不同,则说明这两个点属于不同的强连通分量中,即可让u的度数加1.
统计完成后,对于每种颜色(即所有强连通分量)统计度数为0的点,即可得到被所有牛崇拜的“超级点”个数。注意,若该点为点数大于1的强连通分量构成的“超级点”,则要将该强连通分量内的点数加入答案中,而不是加1.
#include<cstdio>
#include<vector>
#include<cstring>
using namespace std;
int n,m;
const int maxn=10010;
int dfn[maxn],low[maxn],cnt[maxn],color[maxn],degree[maxn],vis[maxn];
int tot;
int st[maxn],top;
int tmp,ans;
int deep;
vector<int> g[maxn];
void tarjan(int u)
{
dfn[u]=++deep;
low[u]=deep;
vis[u]=1;
st[++top]=u;
int sz=g[u].size();
for(int i=0;i<sz;i++)
{
int v=g[u][i];
if(!dfn[v])
{
tarjan(v);
low[u]=min(low[u],low[v]);
}
else
{
if(vis[v])
low[u]=min(low[u],low[v]);
}
}
if(dfn[u]==low[u])
{
color[u]=++tot;
vis[u]=0;
while(st[top]!=u)
{
color[st[top]]=tot;
vis[st[top--]]=0;
}
top--;
}
}
int main()
{
while(~scanf("%d%d",&n,&m))
{
memset(vis,0,sizeof(vis));
memset(dfn,0,sizeof(dfn));
memset(color,0,sizeof(color));
memset(degree,0,sizeof(degree));
memset(low,0,sizeof(low));
memset(cnt,0,sizeof(cnt));
memset(st,0,sizeof(st));
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y);
}
for(int i=1;i<=n;i++)
if(!dfn[i])tarjan(i);
for(int i=1;i<=n;i++)
{
int sz=g[i].size();
for(int j=0;j<sz;j++)
{
int v=g[i][j];
if(color[i]!=color[v])
degree[color[i]]++;
}
cnt[color[i]]++;
}
for(int i=1;i<=tot;i++)
if(degree[i]==0) tmp++,ans=cnt[i];
if(tmp>1) printf("0\n");
else printf("%d\n",ans);
}
return 0;
}
割点
在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。
割点的求法
由tarjan的算法过程,我们可以得知,若一个点u为割点,则其子孙中必有dfs序比其小的点v,使low[v]<low[u],在去掉这个点u后,必然让强连通分量中的环上一点去掉,则割掉后的子图不能构成强连通分量。
模板题:洛谷3388
求割点的个数和数量
#include<bits/stdc++.h>
using namespace std;
const int maxn=20010;
int low[maxn],dfn[maxn],iscut[maxn];
int n,m,ans;
vector<int> g[maxn];
int st[maxn],top;
int deep;
void tarjan(int u,int fa)
{
int child=0;
int sz=g[u].size();
dfn[u]=low[u]=++deep;
for(int i=0;i<sz;i++)
{
int v=g[u][i];
if(!dfn[v])
{
child++;
tarjan(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) iscut[u]=1;
}
else
{
if(v!=fa&&dfn[v]<dfn[u]) low[u]=min(low[u],dfn[v]);
}
}
if(fa<0&&child==1) iscut[u]=0;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;
scanf("%d%d",&x,&y);
g[x].push_back(y);
g[y].push_back(x);
}
for(int i=1;i<=n;i++)if(!dfn[i])
tarjan(i,-1);
for(int i=1;i<=n;i++) ans+=iscut[i];
printf("%d\n",ans);
for(int i=1;i<=n;i++) if(iscut[i]) printf("%d ",i);
puts("");
return 0;
}