60 分钟搞定图论中的 Tarjan 算法(一)

2024-04-12 02:32

本文主要是介绍60 分钟搞定图论中的 Tarjan 算法(一),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Tarjan 算法是图论中非常实用 / 常用的算法之一,能解决强连通分量,双连通分量,割点和桥,求最近公共祖先(LCA)等问题。

关于 Tarjan 算法,笔者将用一系列文章系统介绍 Tarjan 算法的原理以及其主要解决的问题。本篇文章我们主要介绍如何使用 Tarjan 算法求解无向图的割点与桥

我们先来简单地了解下什么是 Tarjan 算法?

一、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。

 

四、代码详解

在了解了 Tarjan 算法的基本概念与操作流程之后,我们来看看具体的代码。这里推荐大家学习一篇 GitHub 上介绍 Tarjan 算法 的文章。

无向图的桥判定法则

在一张无向图中,判断边 e (其对应的两个节点分别为 u 与 v)是否为桥,需要其满足如下条件即可:dfn[u] < low[v]

它代表的是节点 u 被访问的时间,要优先于(小于)以下这些节点被访问的时间 —— low[v] 。

  • 以节点 v 为根的搜索树中的所有节点
  • 通过一条非搜索树上的边,能够到达搜索树的所有节点(在追溯值内容中有所解释)

是不是上面的两个条件很眼熟?对,其实就是前文提到的追溯值 —— low[v]。

C++ 实现

// 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]);
}

首先,我们来看看变量的含义。

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];
  • SIZE —— 节点的个数,可以基于实际的节点个数增加一些冗余度
  • 关于 head / Next / ver 变量的介绍,可以参阅下面的动画配合理解
  • 假设场景,有一个节点 u,以及其直接相连的若干节点 v1,v2,v3
  • head[u] 代表节点 u 直接相邻的第一个节点 v1 的序号(tot1)
  • Next[tot1] 代表节点 u 直接相邻的下一个节点的序号(tot2)
  • ver[tot1] 代表序号为 tot1 的节点的值(v1)
  • dfn[x] 代表节点 x 对应的时间戳,low[x] 代表节点 x 的追溯值
  • n 与 m 代表程序的标准输入的参数,n 代表节点的个数,m 代表边的个数
  • tot 代表每一个节点的序号
  • num 代表每一个节点的时间戳
  • bridge[tot] 代表序号为 tot 的边是否为桥

针对于 head、Next、ver 变量的关系还是比较难懂。那么,我们用图像的方式,把节点 u、v1、v2 与 v3 画出来看看。

通过上图我们可以形象地理解这三个变量之间的关系。那么,针对于 add 方法,想必也可以很顺利地想明白 —— 建立并维护节点 x、y 形成的边。

接着,我们看看 Tarjan 算法的主体

// 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

这句话是为了标记某个节点对应的边是桥。而又因为我们在建立边时是成对地,那么相邻的两个节点都应该被标记。

 

​七、实践题

力扣 1192. 查找集群内的「关键连接」

题目描述

力扣数据中心有 n 台服务器,分别按从 0 到 n-1 的方式进行了编号。

它们之间以「服务器到服务器」点对点的形式相互连接组成了一个内部集群,其中连接connections是无向的。

从形式上讲,connections[i] = [a, b] 表示服务器a和b之间形成连接。任何服务器都可以直接或者间接地通过网络到达任何其他服务器。

「关键连接」是在该集群中的重要连接,也就是说,假如我们将它移除,便会导致某些服务器无法访问其他服务器。

请你以任意顺序返回该集群内的所有 「关键连接」。

示例 1

 

 

输入:n = 4, connections = [[0,1],[1,2],[2,0],[1,3]]
输出:[[1,3]]
解释:[[3,1]] 也是正确的。

 

​提示

  • 1 <= n <= 10^5
  • n-1 <= connections.length <= 10^5
  • connections[i][0] != connections[i][1]
  • 不存在重复的连接

这道题描述非常直接 —— 求解无向图的桥,大家可以试试使用上述的 Tarjan 算法进行求解。

上面的代码,大家可以保留下来作为一个模板来学习或使用。

 

总结

