Kosaraju算法找强连通分量(SCC)

223 阅读2分钟

Kosaraju算法找强连通分量(SCC)

强连通分量SCC

在有向图中,如果某个点集中任意两点u,v都满足u可以到达v,v也可以到达u,则该点集为一个强连通分量.

注意:

  1. 强连通分量可以是一个点,也可以是多个点.
  2. 若几个点形成环,一定属于同一个强连通分量.

Kosaraju算法核心步骤

  1. 对原图dfs.任取一点dfs,返回时入栈,直至所有点都被访问过.
  2. 构建反图,将所有边反转.
  3. 在反图上按栈的顺序dfs,一次dfs访问到的所有点构成一个强连通分量.

原理(个人理解):

  • 为什么两次dfs?

强连通块中的点必须从任意一点出发dfs能到达任意一点.第一次正向dfs把某个方向上能到达的点放在一起(栈中连续摆放),相当于将点分类,去除了这个方向上就不满足的点.第二次dfs反向进行,确保另一个方向上也能到达.

  • 为什么第一次dfs时要在返回时入栈,而不是访问时入栈?

返回时入栈相当于后序遍历,保证这个点和这个点的子图都检查完毕,该点以下的点都能访问到.入栈顺序按访问时间降序排列,越后入栈的点越"重要",保证了我们在反图上从"最重要的点"(拓扑顺序最靠后的)开始第二次 DFS,从而找出整个强连通分量.

代码示例

图的基本框架

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_NODES 1000

typedef struct Node{
    int vertex;
    struct Node* next;
} Node;

// 添加边
void addEdge(Node* g[],int v1,int v2) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->vertex = v2;
    newNode->next = g[v1];
    g[v1] = newNode;
}

// 清理内存
void freeGraph(Node* graph[], int n) {
    for (int i = 0; i < n; i++) {
        Node* current = g[i];
        while (current != NULL) {
            Node* temp = current;
            current = current->next;
            free(temp);
        }
        g[i] = NULL;
    }
}

int main() {
    int n = 5;  // 顶点个数

    addEdge(g, 0, 2);  // 样例
    addEdge(g, 2, 1);
    addEdge(g, 1, 0);
    addEdge(g, 0, 3);
    addEdge(g, 3, 4);

    kosaraju(n);

    freeGraph(g, n);
    freeGraph(revg, n);
    return 0;
}

Kosaraju算法部分

// 正向dfs,建立栈
void dfs1(int v) {
    vis[v] = 1;        // 标记为已访问
    Node* tmp = g[v];  // 遍历v的出点
    while (tmp) {
        if (!vis[tmp->vertex]) {
            dfs1(tmp->vertex);  // 递归dfs
        }
        tmp = tmp->next;
    }
    stack[++top] = v;  // 返回时入栈,逆序为访问顺序
}

// 反向dfs,输出强连通分量
void dfs2(int v) {
    vis[v] = 1;        // 标记为已访问
    printf("%d ", v);  // 该点在这个强连通块中

    Node* tmp = revg[v];  // 遍历反图中v的出点
    while (tmp) {
        if (!vis[tmp->vertex]) {
            dfs2(tmp->vertex);  // 递归dfs
        }
        tmp = tmp->next;
    }
}

void kosaraju(int n) {
    // 第一次dfs
    memset(vis, 0, sizeof(vis));
    for (int i = 0; i < n; i++) {
        if (!vis[i]) {
            dfs1(i);
        }
    }

    // 构建反图
    for (int i = 0; i < n; i++) {
        Node* tmp = g[i];
        while (tmp) {
            addEdge(revg, tmp->vertex, i);
            tmp = tmp->next;
        }
    }

    // 反向dfs
    memset(vis, 0, sizeof(vis));
    printf("strongly connected components: \n");
    while (top != -1) {
        int v = stack[top--];
        if (!vis[v]) {
            dfs2(v);
            printf("\n");
        }
    }
}

时间复杂度

步骤操作时间复杂度
1正向 DFS 构建栈O(V + E)
2图反转O(V + E)
3反向 DFS 查找SCCO(V + E)
总计O(V + E)