【算法模板】图论:Tarjan算法求强连通分量

Tarjan 算法是一种高效的求有向图中所有强连通分量的方法。一个强连通分量(SCC)是一个极大子图,其中任意两个顶点之间都是可达的。

概念

强连通

在图论中,强连通通常用于描述有向图的性质。一个有向图被称为强连通的,如果对于图中的任意两个顶点 uv,都有一条从 u 到 vv 的路径,以及一条从 vu 的路径。这意味着在强连通的图中,任意两个顶点都可以互相到达。

强连通分量

强连通分量(Strongly Connected Component,简称 SCC)是有向图中的一个重要概念。强连通分量是图的一个子集,它满足以下两个条件:

  1. 强连通性:该子图中的任意两个顶点 uv 都满足有从 uv 的路径,以及从 vu 的路径。
  2. 极大性:这个子集是极大的,即不能再增加其他顶点而仍然保持强连通性。如果将其中的一个顶点去掉,子集将不再是强连通的。

换句话说,强连通分量是一个最大化的强连通子图。如果考虑一个有向图,它可能由多个强连通分量组成。每个强连通分量都是一个独立的部分,且其中的每一个顶点都可以互相到达。

缩点

在图论中,缩点(also known as contracting a graph or condensation of a graph)是将图中的强连通分量(SCCs)视为单个节点,从而将原图简化为一个DAG(有向无环图)的过程。这对于许多算法和图论问题来说是一个重要的步骤,特别是在分析有向图的结构时。缩点的过程:

  1. 寻找强连通分量(SCCs):使用Tarjan算法或Kosaraju算法找到图中的所有强连通分量。
  2. 创建新图:每个强连通分量作为新图中的一个节点。
  3. 添加边:如果原图中有从一个强连通分量到另一个强连通分量的边,那么在新图中添加对应的边。

Tarjan算法

算法思想

  1. 深度优先搜索 (DFS): 算法对图进行深度优先搜索(DFS)。
  2. 索引和低值: 为每个节点分配一个唯一的索引,并维护一个低值数组,表示该节点或其子节点能够回溯到的最小索引。
  3. 栈: 使用栈来存储当前路径上的节点,且这些节点尚未确定其所在的强连通分量。
  4. 回溯: 在DFS过程中,如果发现一个节点能够回溯到自身或其祖先,则表明找到一个强连通分量。

步骤

  1. 对每个未访问的节点,执行DFS。
  2. 在DFS过程中,为当前节点分配索引和低值,并将其压入栈中。
  3. 遍历当前节点的邻接节点,对于每一个邻接节点:
    • 如果未被访问,递归执行DFS,并更新当前节点的低值。
    • 如果在栈中,更新当前节点的低值。
  4. 如果当前节点的低值等于其索引,则从栈中弹出所有节点直到当前节点,形成一个强连通分量。

算法模板

#include <bits/stdc++.h>
using namespace std;

vector<vector<int>> tarjan(const vector<vector<int>> &graph) {
    const int n = graph.size();
    vector<vector<int>> SCCS; // 存储所有强连通分量
    vector<int> dfn(n, -1), low(n); // dfn 用于记录节点的访问顺序,low 用于记录最小可达节点
    vector<bool> insta(n, false); // 记录栈中是否存在该节点
    stack<int> sta; // 存储当前的 DFS 栈
    int time = 0; // 时间戳

    function<void(int)> dfs = [&](int x) {
        sta.push(x);
        insta[x] = true;
        dfn[x] = low[x] = time++;

        for (int y : graph[x]) {
            if (dfn[y] == -1) { // y 还未被访问
                dfs(y);
                low[x] = min(low[x], low[y]);
            } else if (insta[y]) { // y 在栈中,说明是一个回边
                low[x] = min(low[x], dfn[y]);
            }
        }

        // 如果 x 是一个强连通分量的根节点
        if (dfn[x] == low[x]) {
            vector<int> component; // 当前强连通分量
            while (true) {
                int node = sta.top();
                sta.pop();
                insta[node] = false; // 从栈中移除
                component.push_back(node); // 加入当前强连通分量
                if (node == x) break; // 如果到达根节点,则结束
            }
            SCCS.push_back(component); // 将当前强连通分量加入结果
        }
    };

    // 遍历每个节点,进行 DFS
    for (int i = 0; i < n; ++i) {
        if (dfn[i] == -1) { // 仅在未访问的节点上调用 DFS
            dfs(i);
        }
    }

    return SCCS;
}

vector<vector<int>> condenseGraph(const vector<vector<int>> &graph, const vector<vector<int>> &sccs) {
    unordered_map<int, int> scc_map; // 记录每个节点所属的强连通分量索引

    // 遍历所有强连通分量,将每个节点映射到其对应的强连通分量索引
    for (int i = 0; i < sccs.size(); ++i) {
        for (int node : sccs[i]) {
            scc_map[node] = i;
        }
    }

    // 创建一个新的图,用于存储缩点后的结果
    vector<unordered_set<int>> condensed_graph(sccs.size());

    // 遍历原图中的每个节点及其邻居节点
    for (int v = 0; v < graph.size(); ++v) {
        for (int w : graph[v]) {
            // 如果两个节点不在同一个强连通分量中,则在新图中添加一条边
            if (scc_map[v] != scc_map[w]) {
                condensed_graph[scc_map[v]].insert(scc_map[w]);
            }
        }
    }

    // 将 unordered_set 转换为 vector,以便返回结果
    vector<vector<int>> result(condensed_graph.size());
    for (int i = 0; i < condensed_graph.size(); ++i) {
        result[i].assign(condensed_graph[i].begin(), condensed_graph[i].end());
    }

    return result; // 返回缩点后的图
}

例题

B3609 图论与代数结构 701] 强连通分量

给定一张 n 个点 m 条边的有向图,求出其所有的强连通分量。第一行一个整数表示这张图的强连通分量数目。接下来每行输出一个强连通分量。

#include <bits/stdc++.h>
using namespace std;

vector<vector<int>> tarjan(const vector<vector<int>> &graph);

int main(){
    int n,m;
    cin>>n>>m;
    vector<vector<int>> G(n);
    for(int i=0;i<m;i++){
        int u,v;
        cin>>u>>v;
        --u,--v;
        if(u==v)continue;
        G[u].push_back(v);
    }
    auto sccs=tarjan(G);

    unordered_map<int, int> scc_map;
    for (int i = 0; i < sccs.size(); ++i) 
        for (int node : sccs[i]) scc_map[node] = i;
    
    vector<bool> vis(sccs.size());
    cout<<sccs.size()<<endl;
    for(int i=0;i<n;i++){
        if(vis[scc_map[i]])continue;
        vis[scc_map[i]]=true;
        auto& scc=sccs[scc_map[i]];
        sort(scc.begin(),scc.end());
        for(int i:scc)cout<<i+1<<' ';
        cout<<endl;
    }
    return 0;
}
  • 15
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值