[双连通分量] tarjan算法

本文介绍了无向图中的割点与桥的概念,并详细解释了如何利用DFS算法求解割点和桥的方法,同时给出了基于DFS算法的Tarjan算法实现双连通分量的求解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在前面说的话

其实这个借鉴了网上的一些教程/总结,但是主要还是看lrj的蓝书并有部分引用以及自己的一些理解而成的,仅仅是为了给自己或他人总结用的,而不希望用于任何其他的用途亦或是被说抄袭。

引入

首先先来看几个概念
割点(割顶、关节点),在一个无向图中,如果删除了某一个点,能使连通分量的个数增加, 那么称这个点为割点
特殊的情况:对于一个连通图,割点就是在删除以后能让原图不再连通的点
桥,在一个无向图中,删除了一条边,能使连通分量个数增加,则这条边为桥
那么首先先来看看如何在一个无向图中求出割点
暴力
暴力显然是首先想到的,在一个无向图中,尝试删除每一个点(边),然后做一次dfs检查连通分量的个数是否增加。
但很显然这种办法很慢,预计的时间复杂度应为 O(n(n+m))
lrj蓝书上的基于DFS的做法
其实这种做法并不一定是lrj首创,但在这里也没有去找原作者,而lrj也没有提,或许是人人皆知的做法吧(其实也可能是tarjan算法的基础)
首先这个算法需要时间戳,于是这里用 pre 记录第一次访问的时间戳
那么DFS会产生一个DFS树,在这棵树中,又有几个概念要认识
首先是树边,就是DFS树上的边,但是除了树边还会有反向边,即由后代连回祖先的边。
那么考虑出现什么情况才能证明是割点呢。先考虑一棵树的树根,显然,当树根下有多个子树时,它会是割点,但当只有一棵子树时,它不是割点。
然后考虑下面的点,这里有定理
非根节点 u 是图G的割点时,当且仅当u存在一个子节点及该子节点的后代不存在连向 u 的祖先的反向边。证明
很显然,如果u的任何一个子节点都能够连接到 u 的祖先,那么砍掉u,它依然连通,如果有一个或是更多子节点不能连回 u 的祖先而只是连回u,那么这一棵子树在 u 被去掉后会成为一个单独的连通块,因此u是一个割点。
干说不好懂,那么上图。
这里写图片描述
那么显然地,如果在v以及其子树中,最多连回 u ,那么在u被砍掉之后,这块就独立了2333
试想,如果连回了 u 的祖先,那么这棵dfs树,可以在u被删除后,沿着这条反向边将这一块变成另外一棵子树,所以原图并没有增加连通分量,即 u 不是割点。
那么如果用low(u)来表示从 u 及其后代中能够通过反向边连回的最前的祖先,也就是pre值最小的那个
那么这个定理可以简写为当 u 存在一个子节点v,使得 low(v)pre(u) 时, u 为割点
那么在这里其实还有一种特殊情况,当low(v)>pre(u), 也就是连u都没连回,可以脑补一下上面的图,把那条反向边连到 v 上,我们可以发现这是u v <script type="math/tex" id="MathJax-Element-28">v</script>之间的边其实就是原图的一个桥
代码还是比较容易写的,而且在后面求双连通分量的tarjan算法的思路基本是一致的。

双连通分量

那么依旧,先来看看定义
什么是双连通分量呢?
就是说,在一个连通图中,任意两个点都能通过至少两条不经过同一个点的路径而互相到达,就说明它是点双连通的。
同样举几个栗子例子
这里写图片描述
我们暂且称这样的图叫铁三角好了23333
在这个图中1和2可以通过直接到达和走经过3的弯路而到达,1和3,2和3同理,所以这是一个点双连通的图,点双连通的判定条件比较复杂,这就涉及到我们的tarjan算法了,但是我们先放一放,看看边双连通。
如果是边双连通呢,那要求就更加简单,只要两条路径之间没有公共的边即可,就算经过同一个点也没关系。因此我们发现,只要一个图是点双连通的,就一定是边双连通的,那么判定就更加简单,只要是没有桥的一个图就好了。
那么双连通分量就是一个无向图中的双连通的一个极大子图,只要多画了几个图就会发现,割点总是多个点双连通分量的公共点,而边双连通分量则不会有公共点。
所以tarjan算法的基本思路就是,用找割点的办法, 凡是找到了一个割点,并且在这一棵子树会在这个点被去掉之后单独独立出来的,就是一个点双连通分量。因此这里用一个栈来保存访问过的边,直到发现了就不停地出栈,直到访问回这条边的时候就停止出栈,因为前面的边就是和祖先是一个双连通分量或是独立的了。那么代码和前面求割点的基本一致,只是在dfs处稍微多加了几句,还是比较好写的。

