Tarjan强联通分量学习笔记

1.什么是强连通分量


1.定义

给定一个有向图\(\small G=(V,E)\),若存在\(\small H\subseteq G\),\(\small H\)是连通子图,且不存在\(\small H\subsetneq F\subseteq G\),且\(\small F\)是连通子图,则称\(\small H\)\(\small G\)的一个强连通分量。

2.说人话

满足两两互达的最大的有向图\(\small G\)的子图。

2.如何求强连通分量


1.前置芝士

时间戳:对有向图\(\small G\)的节点\(r\)进行深度优先搜索,按照每一个点第一次被访问的时间顺序,依次对有向图中的节点进行整数标记,该标记被称为时间戳,记作\(\small dfn[x]\)
设当前节点为\(x\),下一个将要访问的节点为\(\small y\)
对于有向边\(\small (x,y)\),有以下四种情况:
1.树枝边\(y\)是第一次被访问,即\(\small x\)\(\small y\)的父亲节点。
2.前向边\(y\)不是第一次被访问,且存在另一条从\(\small x\)\(\small y\)的路径[1],即\(\small x\)\(\small y\)的祖先节点。
3.后向边\(y\)不是第一次被访问,且存在另一条从\(\small y\)\(\small x\)的路径,即\(\small y\)\(\small x\)的祖先节点。
4.横叉边\(y\)不是第一次被访问,除此之外没有其他限制的边。
由这些边构成的集合被称为搜索树

注意:搜索树就是原图,只不过在Tarjan算法中,为了计算强连通分量而人为对这些边进行了分类。(当时的我为此理解了一个月)

2.分析

显然,根据对\(\small dfn\)的定义,
在树枝边和前向边中,\(\small dfn[x]<dfn[y]\)
在后向边和横叉边中,\(\small dfn[x]>dfn[y]\)
上图
graph.png

四种边分别用其对应种类的首字进行了标注,
节点的序号即是其对应的\(\small dfn\)

  1. 对于后向边\(\small (10,8)\),因为它的存在,节点\(8,9,10\)构成了一个强连通分量。
    同时对于后向边\(\small (4,2)\),节点\(2,3,4\)也构成了一个连通子图,
    因此在计算强连通分量时,这种边是需要考虑的。

  2. 对于前向边\(\small (1,6)\),因为树枝边\(\small(1,2),(2,6)\)的存在,已经存在一条从节点\(\small1\)到节点\(\small6\)的路径,
    同时它对构成强连通分量没有任何贡献(不能像后向边一样让节点\(\small 1,2,6\)构成强连通分量),
    因此,在计算强连通分量时,我们忽略这种边。

  3. 对于横叉边\(\small (7,4),(7,5)\),我们需要讨论:
    因为存在一条路径从节点\(\small 4\)到节点\(\small 7\),因此节点\(\small 2,3,4,6,7\)构成一个强连通分量
    因为不存在一条路径从节点\(\small 5\)到节点\(\small 7\),因此它不对强连通分量产生贡献。
    那什么横叉边有用呢

  4. 对于树枝边边太多所以就不列举了,它是上述两种边能产生贡献的基础,因此必然要考虑这种边。

3.结论

  1. 树枝边后向边必须考虑;
  2. 横叉边视情况而定;
  3. 前向边没有用。

4.后置芝士

设当前节点为\(x\),下一个将要访问的节点为\(\small y\)

1. 维护一个栈,这个栈内储存了两类节点
  1. \(\small x\)的祖先节点,记为集合\(\small A\)
  2. 已经访问过,并且存在一条路径能到达\(\small x\)的祖先节点的节点,记为集合\(\small B\)
    可以看出,若节点\(\small y\in A\)或者\(\small y \in B\),\(\small x,y\)之间必然构成一个环
