图论 - 割点(Tarjan)

本文介绍了无向图中的割点概念,详细解释了如何使用Tarjan算法高效地找出所有割点,时间复杂度为O(N+M),并提供了一个完整的C++实现代码示例。

文章目录

定义

在一个无向图中,如果有一个顶点集合,删除这个顶点集合以及这个集合中所有顶点相关联的边以后,图的连通分量增多,就称这个点集为割点集合。
如果某个割点集合只含有一个顶点X(也即{X}是一个割点集合),那么X称为一个割点。
如果图是连通的,那么割点的定义就是,在一个无向连通图中,如果删除某个顶点后,图不再连通,则这个顶点为割点。

通过Tarjan算法可以求出所有割点,时间复杂度为O(N + M)。

例题

洛谷P3388
给定一个无向图(不一定连通),求出所有割点。

#include <iostream>
#include <vector>
#include <cstdio>
#include <string>
#include <map>
#include <set>
#include <cstring>
#include <climits>
#include <algorithm>
#include <queue>
#include <stack>
#include <cmath>
#include <cassert>
using namespace std;

const int MAXN = 20005;
const int MAXM = 200005;

struct Edge {
    int dest, next;
} edges[MAXM];
int head[MAXN];
int edges_cnt = 0;
int n, m;

int low[MAXN];  // 记录每个结点在不结果父结点时,能够回到的最小时间戳
int dfn[MAXN];  // 记录每个结点的时间戳
bool cut[MAXN]; // 记录每个结点是否为割点
int dfi = 0;    // 当前时间戳
int root;       // 根结点

void addEdge(int src, int dest) {
    ++edges_cnt;
    edges[edges_cnt].dest = dest;
    edges[edges_cnt].next = head[src];
    head[src] = edges_cnt;
}

void tarjan(int node, int father) {
    int child = 0;  // 记录生成树中当前结点node的孩子数量
    dfn[node] = low[node] = ++dfi;
    for (int i = head[node]; i; i = edges[i].next) {
        int dest = edges[i].dest;
        if (!dfn[dest]) {  // 如果dest的时间戳为0,说明它还没被访问过
            ++child;
            tarjan(dest, node);
            // 更新当前结点node能访问到的最早时间戳
            low[node] = min(low[node], low[dest]);
            // 如果当前结点不是根结点且low[dest]>=dfn[node],则当前结点为割点
            if (node != root && low[dest] >= dfn[node]) {
                cut[node] = true;
            }
            // 如果当前结点是根结点,在生成树中必须有2个孩子,才是割点
            else if (node == root && child == 2) {
                cut[node] = true;
            }
        }
        // 否则如果dest曾被访问过,并且不是node的父结点,则说明此时dest为
        // node的祖先,因此需要更新node能访问到的最早时间戳
        else if (dest != father) {
            low[node] = min(low[node], dfn[dest]);
        }
    }
}

