强连通分支
作者:互联网
强连通分支
有向图中,如果一个点集中所有点对之间都可以相互到达,那么这个点对组成的极大集合就叫做强联通分支。
求解强联通分支的方法这里介绍两种。
两边DFS法
主要的依据就在于,一个强联通分支中的点都是可以互相到达的,那么当我们翻转图中的边的方向后,我们就可以得到一个逆图,在这个新的图中,强联通分支不变,如果我们把强连通分支看做一个缩点,那么只要我们按照拓扑逆序遍历即可得到所有强连通分支,而如何求解拓扑排序,实际上有一种DFS遍历的方法,即DFS遍历完成节点的逆序就是拓扑序列。
这样我们只需要一遍DFS求出拓扑逆序,在按照拓扑逆序DFS遍历一遍逆图,每一次DFS扫描到的点就属于一个强连通分支。
例题
#include<bits/stdc++.h>
using namespace std;
const int N=101;
const int M=10010;
int edge[M];
int nest[M];
int last[N];
int cnt=1;
void add(int u,int v){
edge[cnt]=v;
nest[cnt]=last[u];
last[u]=cnt;
cnt++;
return;
}
int u[M];
int v[M];
int tot=1;
int vise[N];
vector<int> po;
//一遍dfs构建拓扑逆序
void dfs(int k){
vise[k]=1;
for(int i=last[k];i;i=nest[i]){
if(!vise[edge[i]]){
dfs(edge[i]);
}
}
po.push_back(k);
return;
}
int id=1;
void connect(int k){
vise[k]=id;
for(int i=last[k];i;i=nest[i]){
if(!vise[edge[i]]){
connect(edge[i]);
}
}
return;
}
int din[N];
int dou[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int a;
for(;;){
scanf("%d",&a);
if(a==0)break;
u[tot]=i;
v[tot]=a;
tot++;
add(i,a);
}
}
//先执行一遍dfs求拓扑逆序
for(int i=1;i<=n;i++){
if(!vise[i])dfs(i);
}
//否建逆图
cnt=1;
//重构的时候需要将last清0,否则不知道链表边界。
memset(last,0,sizeof(last));
for(int i=1;i<tot;i++){
add(v[i],u[i]);
}
//二遍DFS求解强联通分支
memset(vise,0,sizeof(vise));
//逆图中应该按照拓扑排序的顺序进行遍历
for(int i=po.size()-1;i>=0;i--){
if(!vise[po[i]]){
connect(po[i]);
id++;
}
}
//统计入度出度
for(int i=1;i<tot;i++){
if(vise[v[i]]==vise[u[i]])continue;
din[vise[v[i]]]++;
dou[vise[u[i]]]++;
}
int p=0;
for(int i=1;i<id;i++)if(din[i]==0)p++;
int q=0;
for(int i=1;i<id;i++)if(dou[i]==0)q++;
cout<<p<<endl;
if(id==2) cout<<0;
else cout<<max(q,p);
return 0;
}
tarjan算法求解
该方法操作更为简单,但是理解稍微困难一些,但是追根溯源后我们可以发现,其实原理也比较简单,也就是考虑到强连通分支的特点是,点集中的点可以相互到达,那么遍历到一个强连通分支后,最终这个点集合中所有点的low值只有一个点的满足dfn[x]==low[x]成立,这是因为对于扫描进入该强联通分支的点来说,它扫描到一个点后,该点还存在另外一条路径到达入口点,那么而一个强连通分支中low值只有入口点最小,所以结论成立。
问题的难点在于,我们该如何更新low值,因为交叉边的存在,我们需要判断交叉边所指的点到底可不可以到达入口点,所以我们用一个栈来维持当前所有扫描到的点,只有交叉边扫描到的点位于当前栈中时,才会更新。
例题:
#include<bits/stdc++.h>
using namespace std;
const int N=101;
const int M=10010;
int edge[M];
int nest[M];
int last[N];
int cnt=1;
void add(int u,int v){
edge[cnt]=v;
nest[cnt]=last[u];
last[u]=cnt;
cnt++;
return;
}
//执行tarjan+缩点,可以然后再缩点后的图上统计入度出度即可得到答案。
//如何执行锁点,实际上就是low值相同的设置为同一个点即可。
int dfn[N];
int low[N];
int st[N];
int top=-1;
bool vise[N];
int id=1;
int now=1;
int node[N];
void dfs(int k){
dfn[k]=low[k]=id;
id++;
st[++top]=k;
vise[k]=true;
//自己理清楚逻辑。
for(int i=last[k];i;i=nest[i]){
if(!dfn[edge[i]]){
//dfs时会压栈
//没有遍历时就遍历压栈,同时返回时更新low值
dfs(edge[i]);
low[k]=min(low[k],low[edge[i]]);
}else{
//一个圈里面的一定能遍历到,所以只有指向祖先节点才可以,否则都是不行的。
//遍历过得节点要判断是不是在一个圈中。
//如何判断是不是在一个圈中,我们使用一个栈来标志,在栈用的元素就是在同一个圈中。
if(vise[edge[i]])low[k]=min(low[k],dfn[edge[i]]);
}
}
if(low[k]==dfn[k]){
//这里不能直接对low数据进行更改
while(st[top]!=k){
node[st[top]]=now;
vise[st[top]]=false;
top--;
}
node[st[top]]=now;
vise[k]=false;
top--;
now++;
}
return ;
}
int din[N];
int dou[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++){
int v;
for(;;){
scanf("%d",&v);
if(v==0)break;
add(i,v);
}
}
//只有出度的点必须要分配协议,所以统计这个数据即可
//tarjan求解强联通分支
for(int i=1;i<=n;i++){
if(!dfn[i])dfs(i);
}
//以low值作为当前强联通分支缩点后代表的点。
for(int i=1;i<=n;i++){
//只需要遍历所有边即可
for(int j=last[i];j;j=nest[j]){
//不在同一个强联通分支,则出度+1;
if(node[edge[j]]!=node[i]){
dou[node[i]]++;
din[node[edge[j]]]++;
}
}
}
int p=0;
for(int i=1;i<now;i++)if(din[i]==0)p++;
int q=0;
for(int i=1;i<now;i++)if(dou[i]==0)q++;
cout<<p<<endl;
if(now==2) cout<<0;
else cout<<max(q,p);
return 0;
}
标签:cnt,last,int,edge,low,vise,连通分支 来源: https://blog.csdn.net/qq_37957064/article/details/115262228