其他分享
首页 > 其他分享> > 强连通分支

强连通分支

作者:互联网

强连通分支

有向图中,如果一个点集中所有点对之间都可以相互到达,那么这个点对组成的极大集合就叫做强联通分支。
求解强联通分支的方法这里介绍两种。

两边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