【图论】Tarjan算法

Tarjan算法:强连通分量识别利器

1. 基本概念

1.1 强连通分量(Strongly Connected Component, SCC)

在有向图中,如果两个顶点 u 和 v 之间存在从 u 到 v 的路径,也存在从 v 到 u 的路径,则称 u 和 v 是强连通的。一个有向图的极大强连通子图称为强连通分量。

1.2 Tarjan 算法的核心思想

利用 DFS 遍历图,通过维护每个节点的两个关键值来识别强连通分量:

  • dfn[u]:节点 u 的 DFS 访问序号(时间戳)
  • low[u]:以 u 为根的搜索子树中,能通过后向边追溯到的最早节点的 dfn 值

2. 算法原理

2.1 关键数据结构

vector<vector<int>> graph;  // 邻接表表示的图
vector<int> dfn;            // 访问时间戳
vector<int> low;            // 能追溯到的最早时间戳
vector<bool> inStack;       // 节点是否在栈中
stack<int> st;              // 存储当前 DFS 路径上的节点
int timestamp;              // 时间戳计数器

2.2 核心逻辑

  1. 从任意未访问节点开始 DFS
  2. 为当前节点分配时间戳,入栈
  3. 遍历所有邻接节点:
    • 未访问:递归处理,更新 low 值
    • 已访问且在栈中:更新 low 值
    • 已访问但不在栈中:跳过
  4. 如果 dfn[u] == low[u],说明找到一个强连通分量

3. C++ 实现

#include <iostream>
#include <vector>
#include <stack>
#include <algorithm>
using namespace std;

class TarjanSCC {
private:
    vector<vector<int>> graph;
    vector<int> dfn, low;
    vector<bool> inStack;
    stack<int> st;
    int timestamp;
    vector<vector<int>> sccs;  // 存储所有强连通分量

    void tarjan(int u) {
        // 为节点 u 分配时间戳
        dfn[u] = low[u] = ++timestamp;
        st.push(u);
        inStack[u] = true;

        // 遍历所有邻接节点
        for (int v : graph[u]) {
            if (!dfn[v]) {
                // 节点 v 未被访问过,递归处理
                tarjan(v);
                low[u] = min(low[u], low[v]);
            } else if (inStack[v]) {
                // 节点 v 已被访问且在栈中,更新 low 值
                low[u] = min(low[u], dfn[v]);
            }
            // 如果 v 已被访问但不在栈中,说明 v 属于其他 SCC,跳过
        }

        // 如果 dfn[u] == low[u],说明找到了一个 SCC 的根节点
        if (dfn[u] == low[u]) {
            vector<int> scc;
            int w;
            // 从栈中弹出属于当前 SCC 的所有节点
            do {
                w = st.top();
                st.pop();
                inStack[w] = false;
                scc.push_back(w);
            } while (w != u);
            sccs.push_back(scc);
        }
    }

public:
    TarjanSCC(int n, const vector<vector<int>>& edges) {
        // 构建邻接表
        graph.resize(n);
        for (const auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
        }
        
        // 初始化数据结构
        dfn.assign(n, 0);
        low.assign(n, 0);
        inStack.assign(n, false);
        timestamp = 0;
    }

    vector<vector<int>> findSCCs() {
        sccs.clear();
        
        // 对每个未访问的节点进行 Tarjan 算法
        for (int i = 0; i < graph.size(); i++) {
            if (!dfn[i]) {
                tarjan(i);
            }
        }
        
        return sccs;
    }
};

// 测试函数
void testTarjan() {
    // 构建测试图
    // 图结构:
    // 0 -> 1 -> 2 -> 3
    // ^         |    |
    // |         v    v
    // +---------4    5
    vector<vector<int>> edges = {
        {0, 1}, {1, 2}, {2, 3}, {3, 5},
        {2, 4}, {4, 0}, {4, 5}
    };
    
    int n = 6;  // 节点数 0-5
    
    TarjanSCC solver(n, edges);
    vector<vector<int>> sccs = solver.findSCCs();
    
    cout << "找到 " << sccs.size() << " 个强连通分量:" << endl;
    for (int i = 0; i < sccs.size(); i++) {
        cout << "SCC " << i + 1 << ": ";
        for (int node : sccs[i]) {
            cout << node << " ";
        }
        cout << endl;
    }
}

int main() {
    testTarjan();
    return 0;
}

4. 算法复杂度分析

  • 时间复杂度:O(V + E)

    • 每个节点和边只被访问一次
    • DFS 遍历的时间复杂度
  • 空间复杂度:O(V)

    • 存储 dfn、low、inStack 数组:O(V)
    • 递归栈深度:O(V)
    • 结果存储:最坏情况下 O(V²)

5. 算法正确性证明

Tarjan 算法的正确性基于以下关键性质:

  1. 追溯性质low[u] 正确反映了以 u 为根的子树中能追溯到的最早节点
  2. SCC 根性质:当 dfn[u] == low[u] 时,u 是其所在 SCC 的"根"节点
  3. 栈性质:栈中从 u 到栈顶的节点恰好构成以 u 为根的 SCC

6. 应用场景

  1. 图的简化:将有向图缩点为 DAG
  2. 2-SAT 问题:判断布尔表达式的可满足性
  3. 网络分析:识别网络中的紧密连接子群
  4. 编译器优化:循环检测和优化
  5. 社交网络分析:发现紧密联系的用户群体

7. 算法变种

Tarjan 算法还可用于:

  • 求解无向图的割点和桥
  • 求解双连通分量
  • 求解 2-SAT 问题
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值