求解强连通分量有两种算法,tarjan和kosaraju
首先来俩概念:
强连通:在有向图G中,若两个点u、v是互相可达的,则称u和v是连通的。如果G中任意两个点都是互相可达的,称G是强连通图。
强连通分量:如果一个有向图G不是强连通图,那么可以把它分成多个子图,其中每个子图的内部是强连通的,而且这些子图已经扩展到最大,不能与子图外的任意点强连通,称这样一个“极大强连通”子图是G的一个强连通分量(Strongly Connected Component)
有个常见的问题:求G中有多少个scc?
我们先来尝试着把每个scc抽象成一个个的群岛,群岛由有着很多或很少的小岛组成,群岛内部连通;群岛与其他群岛之间有单向道路连接,且不会形成环路。然后将每一个群岛虚拟成一个个点,最后得到一个有向无环图GG,GG中点的数量即为scc数量(还能推出:每个群岛挖掉后,并不会影响其他岛内部的连通性)
那么一开始想到的一定是暴力!!!(暴力不可取。。。)
暴力思路:对每个点求连通性,然后进行比较,互相连通的点就组成了scc(dfs或bfs可实现)
。。。时间复杂度:
下面介绍Kosaraju和Tarjan算法(时间复杂度:)
Kosaraju
原理:
1、将有向图G的所有边反向构建反图rG,rG不会改变原图G的强连通性。(人话:G的scc数量和rG的数量相同)
2、对G和rG各做一次dfs,可以确定scc数量
有一个我之前特别困惑的点:dfs干啥用的?
——对虚拟点进行拓扑排序,排序后将优先级最高的虚拟点进行反dfs,就可以求出被群岛与外界隔离的那些小岛了
模版在下面
Tarjan
Kosaraju是在图中一个一个将scc给挖出来,而Tarjan能在一次dfs就将所有点按照scc分开
其实理解了Kosaraju,来理解Tarjan也不是很难(个人意见)
有几个变量需要注意:
low[i]:表示能返回到的最远祖先
num[i]:表示第几次循环遍历到i(不太重要的样子)
步骤:首先dfs,赋值每个点的low,然后判断low是否与该点的num值相等,如果相等,那么这个点就是自己的祖先点。额。就没了。。(是不是挺简单的~~)
下面上题目::
B3609 [图论与代数结构 701] 强连通分量
一道模版题,下面依次提供Kosaraju和Tarjan两种做法:
Kosaraju::
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
vector<int>g[N],scc[N];
stack<int>st;
int n,m;
int f[N];
int low[N];
int num[N];
int dfn,cnt;
void dfs(int u)
{
dfn++;
low[u]=num[u]=dfn;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!num[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!f[v])
low[u]=min(low[u],num[v]);
}
if(low[u]==num[u])
{
cnt++;
while(!st.empty())
{
int k=st.top();
f[k]=cnt;
st.pop();
scc[cnt].push_back(k);
if(u==k)
break;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g[u].push_back(v);
}
memset(low,0,sizeof(low));
memset(num,0,sizeof(num));
memset(f,0,sizeof(f));
cnt=dfn=0;
for(int i=1;i<=n;i++)
if(!num[i])
dfs(i);
cout<<cnt<<endl;
for(int i=1;i<=n;i++)
sort(scc[i].begin(),scc[i].end());
for(int i=1;i<=n;i++)
if(num[f[i]])
{
for(int j=0;j<scc[f[i]].size();j++)
cout<<scc[f[i]][j]<<" ";
cout<<endl;
num[f[i]]=0;
}
return 0;
}
Tarjan::
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int m,n;
vector<int> g[N];
stack<int> st;
int low[N],num[N],f[N];
int dfn,cnt;
int ans;
int out[N];
int mmp[N];
void dfs(int u)
{
dfn++;
low[u]=num[u]=dfn;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!num[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!f[v])
low[u]=min(low[u],num[v]);
}
if(low[u]==num[u])
{
cnt++;
while(!st.empty())
{
int k=st.top();
st.pop();
f[k]=cnt;
mmp[cnt]++;
if(u==k)
break;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g[u].push_back(v);
}
memset(num,0,sizeof(num));
memset(low,0,sizeof(low));
memset(f,0,sizeof(f));
cnt=dfn=0;
for(int i=1;i<=n;i++)
if(!num[i])
dfs(i);
for(int i=1;i<=n;i++)
{
for(int j=0;j<g[i].size();j++)
if(f[i]!=f[g[i][j]])
out[f[g[i][j]]]++;
}
for(int i=1;i<=cnt;i++)
if(!out[i])
ans++;
cout<<ans<<endl;
}
P2341 [USACO03FALL / HAOI2006] 受欢迎的牛 G
稍微带了点变化,最终是求在只有一个点入度为0的强连通图内点的数目,如果有多个点入度为0,则直接输出0(说明有某点不能连通)
代码:(Kosaraju做法)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
vector<int> g1[N],g2[N];
stack<int> st;
int n,m;
int in[N];
int f[N];
int cnt;
int mmp[N];
int ans;
int num;
bool vis[N];
void dfs1(int u)
{
vis[u]=true;
for(int i=0;i<g1[u].size();i++)
if(!vis[g1[u][i]])
dfs1(g1[u][i]);
st.push(u);
}
void dfs2(int u)
{
if(vis[u])
return ;
f[u]=cnt;
num++;
vis[u]=true;
for(int i=0;i<g2[u].size();i++)
if(!vis[g2[u][i]])
dfs2(g2[u][i]);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g1[u].push_back(v);
g2[v].push_back(u);
}
for(int i=1;i<=n;i++)
if(!vis[i])
dfs1(i);
memset(vis,false,sizeof(vis));
while(!st.empty())
{
int k=st.top();
st.pop();
if(!vis[k])
{
num=0;
cnt++;
dfs2(k);
mmp[cnt]=num;
}
}
for(int i=1;i<=n;i++)
for(int j=0;j<g2[i].size();j++)
if(f[i]!=f[g2[i][j]])
in[f[g2[i][j]]]++;
for(int i=1;i<=cnt;i++)
{
if(in[i]==0)
//入度为0的点,f[i]代表编号,需要找f[i]相同的累加
{
if(ans)
{
cout<<0<<endl;
return 0;
}
ans=mmp[i];
}
}
cout<<ans<<endl;
return 0;
}
Tarjan(做法):
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int m,n;
vector<int> g[N];
stack<int> st;
int low[N],num[N],f[N];
int dfn,cnt;
int ans;
int in[N];
int mmp[N];
void dfs(int u)
{
dfn++;
low[u]=num[u]=dfn;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!num[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!f[v])
low[u]=min(low[u],num[v]);
}
if(low[u]==num[u])
{
cnt++;
while(!st.empty())
{
int k=st.top();
st.pop();
f[k]=cnt;
mmp[cnt]++;
if(u==k)
break;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g[u].push_back(v);
}
memset(num,0,sizeof(num));
memset(low,0,sizeof(low));
memset(f,0,sizeof(f));
cnt=dfn=0;
for(int i=1;i<=n;i++)
if(!num[i])
dfs(i);
for(int i=1;i<=n;i++)
{
for(int j=0;j<g[i].size();j++)
if(f[i]!=f[g[i][j]])
in[f[i]]++;
}
for(int i=1;i<=cnt;i++)
{
if(in[i]==0)
{
if(ans)
{
cout<<0<<endl;
return 0;
}
ans=mmp[i];
}
}
cout<<ans<<endl;
}
P2002 消息扩散
本题最后求出度为0的scc有多少个
代码:(Kosaraju)
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
vector<int> g1[N],g2[N];
stack<int> st;
int n,m;
int in[N],out[N];
int f[N];
int cnt;
int mmp[N];
int ans;
int num;
bool vis[N];
void dfs1(int u)
{
vis[u]=true;
for(int i=0;i<g1[u].size();i++)
if(!vis[g1[u][i]])
dfs1(g1[u][i]);
st.push(u);
}
void dfs2(int u)
{
if(vis[u])
return ;
f[u]=cnt;
num++;
vis[u]=true;
for(int i=0;i<g2[u].size();i++)
if(!vis[g2[u][i]])
dfs2(g2[u][i]);
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g1[u].push_back(v);
g2[v].push_back(u);
}
for(int i=1;i<=n;i++)
if(!vis[i])
dfs1(i);
memset(vis,false,sizeof(vis));
while(!st.empty())
{
int k=st.top();
st.pop();
if(!vis[k])
{
num=0;
cnt++;
dfs2(k);
mmp[cnt]=num;
}
}
for(int i=1;i<=n;i++)
for(int j=0;j<g2[i].size();j++)
if(f[i]!=f[g2[i][j]])
out[f[i]]++;
for(int i=1;i<=cnt;i++)
if(!out[i])
ans++;
cout<<ans<<endl;
return 0;
}
P2863 [USACO06JAN] The Cow Prom S
群岛内小岛数>1的scc个数
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int m,n;
vector<int> g[N];
stack<int> st;
int low[N],num[N],f[N];
int dfn,cnt;
int ans;
int out[N];
int mmp[N];
void dfs(int u)
{
dfn++;
low[u]=num[u]=dfn;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!num[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!f[v])
low[u]=min(low[u],num[v]);
}
if(low[u]==num[u])
{
cnt++;
while(!st.empty())
{
int k=st.top();
st.pop();
f[k]=cnt;
mmp[cnt]++;
if(u==k)
break;
}
}
}
int main()
{
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;
cin>>u>>v;
g[u].push_back(v);
}
memset(num,0,sizeof(num));
memset(low,0,sizeof(low));
memset(f,0,sizeof(f));
cnt=dfn=0;
for(int i=1;i<=n;i++)
if(!num[i])
dfs(i);
for(int i=1;i<=cnt;i++)
if(mmp[i]>1)
ans++;
cout<<ans<<endl;
}
P2746 [USACO5.3] 校园网Network of Schools
A任务:入度为0的scc个数
B任务:入度为0和出度为0的scc个数的较大值
代码:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int m,n;
vector<int> g[N];
stack<int> st;
int low[N],num[N],f[N];
int dfn,cnt;
int ans1,ans2;
int out[N],in[N];
void dfs(int u)
{
dfn++;
low[u]=num[u]=dfn;
st.push(u);
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i];
if(!num[v])
{
dfs(v);
low[u]=min(low[u],low[v]);
}
else if(!f[v])
low[u]=min(low[u],num[v]);
}
if(low[u]==num[u])
{
cnt++;
while(!st.empty())
{
int k=st.top();
st.pop();
f[k]=cnt;
if(u==k)
break;
}
}
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int v;
while(1)
{
cin>>v;
if(v==0)
break;
g[i].push_back(v);
}
}
memset(num,0,sizeof(num));
memset(low,0,sizeof(low));
memset(f,0,sizeof(f));
cnt=dfn=0;
for(int i=1;i<=n;i++)
if(!num[i])
dfs(i);
for(int i=1;i<=n;i++)
{
for(int j=0;j<g[i].size();j++)
if(f[i]!=f[g[i][j]])
{
out[f[i]]++;
in[f[g[i][j]]]++;
}
}
ans1=0;
ans2=0;
for(int i=1;i<=cnt;i++)
{
if(!out[i])
ans2++;
if(!in[i])
ans1++;
}
cout<<ans1<<endl;
if(cnt==1)
cout<<0<<endl;
else
cout<<max(ans1,ans2)<<endl;
}
有兴趣建议看看P1073。。。
完结撒花~~~~~~