想必通过对 Tarjan 算法的基本概念,无向图以及其桥与割点的概念,Tarjan 算法求解桥与割点的代码的详细介绍,大家已经能够掌握了这个知识点。

在学习 Tarjan 算法的过程中,很多小伙伴会遇到无法理解代码的窘境。在这里教大家一个小技巧:可以使用一个简单的例子,在白板上进行一个简单的模拟演练,把代码的执行过程可视化形象化。

最后留一道思考题

无向图的桥与割点的判定在实际生活中有哪些应用?可以在评论区留言。

 

本文作者:胡小旭

声明:本文归 “力扣” 版权所有,如需转载请联系。文中图片和视频为作者“胡小旭”制作,未经允许严禁修改和翻版使用。

发布于 01-09

https://zhuanlan.zhihu.com/p/101923309

这篇关于60 分钟搞定图论中的 Tarjan 算法(一)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/895854

相关文章

Python中的随机森林算法与实战

《Python中的随机森林算法与实战》本文详细介绍了随机森林算法,包括其原理、实现步骤、分类和回归案例,并讨论了其优点和缺点,通过面向对象编程实现了一个简单的随机森林模型,并应用于鸢尾花分类和波士顿房... 目录1、随机森林算法概述2、随机森林的原理3、实现步骤4、分类案例:使用随机森林预测鸢尾花品种4.1

Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单

《Springboot的ThreadPoolTaskScheduler线程池轻松搞定15分钟不操作自动取消订单》:本文主要介绍Springboot的ThreadPoolTaskScheduler线... 目录ThreadPoolTaskScheduler线程池实现15分钟不操作自动取消订单概要1,创建订单后

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系

康拓展开(hash算法中会用到)

康拓展开是一个全排列到一个自然数的双射(也就是某个全排列与某个自然数一一对应) 公式: X=a[n]*(n-1)!+a[n-1]*(n-2)!+...+a[i]*(i-1)!+...+a[1]*0! 其中,a[i]为整数,并且0<=a[i]<i,1<=i<=n。(a[i]在不同应用中的含义不同); 典型应用: 计算当前排列在所有由小到大全排列中的顺序,也就是说求当前排列是第

MySQL数据库宕机,启动不起来,教你一招搞定!

作者介绍:老苏,10余年DBA工作运维经验,擅长Oracle、MySQL、PG、Mongodb数据库运维(如安装迁移,性能优化、故障应急处理等)公众号:老苏畅谈运维欢迎关注本人公众号,更多精彩与您分享。 MySQL数据库宕机,数据页损坏问题,启动不起来,该如何排查和解决,本文将为你说明具体的排查过程。 查看MySQL error日志 查看 MySQL error日志,排查哪个表(表空间

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

综合安防管理平台LntonAIServer视频监控汇聚抖动检测算法优势

LntonAIServer视频质量诊断功能中的抖动检测是一个专门针对视频稳定性进行分析的功能。抖动通常是指视频帧之间的不必要运动,这种运动可能是由于摄像机的移动、传输中的错误或编解码问题导致的。抖动检测对于确保视频内容的平滑性和观看体验至关重要。 优势 1. 提高图像质量 - 清晰度提升:减少抖动,提高图像的清晰度和细节表现力,使得监控画面更加真实可信。 - 细节增强:在低光条件下,抖

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

poj 3974 and hdu 3068 最长回文串的O(n)解法(Manacher算法)

求一段字符串中的最长回文串。 因为数据量比较大,用原来的O(n^2)会爆。 小白上的O(n^2)解法代码:TLE啦~ #include<stdio.h>#include<string.h>const int Maxn = 1000000;char s[Maxn];int main(){char e[] = {"END"};while(scanf("%s", s) != EO

秋招最新大模型算法面试,熬夜都要肝完它

💥大家在面试大模型LLM这个板块的时候,不知道面试完会不会复盘、总结,做笔记的习惯,这份大模型算法岗面试八股笔记也帮助不少人拿到过offer ✨对于面试大模型算法工程师会有一定的帮助,都附有完整答案,熬夜也要看完,祝大家一臂之力 这份《大模型算法工程师面试题》已经上传CSDN,还有完整版的大模型 AI 学习资料,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费