1 条题解

  • 0
    @ 2025-8-24 21:48:27

    自动搬运

    查看原文

    来自洛谷,原作者为

    avatar Alex_Wei
    **

    搬运于2025-08-24 21:48:27,当前版本为作者最后更新于2021-10-10 19:52:54,作者可能在搬运后再次修改,您可在原文处查看最新版

    自动搬运只会搬运当前题目点赞数最高的题解,您可前往洛谷题解查看更多

    以下是正文


    • Update on 2023.6.26:重构题解,原题解见 剪贴板
    • Update on 2024.8.9:修改题解。

    摘自笔记 图论 I

    无向图 DFS 树

    给定无向连通图 GG,从点 rr 开始 DFS,取出进入每个点 ii 时对应的边 (fai,i)(fa_i, i) 并定向为 faiifa_i\to i,得到一棵以 rr 为根的树。称 (fai,i)(fa_i, i)树边,其它边为 非树边

    给每个点标号为它被访问到的次序,称为 时间戳,简称 dfn。DFS 得到的结点序列称为 DFS 序,时间戳为 ii 的结点在 DFS 序中的位置为 ii

    上图是一个可能的 DFS 树以及对应的时间戳。

    无向图 DFS 树的性质(非常重要):

    • 祖先后代性:任意非树边两端具有祖先后代关系。
    • 子树独立性:结点的每个儿子的子树之间没有边(和上一条性质等价)。
    • 时间戳区间性:子树时间戳为一段区间。
    • 时间戳单调性:结点的时间戳小于其子树内结点的时间戳。

    Tarjan 求割点

    前置知识:DFS 树,DFS 序。

    注意区分:DFS 序表示对一张图 DFS 得到的结点序列,而时间戳 dfn 表示每个结点在 DFS 序中的位置。

    xx 的子树为 xx 在 DFS 树上的子树,包含 xx 本身,记作 T(x)T(x)。记 T(x)=V\T(x)T'(x) = V\backslash T(x),即整张图除了 T(x)T(x) 以外的部分。

    不妨认为 GG 是无向连通图。对于非连通图,对每个连通分量分别求割点。

    笔者希望提出一种新的理解 Tarjan 算法的方式。网上大部分博客讲解 Tarjan 算法时 low 数组凭空出现,抽象的定义让很多初学者摸不着头脑,从提出问题到解决问题的逻辑链的不完整性让我们无法感受到究竟是怎样的灵感启发了这一算法的诞生。

    非根结点的割点判定

    xx 不为 DFS 树的根,则 T(x)T'(x) 非空。

    xx 是割点,则删去 xx 之后,对于 zT(x)z\in T'(x),存在 yy 和它不连通。而删去 xx 之后 T(x)T'(x) 通过树边仍然连通,所以 yT(x)y\in T(x)。而如果 yyzz 不连通,又因为 T(x)T'(x) 连通,那么 yy 和所有 T(x)T'(x) 的点均不连通。

    反之,若删去 xx 之后存在 yT(x)y\in T(x)T(x)T'(x) 的点均不连通,那么 xx 显然是割点。这说明 xx 是割点当且仅当存在 yT(x)y\in T(x) 不经过 xx 能到达的所有点均属于 T(x)T(x)

    现在要刻画 “不经过 xx 能到达的所有点均属于 T(x)T(x)”。

    注意到,如果 yT(x)y\in T(x) 不经过 xx 就和 T(x)T'(x) 连通,那么存在 yyvT(x)v\in T'(x) 的路径,满足 vv 是路径上第一个属于 T(x)T'(x) 的结点。设路径上倒数第二个点为 uu,则 uT(x)u\in T(x)。如果 (u,v)(u, v) 是树边,那么 u=xu = x,矛盾。因此 (u,v)(u, v) 是非树边,那么 vvuu 的祖先(祖先后代性)。又因为 xxuu 的祖先且 vvxx 的子树外,所以 vvxx 的祖先。如下图。

    进一步地,因为 xx 的不同儿子子树之间没有非树边(子树独立性),设 xx 的儿子 yy' 的子树包含 yy,那么 uT(y)u\in T(y')

    因此,如果 yy 不经过 xxT(x)T'(x) 连通,即 xx 不是割点,那么存在 uT(y)u\in T(y') 使得 uu 可以通过一条非树边到达 xx 的祖先。设 fxf_x 表示与 xx 通过 非树边 相连的所有点的时间戳的最小值,则条件可写为 fu<dxf_u < d_x

    • 对于 T(y)T(y'),如果存在 uT(y)u\in T(y') 满足 fu<dxf_u < d_x,那么删去 xxT(y)T(y') 的每个点和 T(x)T'(x) 均连通:T(y)T(y') 内所有点通过树边连通,且 uuT(x)T'(x) 某点直接相连。

    • 反之,如果 T(y)T(y') 内所有点的 ff 值均不小于 dxd_x,那么删去 xxT(y)T(y') 的每个点和 T(x)T'(x) 均不连通。因为如果连通,那么总得有一个点能一步连通。

    这样,我们得到了非根结点的割点判定法则:

    xx 是割点当且仅当存在树边 xyx\to y',使得 yy' 子树 不存在uu 使得 fu<dxf_u < d_x

    这等价于存在 xx 的儿子 yy',满足 minuT(y)fudx\min_{u\in T(y')} f_u \geq d_x

    gxg_x 表示 xx 的子树内所有点 uT(x)u\in T(x)fuf_u 的最小值(low 的真正含义),根据树形 DP,有

    $$g_x = \min\left(\min_{y'\in \mathrm{son}(x)} g_{y'}, \min_{(x, y) \in E\land (x, y)\notin T} d_y\right) $$

    对于后半部分,忽略 (x,y)(x, y) 必须是非树边的条件不会导致错误:如果用儿子更新,显然没有问题。如果用父亲更新,即用 dxd_x 更新 gyg_y,也不会导致错误,因为判定是 gydxg_y\geq d_x,有等号。但注意求解割边时不能忽略,因为判定是 gy>dxg_y > d_x

    说明:将 gxg_x 初始化为 dxd_x 显然不会导致错误。

    应用:研究删去 xx 后整张图的形态。删去 xx 后,每个判定 xx 为割点的 yy'T(y)T(y') 单独形成一个连通块,剩余部分(其它所有 T(y)T(y')T(x)T'(x))形成一个连通块。因为判定割点的准则就是删去 xxyy' 是否与 T(x)T'(x) 连通。

    根的割点判定与代码

    xx 为 DFS 树的根。

    xx 在 DFS 树上有大于一个儿子,根据子树独立性,删去 xx 后各儿子子树不连通,所以 xx 是割点。反之删去 xx 后剩余部分通过树边连通,xx 不是割点。

    综上,使用 Tarjan 算法求无向图 GG 的所有割点的时间复杂度为 O(n+m)\mathcal{O}(n + m)

    再次强调,以下代码仅在求解割点时正确。求解割边需要额外的特判。

    模板题 代码。

    #include <bits/stdc++.h>
    using namespace std;
    constexpr int N = 1e5 + 5;
    int n, m, R;
    int dn, dfn[N], low[N], cnt, buc[N]; // dfn 是时间戳 d, low 是 g
    vector<int> e[N];
    void dfs(int id) {
      dfn[id] = low[id] = ++dn; // 将 low[id] 初始化为 dn 不会导致错误, 且一般都这么写
      int son = 0;
      for(int it : e[id]) {
        if(!dfn[it]) {
          son++, dfs(it), low[id] = min(low[id], low[it]);
          if(low[it] >= dfn[id] && id != R) cnt += !buc[id], buc[id] = 1;
        }
        else low[id] = min(low[id], dfn[it]);
      }
      if(son >= 2 && id == R) cnt += !buc[id], buc[id] = 1;
    }
    int main() {
      cin >> n >> m;
      for(int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v), e[v].push_back(u);
      }
      for(int i = 1; i <= n; i++) if(!dfn[i]) R = i, dfs(i);
      cout << cnt << endl;
      for(int i = 1; i <= n; i++) if(buc[i]) cout << i << " ";
      return 0;
    }
    

    P3469 [POI2008] BLO-Blockade

    一道 Tarjan 求割点的练手题。

    设删去与结点 uu 相连的所有边之后形成的连通块大小分别为 s1ks_{1\sim k},则答案为 i=1ksi(nsi)\sum_{i = 1} ^ k s_i (n - s_i)。注意,不要忘记 uu 没有被删去,它本身是一个大小为 11 的连通块。

    因为 vv 判定 uu 为割点当且仅当封锁 uu 之后 vv 及其子树与整张图剩余部分不连通,所以考虑所有判定 xx 为割点的 yiy_i,它们的子树分别单独形成连通块。除去这些结点后,还有一个大小为 n1size(yi)n - 1 - \sum size(y_i) 的连通块(可能为空,但不影响答案)。

    时间复杂度 O(n)\mathcal{O}(n)

    #include <bits/stdc++.h>
    using namespace std;
    constexpr int N = 1e5 + 5;
    int n, m, dfn[N], low[N], dn;
    long long ans[N], sz[N];
    vector<int> e[N];
    void dfs(int id) {
      dfn[id] = low[id] = ++dn;
      long long r = n - 1;
      ans[id] = r, sz[id] = 1;
      for(int it : e[id]) {
        if(!dfn[it]) {
          dfs(it), low[id] = min(low[id], low[it]), sz[id] += sz[it];
          if(low[it] >= dfn[id]) ans[id] += sz[it] * (n - sz[it]), r -= sz[it];
        }
        else low[id] = min(low[id], dfn[it]);
      }
      ans[id] += r * (n - r);
    }
    int main() {
      cin >> n >> m;
      for(int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        e[u].push_back(v), e[v].push_back(u);
      }
      dfs(1);
      for(int i = 1; i <= n; i++) cout << ans[i] << endl;
      return 0;
    }
    
    • 1

    信息

    ID
    1712
    时间
    1000ms
    内存
    125MiB
    难度
    4
    标签
    递交数
    0
    已通过
    0
    上传者