图论专题-学习笔记:割点与桥

1. 前言

割点与桥,是图论的一个分支,常使用 Tarjan 算法实现。

没错又是这个算法

注意割点与桥中的 Tarjan 算法与强连通分量中的 Tarjan 算法在具体实现上有所不同。

前置知识:

  • dfs 树 / dfs 序

2. 割点

例题:P3388 【模板】割点(割顶)

2.1 定义

割点的定义:在一张图中,如果删掉一个点之后,这张图的连通块个数增加,那么这个点就是这张图的割点。

比如下面这张图,4 号点就是割点。

在这里插入图片描述

需要注意的是,一张图可能会有多个割点。

2.2 求法

那么如何求割点呢?

采用 dfs 的方式求割点,此时显然会有一个 dfs 序与一棵 dfs 树。

在过程中记录两个值 d f n , l o w dfn,low dfn,low d f n dfn dfn 表示这个点在 dfs 中是第几个被访问到的,专用名词叫时间戳。 l o w low low 表示这个点在不经过其 dfs 树中的父亲节点(即过来的点)时能够回到的时间戳最小的点。初始值 l o w = d f n low=dfn low=dfn

那么对于一条边 ( u , v ) (u,v) (u,v)(此处规定 dfs 树中 u u u v v v 的父亲),如果 l o w v > = d f n u low_v>=dfn_u lowv>=dfnu,那么此时此刻 u u u 就是割点,因为 v v v 除了经过 u u u 不能再到 u u u 上面的点了。

如何更新 l o w low low 呢?

对于当前点 n o w now now,设其下一个走向点为 u u u,分两种情况:

  • u u u 没有被走过。
    这个时候我们走向 u u u 继续 dfs。dfs 完之后令 l o w n o w ← min ⁡ ( l o w n o w , l o w u ) low_{now} \leftarrow \min(low_{now},low_u) lownowmin(lownow,lowu),因为 u u u 能到 n o w now now 也一定能到。
  • u u u 被走过了。
    此时直接更新 l o w n o w ← min ⁡ ( l o w n o w , d f n u ) low_{now} \leftarrow \min(low_{now},dfn_u) lownowmin(lownow,dfnu) 即可,因为此时 n o w now now 没有经过父亲节点便走到了 u u u

需要注意的是,代码中并没有对第 2 种情况单独拎出来处理,具体原因是对于任意点 u u u d f n u ≥ l o w u dfn_u \geq low_u dfnulowu

因此实质上如果是第一种情况,更新 l o w n o w ← min ⁡ ( l o w n o w , d f n u ) low_{now} \leftarrow \min(low_{now},dfn_u) lownowmin(lownow,dfnu) 这一步是无效的。

算法清晰,正确性显然,但是有一个点我们没有处理到。

dfs 开始时的节点呢?

这个节点在 dfs 树中是根节点,这个点的时间戳已经是最小的了,没办法更小。

因此对于这一个点,其判定方法为:当其进入下一层 dfs 次数不少于 2 次时,这个点为割点。

因为此时如果删掉根节点,由于往下递归了至少 2 次,说明至少会多出一个连通块。由定义,这个点为割点。

现在一切处理完毕。

代码:

