Tarjan(连通性相关) 笔记

概念有点多

点(vertex)边(edge)

  • 无向图中

若图中存在两点可以到达,则称这两个点是 连通的(connected)

若图中任意两点都连通,则称该无向图为 连通图(connected graph)

若图 G 中存在一个连通子图 HHG),没有严格更大的连通子图 I 使 HI,则称 HG连通分量(connected component)(极大连通子图)


若图中删去某一条边,会使这个连通图分裂成两个不相连的子图,则称这条边为 桥 / 割边,类似地,若删去某一个点(以及与它相连的边),使所在的连通图分裂,则称这个点为 割点

(有割点不一定有桥,有桥不一定有割点)

image

若图中不存在割边,则称该图为 边双连通图(edge double connected graph)

若图中不存在割点,则称该图为 点双连通图(vertex double connected graph)

无向图的极大边双连通子图称为 边双连通分量(edge double connected component,e-DCC)

无向图的极大点双连通子图称为 点双连通分量(vertex double connected component,v-DCC)

  • 有向图中

若图中存在单向路径 u -> v,则称 u 可达 v

若图中任意两点都互相可达,则称该有向图为 强连通图(strongly connected graph)

若不强连通的有向图把有向边变为无向边的无向图为连通图,则称该图为 弱连通图(weakly connected graph)

与无向图的连通分量类似,有 强连通分量(strongly connected component,SCC)(极大强连通子图)、弱连通分量(weakly connected component)(极大弱连通子图)


dfs 搜索一个有向图 G,产生一颗搜索树,对于 G 中的边有分类:

  • 树枝边(tree edge),在搜索树中就有的边,即搜索过程经过的边
  • 反祖边(back edge),指向祖先的边
  • 前向边(forword edge),指向子树的边(实际上没屁用的概念 qwq,因为显然这种边放到搜索树中不可能形成环)
  • 横叉边(cross edge),右子树的点指向左子树的点的边(左子树指向未访问过的右子树,实际上就是一条树枝边)

强连通分量 scc

若一个点在一个 scc 中,则它一定在一个环路中,若搜索树中要形成环,只有两种情况

  • 一是直接有返祖边
  • 二是经过横叉边,再向上经过反祖边

引入两个变量:

dfn[u] 表示 u 的时间戳;low[u] 表示 从 u 开始往下走,能达到的最小时间戳

判定:low[u] = dfn[u]

此时该点是所属 scc 的“最高点”,因为若还能往上走形成更大的环,则一定有 low[u] < dfn[u]

在这里也说明,在 dfs 时,我们一定要更新的是 low,避免 scc 的节点提前被判定(单个点是显然成立的)

时间复杂度 O(n+m)

code
void tarjan(int u)
{
low[u] = dfn[u] = ++ idx;
in_stk[u] = true;
stk.push(u);
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (!dfn[v]) // tree edge
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if (in_stk[v]) // back edge / cross edge
low[u] = min(low[u], low[v]);
}
if (low[u] == dfn[u])
{
int ver;
do
{
ver = stk.top();
stk.pop();
in_stk[ver] = false;
... // id / size ...
}while (ver != u); // (先执行再判断)多执行一次,方便将 u 也一同弹出
cnt ++;
}
}

scc 缩点

因为每个 scc 里的点都是可以互相到达的,所以很多时候都可以把环看成一个点

发现一般的有向图缩点后,会形成 DAG,在 DAG 上就可以进行很多别的操作啦

image

想要实现缩点也很 easy

只要再遍历一遍所有点以及它的邻点,若两点分属不同的 scc,则将两 scc 的 id 连一条边即可。

还有 DAG 上求拓扑序的问题,有一个结论:scc 的递减 id 序列就是 DAG 的拓扑序

简单理解一下就是,从上往下递归,到叶子节点赋予小的 scc id,往上回溯时 scc id 变大。


割边/桥 -> 边双连通分量 e-DCC

有性质:e-DCC 中的任意两点都存在有两条没有公共边的路径


要求 e-DCC,也就是把图中的割边求出来,剩下的连通块就是 e-DCC

那么,对于一条边 x -> y,若它是桥,则,桥判定:dfn[x] < low[y]

感性理解一下就是,y 这个点,除了 x -> y 这条边,没有别的边可以让 low[y] 变得更小了,也就是桥,那么 y 所在的子树也就是 e-DCC

注意:在跑无向图构成的搜索树中,显然是没有 横叉边 和 返祖边的存在的

求完桥,dfs 搜一遍即可。

tip:对于无向图,用边的编号判重边,用 ^1 时,链式前向星存图边计数器 idx 初始化 idx = 1,这样存双向边就能 2, 3 4, 5 ..... 成对编号