参考代码
#include <stack>
#include <cstdio>
#include <vector>
#include <cstring>
#include <algorithm>

using namespace std;

const int MAXN = 1010,
          MAXM = 100010;

struct Edge {
    int u, v;
    int ne;
} e[MAXM * 2];

int pre[MAXN], bcc_cnt, dfs_clock, bccno[MAXN];
// 用bcc_cnt来记录有多少个双连通分量 bccno则是每一个点处于哪一个双连通分量中,对于割点bccno无意义,储存的是最后一个双连通分量的编号
bool iscut[MAXN];

int head[MAXN];

vector  bcc[MAXN];
stack  S;

int dfs(int u, int fa) {
    int lowu = pre[u] = ++dfs_clock;
    int child = 0;

    for(int i = head[u]; ~i; i = e[i].ne) {
        int v = e[i].v;
        if(!pre[v]) { // 未访问 
            S.push(e[i]);
            child++;
            int lowv = dfs(v, u);
            lowu = min(lowu, lowv);
            if(lowv >= pre[u]) { // 如果找到了双连通分量 
                iscut[u] = 1;
                ++bcc_cnt;
                bcc[bcc_cnt].clear();
                for(;;) { // 出栈
                    Edge x = S.top();
                    S.pop();
                    if(bccno[x.u] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.u);
                        bccno[x.u] = bcc_cnt;
                    }
                    if(bccno[x.v] != bcc_cnt) {
                        bcc[bcc_cnt].push_back(x.v);
                        bccno[x.v] = bcc_cnt;
                    }
                    if(x.u == u && x.v == v) {
                        break;
                    }
                }
            }
        } else {
            if(pre[v] < pre[u] && v != fa) {
                S.push(e[i]);
                lowu = min(lowu, pre[v]); // 用反向边更新自己
            }
        }
    }

    if(fa < 0 && child == 1) {
        iscut[u] = 0;
    }
    return lowu;
}

void find_bcc(int n) {
    memset(pre, 0, sizeof pre);
    memset(iscut, 0, sizeof iscut);
    memset(bccno, 0, sizeof bccno);
    dfs_clock = bcc_cnt = 0;
    for(int i = 0; i < n; ++i) {
        if(!pre[i]) {
            dfs(i, -1);
        }
    }
}