int main() {
#ifdef DEBUG
    freopen("in.txt", "r", stdin);
#endif
    scanf("%d%d", &n, &m);
    while (m--) {
        int src, dest;
        scanf("%d%d", &src, &dest);
        addEdge(src, dest);
        addEdge(dest, src);
    }

    for (int node = 1; node <= n; ++node) {
        if (!dfn[node]) {
            root = node;
            tarjan(node, 0);
        }
    }

    int cut_cnt = 0;
    for (int node = 1; node <= n; ++node) {
        if (cut[node])
            ++cut_cnt;
    }
    printf("%d\n", cut_cnt);
    for (int node = 1; node <= n; ++node) {
        if (cut[node])
            printf("%d ", node);
    }
    putchar('\n');

    return 0;
}
<think>嗯,用户想了解缩Tarjan算法的原理。我需要先理清楚这些概念的定义以及它们之间的关系,然后逐步解释。首先,Tarjan算法是图论中常用的算法,主要用于寻找强连通分量(SCC)、、桥等。缩通常是在找到强连通分量后,将每个SCC缩成一个节,形成有向无环图(DAG)。则是无向图中删除后会导致图不连通的节。可能需要分步骤来讲解。 首先,我应该从Tarjan算法的基础开始,比如深度优先搜索(DFS)的应用,以及如何通过维护发现时间和low值来识别这些结构。的判断条件是什么?当一个顶的子节无法绕过该顶到达更早的节时,该顶就是。而缩则是处理有向图中的强连通分量,通过Tarjan找到所有SCC后,每个SCC合并成一个节,形成DAG。 接下来,要确保逻辑清晰,先讲Tarjan算法的核心思想,再分别介绍和缩的应用。可能需要举例说明,比如的例子,如何通过DFS树中的子节low值是否大于等于当前节的发现时间来判断。缩的例子,如何处理每个节的栈和low值来识别SCC,并合并它们。 需要注意用户可能需要这些知识来解决实际问题,比如算法竞赛或网络分析中的问题,因此要强调实际应用和算法步骤。另外,用户提到要使用中文,结构清晰,所以分解释可能更合适。还要检查LaTeX格式是否正确,比如数学符号和公式用$...$包裹,独立公式用$$...$$。 可能会有混淆的地方,比如Tarjan算法在无向图和有向图中的不同应用,属于无向图,而缩处理的是有向图的强连通分量。需要明确区分这两种情况,避免用户混淆。此外,解释low数组的含义时,要清楚说明其代表当前节通过DFS树中的子树能够回溯到的最早节的时间戳。 最后,总结Tarjan算法的优势,比如线性时间复杂度,高效处理大规模图结构。可能还需要提到相关概念,比如桥(边)和双连通分量,但根据用户问题,重放在缩上。</think>好的,我将分步骤介绍**缩Tarjan算法的原理**,尽量保证逻辑清晰。 --- ### 一、Tarjan算法的核心思想 Tarjan算法基于**深度优先搜索(DFS)**,通过维护两个关键数组: - **$dfn[u]$**:记录节$u$的DFS遍历时间戳(发现顺序) - **$low[u]$**:记录节$u$通过其子树能回溯到的最小时间戳 通过比较$dfn$和$low$的值,可以识别图中的关键结构(如、强连通分量)。 --- ### 二、(Cut Vertex)的定义与判定 #### 1. 什么是? 在**无向图**中,若删除某节后图的连通分量数增加,则该节称为。 #### 2. 判定条件(基于Tarjan算法) 对于当前节$u$: - **情况1**:若$u$是DFS树的根节,且有两个及以上子树 → $u$是- **情况2**:若$u$非根节,存在子节$v$满足: $$low[v] \ge dfn[u]$$ 即$v$无法绕过$u$访问到更早的祖先 → $u$是。 #### 3. 示例代码逻辑 ```python def tarjan(u, is_root): global index dfn[u] = low[u] = index index += 1 child = 0 for v in adj[u]: if not dfn[v]: tarjan(v, False) low[u] = min(low[u], low[v]) if not is_root and low[v] >= dfn[u]: cut_vertex.add(u) child += 1 else: low[u] = min(low[u], dfn[v]) if is_root and child >= 2: cut_vertex.add(u) ``` --- ### 三、缩(将强连通分量合并为) #### 1. 什么是缩? 在**有向图**中,将每个强连通分量(SCC)合并为一个超级节,最终形成**有向无环图(DAG)**。 #### 2. Tarjan算法找SCC的判定 - 在DFS过程中维护一个栈,当回溯时满足: $$low[u] = dfn[u]$$ 说明栈中从$u$到栈顶的元素构成一个SCC。 #### 3. 示例代码逻辑 ```python stack = [] on_stack = [False] * (n+1) scc_id = 0 def tarjan(u): global index dfn[u] = low[u] = index index += 1 stack.append(u) on_stack[u] = True for v in adj[u]: if not dfn[v]: tarjan(v) low[u] = min(low[u], low[v]) elif on_stack[v]: # 仅处理栈中节(避免交叉边) low[u] = min(low[u], dfn[v]) if low[u] == dfn[u]: # 发现SCC scc_id += 1 while True: v = stack.pop() on_stack[v] = False scc[v] = scc_id if v == u: break ``` --- ### 四、关键原理总结 1. ****:依赖$low[v] \ge dfn[u]$判断子节是否无法绕过当前节。 2. **缩**:利用栈和$low[u] = dfn[u]$分离SCC,每个SCC对应DAG中的一个节。 3. **时间复杂度**:均为$O(V+E)$,与DFS相同。 --- ### 五、典型应用场景 1. ****:网络可靠性分析,关键节识别。 2. **缩**:有向图的简化(如求解DAG上的最长路径、环路消除)。 希望以上内容能帮助您系统理解这些概念!
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值