code 1
void tarjan(int u, int id)
{
low[u] = dfn[u] = ++ idx;
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (dfn[u] < low[v])
bridge[i] = bridge[i ^ 1] = true;
}
else if (i != (id ^ 1))
low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u)
{
edcc[u] = cnt;
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (edcc[v] || bridge[i]) continue;
dfs(v);
}
}

同时,还有一种方法,其实在构成搜索树时,我们就把这个无向图看成了有向图,每条边至多访问一遍,被抽象为有向图的无向图中的一个强连通分量,在原图中是一定一个边双联通分量。那么可以仿照求 scc 的思路求 e-dcc

code 2
void tarjan(int u, int id)
{
low[u] = dfn[u] = ++ idx;
stk.push(u);
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
}
else if (i != (id ^ 1))
low[u] = min(low[u], dfn[v]);
}
if (dfn[u] == low[u])
{
...
}
}

e-DCC 缩点

我们在求 e-DCC 时,标记了割边的编号

那么缩点就很容易了,只需便利一遍所有边,若为割边,则对连点所在的 e-DCC 连边

可以发现,缩完点后,图中只剩下割边,也就是形成了一棵树或森林


割点 -> 点双连通分量 v-DCC

对于图中某个点 x 及其儿子 y, 割点判定:

  • xrootdfn[x]low[y]
  • x=root,则应至少存在两组 y1,y2,使 dfn[x]low[y1], low[y2]

因为在搜索树中,非根节点(且有儿子,非叶子节点)一定至少有两条边连着它,而根节点不一定有多个儿子节点。

tip:注意,这里求割点,对于已访问的入点,必须 low[u] = min(low[u], dfn[v]),而不能用入点的 low[v] 更新,因为如果出现了入点已访问过,那么说明出现了环,而注意到我们的判定 dfn[x]low[y] 的实际含义是入点在 不经过出点 的情况下,不能绕行其他节点到达更早访问的点,则可以判定出点为割点。

所以若用 low[v] 更新,则代表 穿过出点,不合法。

求割点 code
void tarjan(int u) // 每次传入 u = root
{
low[u] = dfn[u] = ++ idx;
int cnt = 0;
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v])
{
cnt ++;
if (u != root || cnt > 1) cut[u] = true;
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
求 v-dcc code
void tarjan(int u)
{
low[u] = dfn[u] = ++ idx;
stk.push(u);
// int kid = 0;
if (u == root && h[u] == 0)
{
vdcc[++ cnt].push_back(u);
return;
}
for (re i = h[u]; i; i = e[i].next)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (dfn[u] <= low[v])
{
// kid ++;
// if (u != root || kid > 1) cut[u] = true;
cnt ++;
int ver;
do
{
ver = stk.top(); stk.pop();
vdcc[cnt].push_back(ver);
}while (ver != v); // 注意这里是个易错点,不要打成 ver != u
vdcc[cnt].push_back(u);
}
}
else
low[u] = min(low[u], dfn[v]);
}
}

v-DCC 缩点

image

对割点 裂点

所以,我们其实可以得到一颗节点数为 点双数 + 割点数 的树或森林,

考虑遍历整个图,对于每个 v-DCC,把它与它包含的割点连边即可。


最后,如果一道题需要无向图缩点,而并不好确定是 点双缩点 还是 边双缩点,注意:

边双是定义在点上的,即每个点只属于一个边双;点双是定义在边上的,即每条边只属于一个点双


圆方树

这里我讨论的仅仅是用于处理一般图的广义圆方树,仙人掌?是什么,能吃吗 qwq

圆方树,可以说是一种思想,这里我看到主要是用来处理 有环无向图的必经点问题

首先,两点的必经点在无向图上,也就是割点,所以,圆方树就是定义在点双上的

具体地,把原图中的点叫作圆点,而对于每一个点双,我们可以增加一个代表点,叫做方点,

将图重新构造,令所有圆点都连向所在点双(可能多个)的方点上,可以构造出一颗树!而这颗树与原无向图是等价的,圆点维护原来点上的信息,而方点则可以维护该点双上的信息

image

有树就非常好了

这样要做必经点的问题,就相当于是求给定两点在圆方树上的简单路径上的圆点数量呗,lca 乱搞 ok

练习:

P3854 [TJOI2008] 通讯网破坏
P4320 道路相遇
P5058 [ZJOI2004] 嗅探器
P4606 [SDOI2018] 战略游戏(由两点拓展为 k 个点,可以用虚树做,但我不会,当然也可以维护一个 dfn 序做)

posted @   Zhang_Wenjie  阅读(12)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 清华大学推出第四讲使用 DeepSeek + DeepResearch 让科研像聊天一样简单!
· 推荐几款开源且免费的 .NET MAUI 组件库
· 实操Deepseek接入个人知识库
· 易语言 —— 开山篇
· Trae初体验
点击右上角即可分享
微信分享提示