强连通分量分解 学习笔记

S C C 强连通分量分解 SCC

一、强连通分量的定义

有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
说的通俗点就是:如果在一个图(子图)G中,任意两点都可以互相到达,那么这个图G就被称为强连通图,其中各个点都强连通,如果这个图是一个有向图的子图,而且任何其他点加入之后都会破坏强连通性,那么这个子图就被称为强连通分量
比如下面这张图

在这里插入图片描述
6、7、8号点构成一个强连通分量,9、10号点也构成一个强连通分量,其余点都独自构成一个强连通分量。
把强连通分量重的点缩成一个点之后,就可以表示出一个DAG(有向无环图),比如上图,表示成DAG之后就是这样的:

并且对于DAG我们可以对其进行拓扑排序,拓扑排序值相同的点属于同一强连通分量。

二、强连通分量的分解

对于给定的一张图,如何找出所有的强连通分量,其实只要做两次简单的DFS就可以解决问题了。
第一次以任意点为起点进行dfs,遍历所有还没被访问过的点,并且在回溯之前进行标记(后序遍历)。然后再对其他没有被访问过的点进行dfs,直到所有点都被访问过,这个dfs类似拓扑排序。
依然是对于上面的图,从1点开始进行dfs,可以得到下面这张图(选取不同的起始点,各个点的顺序标号会有不同),括号里为排序的标号
在这里插入图片描述
标号完成之后,我们把所有的边反向,变成下图的样子:
在这里插入图片描述然后从标号最大的点开始进行第二次dfs,每次DFS结束就表示找到了一个强连通分量。
因为强连通分量中的点都可以互相到达,设a、b两点在一个强连通分量中,那么对于原先的图,a能通过路径v1到达b,b能通过路径v2到达a。当把所有边反向了之后,a能通过路径v2到达b,而b能通过路径v1到达a。
标号最大的点肯定是离图的尾部最近的点,(离尾部最近的点可能会有好多个,类似拓扑序中的点),这样可以保证这个点不会向多边扩展。
其实就是在图中找环状或者网状结构,因为如果反向遍历能有路径的话,说明可以绕一圈再回到原来的点
在第二次DFS的过程中我们可以把缩点之后的各个强连通分量进行拓扑排序,(因为已经变成一个DAG了,可以进行拓扑排序),排序之后可以发现当时标号最大的点就是DAG头部的点。
最后得到的DAG如图:
在这里插入图片描述

三、CODE实现 Kosaraju算法

vector<int> G[maxn];        //邻接表 记录边
vector<int> rG[maxn];       //邻接表 记录反边
vector<int> verid;          //第一次遍历之后的顶点序号
void add_edge(int a,int b){
    G[a].push_back(b);
    rG[b].push_back(a);
}
int topoid[maxn];           //记录强连通分量分解后的DAG的各点拓扑序
int used[maxn];             //访问标记
void dfs(int cur){          //第一次dfs对各点进行类拓扑排序
    used[cur] = 1;
    for(int i=0;i<G[cur].size();i++){
        int u = G[cur][i];
        if(!used[u]) dfs(u);
    }
    verid.push_back(cur);
}
void rdfs(int cur,int k){   //第二次,反向图dfs,找出强连通分量,并标记DAG的拓扑序
    used[cur] = 1;
    topoid[cur] = k;
    for(int i=0;i<rG[cur].size();i++){
        int u = rG[cur][i];
        if(!used[u]) dfs(u,k);
    }
}
int SCC(){
    memset(used,0,sizeof(used));
    for(int i=1;i<=n;i++) if(!used[i]) dfs(i);
    int k = 0; memset(used,0,sizeof(used));
    for(int i=verid.size()-1;i>=0;i--) if(!used[verid[i]]) rdfs(verid[i],++k);
    return k;                   //返回强连通分量数
}

四、例题

  1. PKU2186 http://poj.org/problem?id=2186 入门题
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int maxn = 5e4+7;
int n,m;
vector<int> G[maxn];                            //图的邻接表储存
vector<int> rG[maxn];                           //反向图的邻接表储存
vector<int> id;                                  //后序遍历的序号对应的牛的编号
int used[maxn];
int topoid[maxn];                               //记录反向图的各点拓扑排序
void add_edge(int cowa,int cowb){
    G[cowa].push_back(cowb);
    rG[cowb].push_back(cowa);
}
void input(){
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++){
        int cowa,cowb;
        scanf("%d %d",&cowa,&cowb);
        add_edge(cowa,cowb);
    }
}
void dfs(int cur){                              //第一次DFS进行标号
    used[cur] = 1;
    for(int i=0;i<G[cur].size();i++){
        int u = G[cur][i];
        if(!used[u]) dfs(u);
    }
    id.push_back(cur);
}
void rdfs(int cur,int k){                     //反向dfs
    used[cur] = 1;
    topoid[cur] = k;
    for(int i=0;i<rG[cur].size();i++){
        int u = rG[cur][i];
        if(!used[u]) rdfs(u,k);
    }
}
int scc(){
    for(int i=1;i<=n;i++) if(!used[i]) dfs(i);
    memset(used,0,sizeof(used));
    int k = 0;
    for(int i=id.size()-1;i>=0;i--) if(!used[id[i]]) rdfs(id[i],++k);
    return k;
}
void solve(){
    int k = scc();
    int ans = 0,pos;
    for(int i=1;i<=n;i++) if(topoid[i]==k) pos=i,ans++;
    memset(used,0,sizeof(used));
    rdfs(pos,k);
    for(int i=1;i<=n;i++){
        if(!used[i]){
            ans = 0;
            break;
        }
    }
    printf("%d\n",ans);
}
int main(){
    input();
    solve();
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_43710979/article/details/89215051