什么?都3202年了,你还不会强连通分量?(一)

都3202年了,你还不会强连通分量 ?!快来看看这篇文章吧


首先得先来了解一下一些概念

  1. 连通分量:在无向图中,即为连通子图。
  2. 强连通分量:有向图中,尽可能多的若干顶点组成的子图中,这些顶点都是相互可到达的,则这些顶点成为一个强连通分量。

在这里插入图片描述
上图中有三个强连通分量,分别是 a , b , e a,b,e a,b,e 以及 f , g f,g f,g c , d , h c,d,h c,d,h


概念了解完了,那怎么求呢?

一、 Tarjan \text{Tarjan} Tarjan 算法

Tarjan \text{Tarjan} Tarjan 算法是最常用的一种算法。

它的发明者是 Robert E. Tarjan(罗伯特·塔扬,1948~),生于美国加州波莫纳,计算机科学家。

Tarjan 发明了很多算法和数据结构。不少他发明的算法都以他的名字命名,以至于有时会让人混淆几种不同的算法。比如求各种连通分量的 Tarjan \text{Tarjan} Tarjan 算法,求 LCA(Lowest Common Ancestor,最近公共祖先) \text{LCA(Lowest Common Ancestor,最近公共祖先)} LCALowest Common Ancestor,最近公共祖先) 的 Tarjan 算法。并查集、 Splay \text{Splay} Splay Toptree \text{Toptree} Toptree 也是 Tarjan 发明的。

我们这里要介绍的是在有向图中求强连通分量的 Tarjan \text{Tarjan} Tarjan 算法。


但,在介绍该算法之前,先来了解 DFS ⁡ \operatorname{DFS} DFS 生成树

有向图的 DFS ⁡ \operatorname{DFS} DFS 生成树主要有 4 种边(不一定全部出现):

  1. 树边( tree edge \text{tree edge} tree edge):示意图中以黑色边表示,每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
  2. 反祖边( back edge \text{back edge} back edge):示意图中以红色边表示(即 7 → 1 7 \rightarrow 1 71),也被叫做回边,即指向祖先结点的边。
  3. 横叉边( cross edge \text{cross edge} cross edge):示意图中以蓝色边表示(即 9 → 7 9 \rightarrow 7 97),它主要是在搜索的时候遇到了一个已经访问过的结点,但是这个结点 并不是 当前结点的祖先。
  4. 前向边( forward edge \text{forward edge} forward edge):示意图中以绿色边表示(即 3 → 6 3 \rightarrow 6 36),它是在搜索的时候遇到子树中的结点的时候形成的。

我们考虑 DFS ⁡ \operatorname{DFS} DFS 生成树与强连通分量之间的关系。

如果结点 u u u 是某个强连通分量在搜索树中遇到的第一个结点,那么这个强连通分量的其余结点肯定是在搜索树中以 u u u 为根的子树中。结点 u u u 被称为这个强连通分量的根。


Tarjan \text{Tarjan} Tarjan 算法怎么求强连通分量?

Tarjan \text{Tarjan} Tarjan 算法中为每个结点 u u u 维护了以下几个变量:

  1. dfn u \text{dfn}_u dfnu:深度优先搜索遍历时结点 u u u 被搜索的次序。
  2. low u \text{low}_u lowu:在 u 的子树中能够回溯到的最早的已经在栈中的结点。设以 u u u 为根的子树为 Subtree u \text{Subtree}_u Subtreeu low u \text{low}_u lowu 定义为以下结点的 dfn \text{dfn} dfn 的最小值: Subtree u \text{Subtree}_u Subtreeu 中的结点;从 Subtree u \text{Subtree}_u Subtreeu 通过一条不在搜索树上的边能到达的结点。

注意:
一个结点的子树内结点的 dfn \text{dfn} dfn 都大于该结点的 dfn \text{dfn} dfn

从根开始的一条路径上的 dfn \text{dfn} dfn 严格递增, low \text{low} low 严格非降。

过程:

按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfn \text{dfn} dfn low \text{low} low 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 u u u 和与其相邻的结点 v v v v v v 不是 u u u 的父节点)考虑 3 3 3 种情况:

  1. v v v 未被访问:继续对 v v v 进行深度搜索。在回溯过程中,用 low v \text{low}_v lowv 更新 low u \text{low}_u lowu。因为存在从 u u u v v v 的直接路径,所以 v v v 能够回溯到的已经在栈中的结点, u u u 也一定能够回溯到。
  2. v v v 被访问过,已经在栈中:根据 t e x t l o w text{low} textlow 值的定义,用 dfn v \text{dfn}_v dfnv 更新 low u \text{low}_u lowu
  3. v v v 被访问过,已不在栈中:说明 v v v 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

注意:下节的链接在讨论区

代码:

int dfn[N], low[N], dfncnt, s[N], in_stack[N], tp;
int scc[N], sc;  // 结点 i 所在 SCC 的编号
int sz[N];       // 强连通 i 的大小

void tarjan(int u) {
  low[u] = dfn[u] = ++dfncnt, s[++tp] = u, in_stack[u] = 1;
  for (int i = h[u]; i; i = e[i].nex) {
    const int &v = e[i].t;
    if (!dfn[v]) {
      tarjan(v);
      low[u] = min(low[u], low[v]);
    } else if (in_stack[v]) {
      low[u] = min(low[u], dfn[v]);
    }
  }
  if (dfn[u] == low[u]) {
    ++sc;
    while (s[tp] != u) {
      scc[s[tp]] = sc;
      sz[sc]++;
      in_stack[s[tp]] = 0;
      --tp;
    }
    scc[s[tp]] = sc;
    sz[sc]++;
    in_stack[s[tp]] = 0;
    --tp;
  }
}


那啥啥?考虑到有Py党的朋友,所以……我去学了一下Py的写法

dfn = [] * N; low = [] * N; dfncnt = 0; s = [] * N; in_stack  = [] * N; tp = 0
scc = [] * N; sc = 0 # 结点 i 所在 SCC 的编号
sz = [] * N # 强连通 i 的大小
def tarjan(u):
    low[u] = dfn[u] = dfncnt; s[tp] = u; in_stack[u] = 1
    dfncnt = dfncnt + 1; tp = tp + 1
    i = h[u]
    while i:
        v = e[i].t
        if dfn[v] == False:
            tarjan(v)
            low[u] = min(low[u], low[v])
        elif in_stack[v]:
            low[u] = min(low[u], dfn[v])
        i = e[i].nex
    if dfn[u] == low[u]:
        sc = sc + 1
        while s[tp] != u:
            scc[s[tp]] = sc
            sz[sc] = sz[sc] + 1
            in_stack[s[tp]] = 0
            tp = tp - 1
        scc[s[tp]] = sc
        sz[sc] = sz[sc] + 1
        in_stack[s[tp]] = 0
        tp = tp - 1


下次我们继续!


参考资料:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值