/*
========= Plozia =========
    Author:Plozia
    Problem:P3388 【模板】割点(割顶)
    Date:2021/5/10
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int MAXN = 2e4 + 10, MAXM = 1e5 + 10;
int n, m, Head[MAXN], cnt_Edge = 1, dfn[MAXN], Low[MAXN], cnt_node;
struct node { int to, Next; } Edge[MAXM << 1];
bool book[MAXN];

int read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
void add_Edge(int x, int y) { ++cnt_Edge; Edge[cnt_Edge] = (node){ y, Head[x] }; Head[x] = cnt_Edge; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

void dfs(int now, int father)
{
    dfn[now] = Low[now] = ++cnt_node; int val = 0;
    for (int i = Head[now]; i; i = Edge[i].Next)
    {
        int u = Edge[i].to;
        if (!dfn[u])
        {
            dfs(u, now); ++val;
            Low[now] = Min(Low[now], Low[u]);
            if (Low[u] >= dfn[now] && now != father) book[now] = 1;
        }
        Low[now] = Min(Low[now], dfn[u]);
    }
    if (now == father && val >= 2) book[now] = 1;
}

int main()
{
    n = read(), m = read();
    for (int i = 1; i <= m; ++i)
    {
        int x = read(), y = read();
        add_Edge(x, y); add_Edge(y, x);
    }
    for (int i = 1; i <= n; ++i)
        if (!dfn[i]) dfs(i, i);
    int ans = 0;
    for (int i = 1; i <= n; ++i)
        if (book[i]) ++ans;
    printf("%d\n", ans);
    for (int i = 1; i <= n; ++i)
        if (book[i]) printf("%d ", i);
    printf("\n"); return 0;
}

3. 桥

笔者暂时没有找到模板题。

2.1 定义

桥的定义与割点的定义非常类似。

桥的定义:对于一条边 ( u , v ) (u,v) (u,v),如果删去这条边之后图的连通块个数增多,那么这条边为割边,也称其为桥。

比如下面这张图, ( 4 , 5 ) (4,5) (4,5) 就是这张图的桥。

在这里插入图片描述

同样的,一张图可能会有多个桥。

2.2 求法

同样采用 Tarjan 算法,仍然要维护 d f n , l o w dfn,low dfn,low

不过这里对于边 n o w − > u now->u now>u,如果 l o w u ≥ d f n n o w low_u \geq dfn_{now} lowudfnnow,说明这条边为割边。

而特别的,我们在更新的时候 绝对不能走原来的边,也就是说哪条边过来我们不能从这条边上更新,否则此时会造成答案错误。

为什么?

因为实质上桥是 割点与一整块连通块之间的连边,所以如果更新了 l o w low low,就会出现这样一个情形:

在这里插入图片描述

如果回去的边能够被更新的话,那么:

一开始 d f n 1 = l o w 1 = 1 dfn_1=low_1=1 dfn1=low1=1

因为回去的边可以被更新,那么 l o w 2 = 1 low_2=1 low2=1

此时会满足 l o w 2 ≥ d f n 1 low_2 \geq dfn_1 low2dfn1 ( 1 , 2 ) (1,2) (1,2) 为桥。

同理可得, l o w 3 = 1 low_3=1 low3=1

此时 l o w 3 < d f n 2 = 2 low_3 < dfn_2=2 low3<dfn2=2,算法判断其不为桥,但是实际上其为桥。

因而算法错误。

但是如果回去的边不能更新就不会有这个问题啦~

代码:

/*
========= Plozia =========
    Author:Plozia
    Problem:(暂无题目)【模板】桥
    Date:2021/5/10
    Remarks:本题的数据范围采用 P3388 的数据范围
========= Plozia =========
*/

#include <bits/stdc++.h>

typedef long long LL;
const int MAXN = 2e4 + 10, MAXM = 1e5 + 10;
int n, m, Head[MAXN], cnt_Edge = 1, Cut[MAXM << 1], Dfn[MAXN], Low[MAXN], cnt_node;
struct node { int to, Next; } Edge[MAXM << 1];

int read()
{
    int sum = 0, fh = 1; char ch = getchar();
    for (; ch < '0' || ch > '9'; ch = getchar()) fh -= (ch == '-') << 1;
    for (; ch >= '0' && ch <= '9'; ch = getchar()) sum = (sum << 3) + (sum << 1) + (ch ^ 48);
    return sum * fh;
}
void add_Edge(int x, int y) { ++cnt_Edge; Edge[cnt_Edge] = (node){y, Head[x]}; Head[x] = cnt_Edge; }
int Min(int fir, int sec) { return (fir < sec) ? fir : sec; }

void dfs(int now, int E)
{
    Dfn[now] = Low[now] = cnt_node;
    for (int i = Head[now]; i; i = Edge[i].Next)
    {
        int u = Edge[i].to;
        if (!Dfn[u])
        {
            dfs(u, i);
            Low[now] = Min(Low[now], Low[u]);
            if (Low[u] >= Dfn[now]) Cut[i] = Cut[i ^ 1] = 1;
        }
        else if (i != (E ^ 1)) Low[now] = Min(Low[now], Dfn[u]);
    }
}

int main()
{
    n = read(), m = read();
    for (int i = 1; i <= m; ++i)
    {
        int x = read(), y = read();
        add_Edge(x, y); add_Edge(y, x);
    }
    for (int i = 1; i <= n; ++i)
        if (!Dfn[i]) dfs(i, 0);
    int ans = 0;
    for (int i = 2; i <= cnt_Edge; i += 2)
        if (Cut[i]) ++ans;
    printf("%d\n", ans);
    for (int i = 2; i <= cnt_Edge; i += 2)
        if (Cut[i]) printf("%d %d\n", Edge[i ^ 1].to, Edge[i].to);
    return 0;
}

2.3 易错点

这里谈两个最常见的易错点:

  • 两个割点的连边一定是割边。
  • 割边的两个端点一定是割点。

这两个实际上都是错的,反例如下:

在这里插入图片描述

上图中 2,3 是割点,但是 ( 2 , 3 ) (2,3) (2,3) 不是割边。

在这里插入图片描述

显然 ( 2 , 3 ) (2,3) (2,3) 为割边,但是 3 不是割点。

4. 总结

割点割边算法采用 Tarjan 算法求解。

d f n dfn dfn 为时间戳, l o w low low 为最早能够回到的点的最小时间戳(不经过父节点)。

再次提醒:不能与求强连通分量的 Tarjan 算法弄混!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值