Tarjan学习笔记

一些普适的概念

时间戳(dfn)

说白了就是用dfs遍历图中所有节点的顺序,很好理解,一般用数组 d f n [   i   ] dfn[\ i\ ] dfn[ i ] 表示第几个访问的节点 i i i
PS:本人在学校机房,不方便把图片放进来,敬请谅解。如有需要请自行手写。

追溯值(low)

看起来比较抽象,其实我也不太好给出合理的解释,大概意思就是 l o w [   i   ] low[\ i \ ] low[ i ] 代表节点 i i i 通过直接或间接存在的路径,可以返回到的时间戳(dfn值)最小的节点 j j j ,其中 l o w [   i   ] = d f n [   j   ] low[\ i \ ] = dfn[\ j \ ] low[ i ]=dfn[ j ] 。然后就引入了一些有关搜索树中边的名称的概念,在下面的板块会提及。

搜索树

这个也很好理解,就是在一张连通图中(不管有向还是无向),所有的节点以及发生递归的边共同构成一张搜索树,显然搜索树中节点数比边数多1(这是树的性质)。
相应的,如果这张图不是连通图,那么相应构成的就是搜索森林。

在搜索树中,我们以有向边 ( x , y ) (x, y) (x,y) 为例(先后顺序不代表前父后子):

  • 若边 ( x , y ) (x, y) (x,y) 是搜索树中的边,则称边 ( x , y ) (x, y) (x,y)树枝边
  • 若边 ( x , y ) (x, y) (x,y) 不是搜索树中的边,且在搜索树中 y y y x x x 的后代,则边 ( x , y ) (x, y) (x,y)前向边(祖先指向后代)。
  • 若边 ( x , y ) (x, y) (x,y) 不是搜索树中的边,且在搜索树中 x x x y y y 的后代,则边 ( x , y ) (x, y) (x,y)后向边(后代指向祖先)。
  • 整个图中除了以上三种边就是横叉边

PS:显然,只有有向图中存在横叉边,证明略。

Tarjan与有向图

强连通

有向图中,若存在一个子图,且该子图中的任意两点都相互连通,则该子图为强连通图

根据定义,显然,一个环一定是强连通图;但是,一个强连通图不一定是一个环,有可能是多个环拼接在一起。

在有向图中最大的强连通图为强连通分量。

PS:有些书和博客把这里的最大强连通子图说成极大强连通子图,都是一个意思。

强连通分量判定法则(scc)

根据定义,亦可得,每一个节点只会属于一个强连通子图。

其实真正求的是哪一个节点属于哪一个强连通图,强连通分量的定义是最大的强连通子图,强连通分量一般来说是一个(有可能是多个相等的),但是好像也可以指强连通图。不知怎么搞的,既然都这么说,那就这么说罢。

我们按照dfn的顺序将每一个节点编号入栈,当 d f n [   x   ] = = l o w [   x   ] dfn[\ x\ ] == low[\ x\ ] dfn[ x ]==low[ x ] 时,将栈顶到 x x x 之间所有的节点出栈,这些节点构成一个强连通分量。

d f n [   x   ] = = l o w [   x   ] dfn[\ x\ ] == low[\ x\ ] dfn[ x ]==low[ x ] 即为强连通分量的判定条件。简要说明:节点 x x x 能返回到最早的节点就是 x x x 本身。从 x x x s . t o p (   ) s.top(\ ) s.top( ) 这些节点的父节点可以看做 x x x ,且它们最多只能返回到节点 x x x ,所以这些节点构成一个强连通分量。

代码实现如下:

void tarjan(int x) {
    dfn[x] = low[x] = ++cnt;
    sta[++top] = x; ins[x] = true; // ins[x] 标记 x 是否入栈

    for (int i = head[x]; ~i; i = nxt[i]) {
        int y = ver[i];

        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);
        } else if (ins[y]) {
            low[x] = min(low[x], dfn[y]);
        }
    }

    if (dfn[x] == low[x]) {
        int y; ++num; // num 是连通块的编号

        do {
            y = sta[top--]; ins[y] = false;
            c[y] = num; // c[y] 保存 y 属于哪个连通块
            scc[num].push_back(y); // scc[num] 保存连通块 num 里有哪些点
        } while (y != x);
    }
}

以大家的能力,不用注释也能看懂的。%%%

scc缩点

强连通分量的缩点很简单。因为每个节点只可能属于一个scc,所以我们可以把一个scc缩成一个点。设存在有向边 ( x , y ) (x, y) (x,y) ,若 c [   x   ] = = c [   y   ] c[\ x\ ] == c[\ y\ ] c[ x ]==c[ y ] ,则这条边会被吞掉;反之,这条边就会被建立。

代码实现如下:

// 以下代码加到主函数中:
for (int x = 1; x <= n; ++x) {
    for (int i = head[x]; i != -1; i = nxt[i]) {
        int y = ver[i];
        if (c[x] == c[y]) continue;
        add_c(c[x], c[y]);
    }
}

Tarjan与无向图

桥(割边)

定义很好理解:在无向连通图中,若删去一条边,使得该连通图分裂成两个不连通的连通图,则这条边为桥(割边)

桥(割边)判定法则

d f n [   x   ] < l o w [   y   ] dfn[\ x \ ] < low[\ y\ ] dfn[ x ]<low[ y ]
其中 y y y x x x 的子节点。
简要说明: y y y 出发,没有任何一条路径能到达 x x x x x x 以上的节点,显然这条边被删去以后分裂成两个子连通图,即这条边是桥(割边)。

代码实现如下:

void tarjan(int x, int fa_edge) { // 第二个参数传的是刚才发生递归的边的编号
    dfn[x] = low[x] = ++cnt;

    for (int i = head[x]; ~i; i = nxt[i]) {
        int y = ver[i];

        if (!dfn[y]) {
            tarjan(y, i);
            low[x] = min(low[x], low[y]);

            if (dfn[x] < low[y]) {
                bridge[i] = bridge[i ^ 1] = true;
                // bridge[i] 保存 i 是否为桥(割边)
            }

        } else if (i != (fa_edge ^ 1)) { // 成对变换
            low[x] = min(low[x], dfn[y]);
        }
    }
}

这里用到了一个叫成对变换的东西,若不懂则可以去借鉴其他文章。这里默认读者理解什么是成对变换。

边双连通分量(e-dcc)

图的双连通分量(dcc)包括边双连通分量(e-dcc)点双连通分量(v-dcc),这里先介绍e-dcc。

在无向连通图中,不包含桥的子图为边双连通图,最大的边双连通子图为边双连通分量。
(定义与强连通分量类似。)
说白了就是把一张无向图中所有的桥删去,每一个连通块就是一个e-dcc。

代码实现如下:

// 各数组名的含义与 scc 上面的类似
void edcc_dfs(int x) {
    c[x] = num;

    for (int i = head[x]; i != -1; i = nxt[i]) {
        int y = ver[i];

        if (bridge[i] || c[y]) continue;
        edcc_dfs(y);
    }
}

// 以下代码加到主函数中:
for (int i = 1; i <= n; ++i) {
    if (c[i]) continue;
    ++num;
    edcc_dfs(i);
}

e-dcc缩点

显然也很简单。和scc的缩点的思路类似。

代码如下:

// 以下代码加到主函数中:
for (int i = 2; i <= tot; ++i) {
    // tot 是链式前向星中边的编号,为了保证正常的成对变换,tot 要从偶数编号开始计起
    int x = ver[i], y = ver[i ^ 1];

    if (c[x] == c[y]) continue;
    add_c(c[x], c[y]);
}

割点(割顶)

割点(割顶)判定法则

d f n [   x   ] ≤ l o w [   y   ] dfn[\ x \ ] \le low[\ y\ ] dfn[ x ]low[ y ]
其中 y y y x x x 的子节点。
解释与桥的类似。简要说明: y y y 出发,无论走哪一条路径都只能到达 x x x x x x 以下的节点,显然这个点被删去以后分裂成两个子连通图,即这条点是割点(割顶)。

值得注意的是,若 x x x 为搜索树的根节点,则 x x x 仍需要满足至少有两个节点与其相连才可能保证 x x x 为割点。所以我们要对根节点进行特判。

代码实现如下:

void tarjan(int x) {
    dfn[x] = low[x] = ++cnt;
    int flag(0); // 记录有多少个点与 x 连通

    for (int i = head[x]; ~i; i = nxt[i]) {
        int y = ver[i];

        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);

            if (dfn[x] <= low[y]) {
                ++flag;
                if (x != rt || flag > 1) cut[x] = true;
                // 若 x 不为根则 x 一定是割点
                // 若 x 为根,且和根相连的节点数 >= 2 亦为真
                // 否则不是割点
            }

        } else {
            low[x] = min(low[x], dfn[y]);
        }
    }
}

点双连通分量(v-dcc)

在无向连通图中,不包含桥的子图为边双连通图,最大的边双连通子图为边双连通分量。
(定义与边双连通分量类似。)

但和e-dcc不同的是,e-dcc把桥删去即可求得每个边双联通分量。v-dcc中,每一个割点可能属于多个点双连通分量。所以v-dcc缩点是把每个割点单拎出来,再把每个v-dcc单拎出来,如果这个v-dcc中包含割点,那么就将这个v-dcc和这个割点连边。

显然,如果一个点是孤立点(就是没有任何边点与其相连),那么它自己属于一个v-dcc。所以也需要对孤立点进行特判。

代码实现如下:

void tarjan(int x) {
    dfn[x] = low[x] = ++cnt;
    sta[++top] = x;
    int flag(0);

    if (x == rt && head[x] == -1) { // 对孤立点的特判
        vdcc[++num].push_back(x);
        return;
    }

    for (int i = 0; i < ver[x].size(); ++i) {
        int y = ver[x][i];

        if (!dfn[y]) {
            tarjan(y);
            low[x] = min(low[x], low[y]);

            if (dfn[x] <= low[y]) {
                ++flag;
                if (x != rt || flag > 1) cut[x] = true;

                int z; ++num;
                do {
                    z = sta[top--];
                    vdcc[num].push_back(z);
                } while (z != y);
                vdcc[num].push_back(x); // x 可能属于多个 v-dcc
            }

        } else {
            low[x] = min(low[x], dfn[y]);
        }
    }
}

v-dcc缩点

刚才在上面的板块说了一些。就是把每个v-dcc缩成一个点。因为割点可能属于多个v-dcc,所以要把每一个割点单拎出来。具体思路要结合上面的看。

代码实现如下:

// 主函数里
cnt = num;
for (int i = 1; i <= n; ++i) {
    if (cut[i]) new_cut[i] = ++cnt;
}

for (int x = 1; x <= num; ++x) {
    for (int i = 0; i < vdcc[i].size(); ++i) {
        int y = vdcc[x][i];

        if (cut[y]) {
            add_c(x, new_cut[y]);
            add_c(new_cut[y], x);
        } else c[y] = x;
    }
}

PS:若有错误和疑问,欢迎在评论区留言,看到一定回复%%%

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值