2. 追溯值
  1. 定义:设\(\small subtree(x)\)为搜索树中以\(\small x\)为根的子树,\(\small x\)的追溯值为满足以下节点的最小时间戳:
    (1):该点在栈中。(说明此点是或者可以到达\(\small x\)的祖先节点)
    (2):存在一条从\(\small subtree(x)\)出发的有向边,以该点为终点。
    将此记作\(\small low[x]\)
    可以看出,追溯值\(\small low[x]\)记录的是\(\small x\)所能到达的节点中,时间戳最小且这个节点本身是祖先或者它可以到达祖先(好多资料没有讲清楚这一点)
  2. 性质
    对于节点\(\small x\),若\(\small low[x]=dfn[x]\),则在搜索树中,以节点\(\small x\)构成的子树包含一个强连通分量,\(\small x\)是搜索时遇到该强连通分量的第一个节点。
    如果觉得证明繁琐可以直接略过QWQ
    证明:反证法。假设\(\small y\)是搜索时遇到该强连通分量的第一个节点
    \(\small \therefore dfn[x]>dfn[y]\),且\(\small y\)\(\small x\)的祖先节点
    \(\small \therefore y\)位于栈中
    根据强连通分量的定义,必存在一条从\(\small x\)\(\small y\)的路径
    取另一位于栈中的节点\(\small z\),分类讨论:
    (1)\(\small z\)位于强联通分量中:显然,\(\small dfn[z]>dfn[y]\)
    (2)\(\small z\)不位于强联通分量中:显然,不存在一条从\(\small x\)\(\small z\)的路径
    \(\small \therefore\)根据追溯值的定义,\(\small low[x]=dfn[y]\)
    \(\small \therefore dfn[x]=dfn[y]\)
    这与\(\small dfn[x]>dfn[y]\)矛盾!
    \(\small \therefore\)假设不成立
    \(\small \therefore x\)是搜索时遇到该强连通分量的第一个节点。 \(\square\)[2]
    由此,当回溯\(\small x\)时,若\(\small dfn[x]=low[x]\),则从节点\(\small x\)到栈顶构成一个强连通分量。

5.算法

经过了充分冗长的铺垫,现在可以正式介绍Tarjan算法了:

  1. 当节点\(\small x\)被第一次访问时,初始化\(\small low[x]=dfn[x]\),将\(\small x\)入栈
  2. 遍历\(\small x\)指向的节点\(\small y\)
    (1)若\(\small y\)未被访问过,则说明\(\small (x,y)\)是树枝边,递归访问\(\small y\),从\(\small y\)回溯后,令\(\small low[x]=min(low[x],low[y])\)
    (2)若\(\small y\)已被访问过:若\(\small y\)在栈中,根据追溯值的定义,\(\small low[x]=min(low[x],dfn[y])\);否则说明\(\small y\)所在的强连通分量已经被计算,无需作处理。
  3. 回溯时判断\(\small dfn[x]=low[x]\),若是则不断弹出栈顶,直到\(\small x\)出栈。

下面的代码实现了Tarjan算法

const int MAXN=1e4+3;
int num,dfn[MAXN],low[MAXN],ins[MAXN];//ins表示当前节点是否在栈中,num记录时间戳
int val[MAXN],new_val[MAXN];
vector<int> graph[MAXN];
stack<int> s;
int n,m;//n为点数,m为边数
void tarjan(int u){
    low[u]=dfn[u]=++num;
    ins[u]=1;
    s.push(u);
    for(int i=0;i<graph[u].size();i++){
        int v=graph[u][i];
        if(!dfn[v]){//若v未被访问,则v不会被打上时间戳
            tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(ins[v]){
            low[u]=min(low[u],dfn[v]);
        }
    }
    if(dfn[u]==low[u]){
        int y;
        do{
            y=s.top();
            s.pop();
            ins[y]=0;
        }while(u!=y);//用do while 可以保证当栈顶即为u时也能执行
    }
}

时间复杂度\(\small O(n+m)\)

3.强连通分量有什么用


缩点,这是它最基础也最广泛的用途,可以将每一个强连通分量缩成一个点,这样剩下的图就形成了一个DAG,可以进行拓扑排序等其他操作,还可优化时间复杂度

4.题目


P3387 【模板】缩点
P2341 [HAOI2006]受欢迎的牛G
P2746 [USACO5.3]Network of Schools
P2272 [ZJOI2007]最大半连通子图

参考资料
OI Wiki - 图论相关概念
OI Wiki - 强连通分量
算法竞赛进阶指南:0x67 Tarjan算法与有向图连通性


  1. 本文提到的路径均是指简单路径 ↩︎

  2. 证明结束的符号 ↩︎

posted @ 2022-08-02 16:28  cmd_pig  阅读(49)  评论(0)    收藏  举报