强连通分量分解详解 超级详细

103 阅读4分钟

(写的有点小多,慢慢看,会有收获的)
(1)
首先我们得了解,什么是强连通?
如果在一个有向图顶点子集内,任取两个点 u 和 v ,都能找到一条路径从 u 到 v ,则称该子集为强连通

(2)
其次我们得了解,什么是强连通分量?
如果我们在一个强连通的顶点集合内,加入其他其他任意顶点集合后,它都会变得不再是强连通的,则称该顶点集合为强连通分量

(3)
最后我们得了解,什么是强连通分量分解?
任意有向图都可被分解成若干个不相交的强连通分量(这个不相交,指不同分量中顶点都是不同的),这就是强连通分量分解

注意:我们一般是对有向有环图进行强连通分量,因为有向无环图中,没有强连通分量,无向图中,所有顶点集是一个强连通分量,这种分解没有意义

(4)
那我们怎么进行强连通分量分解?

我们首先进行一次 dfs ,选取任意顶点作为起点,遍历所有尚未访问过的顶点,并在回溯前给顶点标号(后序遍历),对剩余未访问过的顶点,不断重复上述过程
这次的标号主要是为了使越接近图的尾部,顶点的标号越小,为后续操作做铺垫

怎么标号?
我们可以建立一个 vector 容器,这样我们将越接近图的尾部的,越先放入

为什么可以从任何顶点开始?
因为我们是把尾部最先放入。不管我们从哪个点开始遍历,都能遍历到当前剩余顶点集当中,最接近尾部的点,所以不管我们从哪里遍历,都能将该点及该点后面的所有点正确放入。
例如我们按 1 2 3 号顶点的顺序开始遍历,那么我们放入的点,就是圈起来的部分。
很容易看出不管从哪个点开始,都能正确完成操作
请添加图片描述

为什么需要后序遍历?(即先向下递归,回溯时在放入 vector)
因为我们是越接近图的尾部,越先放入。
如果是前序遍历就相当于越接近头部越先放入,这样会出错,因为我们从任意顶点开始遍历,比如我们按 1 2 3 的顺序进行遍历,这样我们先放的相当于是 顶点1 ,而顶点 1 并非头部,这样就会出错

我们再进行一次 dfs ,先将所有边反向,然后以标号最大的顶点为起点进行 dfs ,每次 dfs 所遍历的顶点集合就构成了一个强连通分量,拿个数组保存以下各个点属于哪个强连通分量即可

反向,其实就是记录边的时候多记录一条反向边
从标号最大的开始遍历,其实就是将 vector 从后向前遍历一次

这个的思路是这样的:
因为我们是按从头部到尾部遍历,所以我们正在遍历的只有两种情况
例:如果我们遍历到 顶点1 ,此时没有遍历过的可能是 ”旁边“ 的,或者 “后面” 的
(阴影的相当于已经遍历过)
对于“旁边”:由于上游的点已经遍历过,所以旁边的点递归不到,所以不用考虑
对于“后面”:假设 顶点v 在 顶点u 的“后面”,因为在“后面”,所以有条 u 到 v 的路径,如果把边反向后,仍然 顶点u 有路径到达 顶点v ,相当于在正向图中有一条 v 到 u 的路径,证明 u 到 v 连通

请添加图片描述

代码如下:

#include <iostream>
#include <stdio.h>
#include <vector>
#include <string.h>
using namespace std;
vector<int> data[10005];
vector<int> rdata[10005];
vector<int> flag;
int used[10005];
int kind[10005];
void add_edge(int i, int j)
{
    data[i].push_back(j);
    rdata[j].push_back(i);
}
void dfs(int v)
{
    used[v] = true;
    for(int i = 0; i < data[v].size(); i++)
    {
        if(!used[data[v][i]])
        {
            dfs(data[v][i]);
        }
    }
    flag.push_back(v);
}
void rdfs(int v, int k)
{
    used[v] = true;
    kind[v] = k;
    for(int i = 0; i < rdata[v].size(); i++)
    {
        if(!used[rdata[v][i]])
        {
            rdfs(rdata[v][i], k);
        }
    }
}
int scc()
{
    memset(used, false, sizeof(used));
    for(int i = 0; i < N; i++)
    {
        if(!used[i]) dfs(i);
    }
    memset(used, false, sizeof(used));
    int num = 0;
    for(int i = flag.size() - 1; i >= 0; i--)
    {

        if(!used[flag[i]])
        {
            rdfs(flag[i], num);
            num++;
        }
    }
	return num
}