int main(void) {
    int n, m;
    int cnt = 0;
    memset(head, -1, sizeof head);
    scanf("%d%d", &n, &m);
    for(int i = 0; i < m; ++i) {
        int u, v;
        scanf("%d%d", &u, &v);
        e[cnt].u = u;
        e[cnt].v = v;
        e[cnt].ne = head[u];
        head[u] = cnt++;
        e[cnt].u = v;
        e[cnt].v = u;
        e[cnt].ne = head[v];
        head[v] = cnt++;
    }
    find_bcc(n);
    printf("%d\n", bcc_cnt);
    for(int i = 1; i <= bcc_cnt; ++i) {
        printf("%d : ", i);
        for(int j = 0; j < bcc[i].size(); j++) {
            printf("%d ", bcc[i][j]);
        }
        puts("");
    }
    return 0;
}
<think>好的,我现在需要给用户介绍C++中的Tarjan强连通分量算法。首先,我得回忆一下Tarjan算法的基本概念和步骤,确保自己理解正确。 Tarjan算法是由Robert Tarjan提出的,用于寻找有向图中的强连通分量(SCC)。强连通分量指的是图中最大的子图,其中任意两个顶点都可以互相到达。这个算法使用深度优先搜索(DFS)来遍历图,并通过维护一个栈和一些辅助数组来跟踪节点的访问状态。 接下来,我应该分步骤详细说明算法的实现过程。首先,每个节点都有一个索引(index)和低链接值(lowlink)。索引表示节点被访问的顺序,低链接值是该节点能回溯到的最早访问的节点的索引。当某个节点的低链接值等于其索引时,说明找到了一个强连通分量,需要将栈中的节点弹出,直到当前节点为止,这些节点组成一个SCC。 然后,我需要考虑如何在C++中实现这个算法。通常会用邻接表来表示图结构,使用栈来保存当前路径上的节点,以及两个数组来存储索引和低链接值。还需要一个布尔数组来记录节点是否在栈中,以避免重复处理。 可能会遇到的问题包括如何初始化这些数组,如何处理未访问的节点,以及在DFS过程中如何更新低链接值。例如,当访问一个已经在栈中的节点时,需要比较当前节点的低链接值和目标节点的索引,取较小值来更新当前的低链接值。 此外,需要注意递归DFS的实现细节,比如递归终止条件,以及何时将节点弹出栈并记录SCC。代码示例需要清晰,变量命名要合理,确保用户能够理解每一步的作用。 最后,我应该测试一下这个算法,比如使用一个简单的有向图来验证输出是否正确。例如,对于包含四个节点的图,边为0→1,1→2,2→0,1→3,算法应该输出两个SCC:{0,1,2}和{3}。 在编写代码示例时,要注意注释和格式,帮助用户理解关键步骤。同时,要提醒用户关于时间复杂度的部分,Tarjan算法的时间复杂度是O(V+E),适合处理较大的图结构。 总结下来,我需要结构清晰地介绍算法原理、步骤,并提供可运行的C++代码示例,确保用户能够理解并实现该算法。同时,要检查是否有遗漏的关键点,比如栈的使用和低链接值的更新逻辑,确保解释准确无误。</think>以下是关于Tarjan强连通分量算法的逐步说明和C++实现: **算法核心思想** 1. 通过DFS遍历图时维护一个栈 2. 使用两个关键数组: - $index[]$ 记录访问顺序 - $lowLink[]$ 记录能回溯到的最早节点 3. 当$lowLink[u] == index[u]$时,栈顶到当前节点的元素构成一个SCC **算法步骤** 1. 初始化全局计数器和标记数组 2. 对未访问的节点进行DFS 3. 每次DFS时: - 设置当前节点的index和lowLink - 将节点压入栈并标记在栈中 4. 遍历邻接节点: - 如果未访问,递归访问并更新lowLink - 如果已在栈中,更新lowLink值 5. 发现根节点时弹出栈中元素构成SCC **C++实现代码** ```cpp #include <vector> #include <stack> #include <algorithm> using namespace std; vector<vector<int>> sccTarjan(vector<vector<int>>& graph) { int n = graph.size(), index = 0; vector<int> indexMap(n, -1), lowLink(n, -1); vector<bool> onStack(n, false); stack<int> st; vector<vector<int>> scc; auto dfs = [&](auto self, int u) -> void { indexMap[u] = lowLink[u] = index++; st.push(u); onStack[u] = true; for(int v : graph[u]) { if(indexMap[v] == -1) { // 未访问 self(self, v); lowLink[u] = min(lowLink[u], lowLink[v]); } else if(onStack[v]) { // 已在栈中 lowLink[u] = min(lowLink[u], indexMap[v]); } } if(lowLink[u] == indexMap[u]) { // 发现SCC vector<int> component; while(true) { int v = st.top(); st.pop(); onStack[v] = false; component.push_back(v); if(v == u) break; } scc.push_back(component); } }; for(int i = 0; i < n; ++i) { if(indexMap[i] == -1) { dfs(dfs, i); } } return scc; } ``` **算法特征** - 时间复杂度:$O(V + E)$(线性时间) - 空间复杂度:$O(V)$(栈和辅助数组) - 特点:单次DFS即可找出所有SCC **使用示例** ```cpp // 构造图:0→1→2→0,1→3 vector<vector<int>> graph = { {1}, // 0 {2,3}, // 1 {0}, // 2 {} // 3 }; auto components = sccTarjan(graph); // 输出结果:[[3], [0,1,2]] ``` **应用场景** 1. 编译器中的代码优化 2. 电子电路分析 3. 社交网络社群发现 4. 代码依赖分析 5. 强连通性检测 关键理解点:$lowLink[u]$的更新策略既包含前向边的更新(递归返回时),也包含横向边的更新(遇到栈中节点时),这保证了能正确识别环路结构。当某个节点的lowLink等于其index值时,说明找到了环路的根节点。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值