Tarjan 缩点,强联通分量

Tarjan 缩点,强联通分量

引入

如果说需要对于一个图上进行 D p \tt Dp Dp,但是同一个环上的点无法进行转移怎么办?

我们只能做 D A G \tt DAG DAG 上的 D p \tt Dp Dp,但是有环我们该怎么办?

缩环!缩点!求强联通!

当然我不是说隔壁的带花树。


虽然说所有的代码都是我随手打的,但是都已经经过测试,请放心阅读。

代码基础定义

  • dfn[x]表示当前点遍历顺序的编号。
  • low[x]表示当前点通过其 d f s \tt dfs dfs 的子树以及能通过一条边到达的点,最浅能到达的编号。

强联通分量

定义:

  有向图中能任意到达的极大点集,被称为一个强联通分量。‘

  对于一张图,我们将其划分成若干个这样的极大点集,每个点集之间的连边关系构成一张 D A G \color{red}\tt DAG DAG

代码实现:

  我们遍历每一个弱联通分量,考虑使用一个栈来维护每个点的进入顺序。

  每次通过其能到达的点来更新 low[p]

  • 如果说当前的儿子没有被遍历过,先遍历然后通过 low更新。
  • 如果说当前的儿子在栈中,直接通过 dfn更新就好了。

  这个更新就是根据定义来的。

  那么什么时候可以统计一个强联通分量呢?当 dfn[p] = low[p]的时候,也就是意味着其是该强联通分量 d f s \tt dfs dfs 时候遍历的第一个点,这样可以保证剩下的点都在栈中,我们直接退栈即可。

void tarjan(int p) {
    dfn[p] = low[p] = ++ tot, vis[p] = 1, st[++ ed] = p;
    for(int i = head[p];i;i = edg[i].next) {
        int to = edg[i].to;
        if(!dfn[to]) tarjan(to), low[p] = min(low[p], low[to]);
        else if(vis[to]) low[p] = min(low[p], dfn[to]);
    }
    if(dfn[p] == low[p]) {
        int y; bel[p] = ++ Sz, vis[p] = 0;
        while((y = st[ed --]) != p) {
            bel[y] = Sz, vis[y] = 0;
        }
    }
}

边双联通分量和桥

注意是在无向联通图中的。

定义:

  桥:割掉当前的边,使得图不连通。

  边双联通分量:一个点集,使得任意两点之间有两条本质不同的路径,本质不同指路径上没有相同的边。

代码实现:

  和之前的强联通分量相似。我们考虑先计算出所有桥,之后直接遍历连通块即可。

  和之前不同的在于,我们需要考虑父亲节点的贡献,我们考虑一条边 u → v u \to v uv 是桥意味着 low[v] > dfn[u]

  具体含义表示其最浅 不能 \color{red}\text{不能} 不能到达点 u u u,那么割掉 u → v u \to v uv 的边就不能联通了。

  正是因为这个定义所以需要考虑 父亲节点 \color{red}\text{父亲节点} 父亲节点,对于父亲节点我们不能让其产生贡献。

int brg[maxn << 1]; // 表示当前边是否为桥
void tarjan(int p,int pre) {
    dfn[p] = low[p] = ++ tot;
    for(int i = head[p];i;i = edg[i].next) {
        int to = edg[i].to;
        if(!dfn[to]) {
            tarjan(to, i);
            low[p] = min(low[p], low[to]);
            if(dfn[p] < low[to]) brg[i] = brg[i ^ 1] = 1;
        }
        else if(i != (pre ^ 1)) low[p] = min(low[p], dfn[to]);
    }
}
/*
注意前向星建边,开始值是 cnt = 1,每次 ++ cnt。
*/

  如果说需要求边双,我们直接对于每一个点进行搜索,不走桥边即可。

本质上就是将桥都断开,看联通的极大点集。


点双联通分量和割点

注意是在无向联通图中的。

定义:

  割点:删除了该点,使得图不连通。

  点双联通分量:点集中任意两点间,存在两条本质不同路径,本质不同指不经过相同的点和边(除了这两个点)。

代码实现

  回忆一下我们边双的求法,判定条件是 dfn[p] < low[to],但是我们割点不同。

  即使儿子能到达当前点,但是我们割掉当前点之后还是不连通,所以是 dfn[p] <= low[to]

  可以发现这时我们就不需要考虑父亲节点是否产生贡献了。

  我们可以直接在条件合法的时候将儿子节点的 d f s \tt dfs dfs 树上的节点都加入,同时加入当前节点 u u u

注意: \Large\color{red}\text{注意:} 注意:

  同一个点可能属于多个点双联通分量,所以我们最终建立点双图的时候需要再注意。

  我们需要特判,每个开始遍历的节点,必须遍历两个儿子之后才可能称为割点,但是点双是不影响的。

  单个点也是一个 d c c \tt dcc dcc

  我们考虑把割点都单独建立成新的点,之后遍历每一个点双,将其和其子集中的割点连边即可。

int Rt; // Rt 表示遍历的开始节点
void tarjan(int p) {
    dfn[p] = low[p] = ++ tot, st[++ ed] = p;
    if(!head[p] && rt == p) return dcc[++ Sz].push_back(p), void();
    int fl(0);
    for(int i = head[p];i;i = edg[i].next) {
        int to = edg[i].to;
        if(!dfn[to]) {
            ++ fl;
            tarjan(to);
            low[p] = min(low[p], low[to]);
            if(dfn[p] <= low[to]) {
                if(fl > 1 || p != rt) cut[p] = 1;
                ++ Sz; int y;
                do {
                    y = st[ed --];
                    dcc[Sz].push_back(y);
                }while(y != to);
                dcc[Sz].push_back(p);
            }
        }
        else low[p] = min(low[p], dfn[to]);
    }
}

  下面是重新建图。

int tSz(Sz);
for(i = 1; i <= n; ++ i) if(cut[i]) nid[i] = ++ tSz, bel[i] = tSz;
for(i = 1; i <= Sz; ++ i) {
    for(int v : dcc[i]) if(cut[v]) {
        Add(i, nid[v]), Add(nid[v], i);
    }
    else bel[v] = i;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值