图论——割点(关节点) Tarjan 算法

引入

连通图

​ 在一个无向图 G G G中,若从顶点 i i i 到顶点 j j j 有路径相连,则称 i i i j j j 是连通的。如果图中任意两点都是连通的,那么图被称作连通图。如果 G G G 是有向图,则称为强连通图(注意:需要双向都有路径)。如果是单向连通,则称 G G G为单向连通图。

割点(关节点)

​ 在无向连通图 G = ( V , E ) G=(V,E) G=(V,E) 中: 若对于 x ∈ V x\in V xV, 从图中删去节点 x x x 以及所有与 x x x 关联的边之后, G G G 分裂成两个或两个以上不相连的子图, 则称 x x x G G G 的割点。 简而言之, 割点是无向连通图中的一个特殊的点, 删去中这个点后, 此图不再连通, 而所以满足这个条件的点所构成的集合即为割点集合。

割边(桥)

​ 如果删除 G G G的一条边 b b b,图 G G G分离成两个非空子图,则称边 b b b为图 G G G的桥。如下图中,顶点 u u u v v v都是割点,其他顶点都不是割点,边 ( u , v ) (u,v) (u,v)是桥,其他边都不是桥。

关节点识别

方案一:DFS (O(n^2))

依次去掉每一个点,判断图是否还连通。

方案二: Tarjan 算法

DFS树

首先需要了解一些关于深度优先搜索树(DFS tree)的概念。

以下图为例:

在这里插入图片描述

它的深度优先搜索树如下:

其中黑色的边为树边:如果结点 u u u因算法对边 ( u , v ) (u,v) (u,v)的搜索而首次被发现,则 ( u , v ) (u,v) (u,v)是一条树边。

简单点说就是,它是正常的一颗树的边,只看 1 , 2 , 3 , 4 1,2,3,4 1,2,3,4结点,是一颗树。【箭头只是表示搜索顺序】

其中红色的边为反向边:方向边 ( u , v ) (u,v) (u,v)是将结点 u u u连接到其 d f s dfs dfs树中的一个祖先结点 v v v的边,环也被认为是反向边。

在这里插入图片描述

算法步骤

首先选定一个根节点,从该根节点开始遍历整个图(使用 D F S DFS DFS)。

对于根节点,判断是不是割点很简单,计算其子树数量就行,如果有2棵即以上的子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。

对于非根节点,判断是不是割点就有些麻烦了。我们维护两个数组 d f n [ ] dfn[] dfn[] l o w [ ] low[] low[]

d f n [ u ] dfn[u] dfn[u]:意思就是在 d f s dfs dfs的过程中,当前的 u u u结点是第几个(首次)被访问的。(之前一直不知道这个 d f n dfn dfn是个什么缩写,豆腐脑?)

l o w [ u ] low[u] low[u]:表示顶点 u u u及其子树中的点,通过反向边,能够回溯到的最早的点( d f n dfn dfn最小)的 d f n dfn dfn

对于边 ( u , v ) (u, v) (u,v),如果 l o w [ v ] > = d f n [ u ] low[v]>=dfn[u] low[v]>=dfn[u],此时 u u u就是割点。

算法可视为线性时间复杂度,采用邻接表存储的话,应与 D F S DFS DFS相同,为 O ( V + E ) O(V+E) O(V+E)

例题1:https://www.luogu.org/problem/P3388

#include <iostream>
#include <set>
#include <stdio.h>
using namespace std;

int cnt, Time = 1;
int Low[20005], d[20005];
bool fuck[20005];
struct Node {
    int data;
    Node* next;
    Node() {}
    Node(int data) :data(data) {};
    void push(int to) {
        Node* s = new Node(to);
        s->next = next;
        next = s;
    }
}head[20005];

void DFS(int u, int father) {
    int cnt = 0;
    Low[u] = d[u] = Time++;
    Node* p = head[u].next;

    while (p) {
        int v = p->data;
        if (d[v] == -1) {//如果v尚未访问
            DFS(v, u);
            if (Low[v] < Low[u])Low[u] = Low[v];
            
            if (father != 0 && Low[v] >= d[u]) fuck[u] = true;
            
            if (father == 0)
                cnt++;    
        }
        //如果v已经访问,但不是v的双亲,则v是一条反向边
        Low[u] = Low[u] < d[v] ? Low[u] : d[v];
        p = p->next;
    }
    if (father == 0 && cnt >= 2)fuck[u] = true;
}

int main() {
    int i, n, m;
    cin >> n >> m;
    for (i = 1; i <= n; i++) {
        d[i] = -1;
        head[i].next = NULL;
    }

    for (i = 0; i < m; i++) {
        int c1, c2;
        //cin >> c1 >> c2;
        scanf("%d%d", &c1, &c2);
        head[c1].push(c2);
        head[c2].push(c1);
    }
    for (int i = 1; i <= n; i++) {
        if (d[i] == -1) DFS(i, 0);
    }
    //DFS(1, 0);

    int res = 0;
    for (int i = 1; i <= n; i++) {
        if (fuck[i])res++;
    }
    cout << res << endl;
    for (int i = 1; i <= n; i++) {
        if (fuck[i])cout << i << ' ';
    }
    cout << endl;
    return 0;
}

例题2: https://www.luogu.org/problem/P2341

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值