Tarjan
Tarjan 算法是基于深度优先搜索的算法,用于求解图的连通性问题。Tarjan 算法可以在线性时间内求出无向图的割点与桥,进一步地可以求解无向图的双连通分量;同时,也可以求解有向图的强连通分量、必经点与必经边。
如果你对上面的一些术语不是很了解,没关系,我们只要知道 Tarjan 算法是基于深度优先搜索的,用于求解图的连通性问题的算法 就好了。
提到 Tarjan,不得不提的就是算法的作者 ——Robert Tarjan。他是一名著名的计算机科学家,我们耳熟能详的 最近公共祖先(LCA)问题、强连通分量 问题、双连通分量 问题的高效算法都是由他发现并解决的,同时他还参与了开发斐波那契堆、伸展树 的工作。
无向图的割点与桥
什么是无向图?简单来说,若一个图中每条边都是无方向的,则称为无向图。
割点
若从图中删除节点 x 以及所有与 x 关联的边之后,图将被分成两个或两个以上的不相连的子图,那么称 x 为图的割点。
桥
若从图中删除边 e 之后,图将分裂成两个不相连的子图,那么称 e 为图的桥或割边。
如何求解图的割点与桥?
在了解了 Tarjan 算法的背景以及图的割点与桥的基本概念之后,我们下面所面临的问题就是 —— 如何求解图的割点与桥?
开门见山,我们直接引出 Tarjan 算法在求解无向图的割点与桥的工作原理。
时间戳
时间戳是用来标记图中每个节点在进行深度优先搜索时被访问的时间顺序,当然,你可以理解成一个序号(这个序号由小到大),用 dfn[x] 来表示。
搜索树
在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,我们可以称之为“无向连通图的搜索树”。
追溯值
追溯值用来表示从当前节点 x 作为搜索树的根节点出发,能够访问到的所有节点中,时间戳最小的值 —— low[x]。那么,我们要限定下什么是“能够访问到的所有节点”?,其需要满足下面的条件之一即可:
以 x 为根的搜索树的所有节点
通过一条非搜索树上的边,能够到达搜索树的所有节点
为了方便理解,让我们通过动画的方式来模拟追溯值真实计算过程。
在上面的计算过程中,我们可以认为以序号 2 为根的搜索树的节点有 {2,3,4,5}。上面所说的“通过一条非搜索树上的边”可以理解成动画中的(1,5)这条边,“能够到达搜索树的所有节点”即为节点 1。
无向图的桥判定法则
在一张无向图中,判断边 e (其对应的两个节点分别为 u 与 v)是否为桥,需要其满足如下条件即可:dfn[u] < low[v]
它代表的是节点 u 被访问的时间,要优先于(小于)以下这些节点被访问的时间 —— low[v] 。
以节点 v 为根的搜索树中的所有节点
通过一条非搜索树上的边,能够到达搜索树的所有节点(在追溯值内容中有所解释)
是不是上面的两个条件很眼熟?对,其实就是前文提到的追溯值 —— low[v]。
// x 代表当前搜索树的根节点,in_edge 代表其对应的序号(tot)
void tarjan(int x, int in_edge) {
// 在搜索之前,先初始化节点 x 的时间戳与追溯值
dfn[x] = low[x] = ++num;
// 通过 head 变量获取节点 x 的直接连接的第一个相邻节点的序号
// 通过 Next 变量,迭代获取剩下的与节点 x 直接连接的节点的序号
for (int i = head[x]; i; i = Next[i]) {
// 此时,i 代表节点 y 的序号
int y = ver[i];
// 如果当前节点 y 没有被访问过
if (!dfn[y]) {
// 递归搜索以 y 为跟的子树
tarjan(y, i);
// 计算 x 的追溯值
low[x] = min(low[x], low[y]);
// 桥的判定法则
if (low[y] > dfn[x])
bridge[i] = bridge[i ^ 1] = true; // 标记当前节点是否为桥(具体见下文)
}
else if (i != (in_edge ^ 1)) // 当前节点被访问过,且 y 不是 x 的“父节点”(具体见下文)
low[x] = min(low[x], dfn[y]);
}
}
语句一:i != (in_edge ^ 1)
首先,我们先明确下“y 不是 x 的父节点”的情况是什么?
如上面视频的过程,以 x' 节点出发进行深度优先搜索,紧接着搜索到节点 x。此时,以 x 为根进行递归搜索,计算出其下一个节点为 y。如果此时 y 与 x' 是一个节点的话 —— y 是 x 的“父节点”,需要忽略这种情况对于追溯值的计算。
我们知道,在建立边的关系时(add),我们为每一条边的两个节点创建了两个相邻的序号值。又因我们 tot 是从 2 开始计数的,故每一条边的两个节点的序号肯定是 一奇一偶,偶数为小 。比如,2 与 3,4 与 5,而不会出现 5 与 6 这样的情况。
在明确了上面的情况之后,我们看看一个数 x 与 1 进行异或的结果是什么?
- 如果 x 为偶数(2),那么 x ^ 1 = 2 ^ 1 = 3
- 如果 x 为奇数(3),那么 x ^ 1 = 3 ^ 1 = 2
最后,我们来想想如何判定两个点是否属于一条边的两个端点?是不是只要满足 a ^ 1 == b 条件,那么 a 与 b 就是一条边的两个端点了?对,就是这样!
语句二:bridge[i] = bridge[i ^ 1] = true
这句话是为了标记某个节点对应的边是桥。而又因为我们在建立边时是成对地,那么相邻的两个节点都应该被标记。
// tarjan 算法求无向图的桥
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
const int SIZE = 100010;
int head[SIZE], ver[SIZE * 2], Next[SIZE * 2];
int dfn[SIZE], low[SIZE];
int n, m, tot, num;
bool bridge[SIZE * 2];
void add(int x, int y) {
ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
}
void tarjan(int x, int in_edge) {
dfn[x] = low[x] = ++num;
for (int i = head[x]; i; i = Next[i]) {
int y = ver[i];
if (!dfn[y]) {
tarjan(y, i);
low[x] = min(low[x], low[y]);
if (low[y] > dfn[x])
bridge[i] = bridge[i ^ 1] = true;
}
else if (i != (in_edge ^ 1))
low[x] = min(low[x], dfn[y]);
}
}
int main() {
// [[0,1],[1,2],[2,0],[1,3]]
cin >> n >> m;
tot = 1;
for (int i = 1; i <= m; i++) {
int x, y;
scanf("%d%d", &x, &y);
add(x, y), add(y, x);
}
for (int i = 1; i <= n; i++)
if (!dfn[i]) tarjan(i, 0);
for (int i = 2; i < tot; i += 2)
if (bridge[i])
printf("%d %d\n", ver[i ^ 1], ver[i]);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】