SCC-Tarjan算法,强连通分量算法,从dfs到Tarjan详解

2023-12-18 15:04

本文主要是介绍SCC-Tarjan算法,强连通分量算法,从dfs到Tarjan详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 定义
      • 强连通
      • 强连通分量
    • Tarjan算法原理及实现
      • 概念引入
        • 搜索树
        • 有向边的分类
        • 强连通分量的根
        • 时间戳
        • 追溯值
      • 算法原理
        • 从深搜到Tarjan
        • Tarjan算法流程
        • Tarjan算法代码实现
    • OJ练习:

前言

强连通分量是图论中的一个重要概念,它在许多领域都有广泛的应用,如网络路由中识别环路,社交网络分析,编译器优化识别出代码中的循环结构,图像处理中识别出图像中的连通区域,从而进行图像分割和特征提取等。因而了解强连通分量的概念以及其求解算法是十分重要的。我们本文介绍Tarjan算法,Tarjan算法在双连通分量求解中也有应用,我们后续博客中也会介绍。


定义

强连通

若一张有向图的节点两两互相可达,则称这张图是**强连通(SC,strongly connected)**的。

强连通分量

强连通分量(Strongly Connected Components, SCC): 极大的强连通子图。
如下图中G1 = {1,2,3,4,9} 和 G2 = {5,6,8} 以及 G3 = {7}就是三个强连通分量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Tarjan算法原理及实现

概念引入

搜索树

我们dfs对图遍历,保证每个点只访问一次,访问过的节点和边构成一棵有向树,我们称之为搜索树

如我们上图从1开始深搜遍历就会得到如下搜索树。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

有向边的分类

深搜对于有向边的访问分为四类:

1.树边(tree edge):访问节点走过的边。图中的黑色边。
2.返祖边(back edge):指向祖先节点的边。图中的红色边。
3.横叉边(cross edge):右子树指向左子树的边。图中的绿色边。.
4.前向边(forward edge):指向子树中节点的边。图中的蓝色边。

我们不难发现返祖边必和树边构成环。横叉边可能和返祖边构成环。

前向边无用,因为前向边如果作为某个环中的边,必有一个更大的环替代该环。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

强连通分量的根

如果节点x是某个强连通分量在搜索树中遇到的第一个节点,那么这个强连通分量的其余节点肯定是在搜索树中以x为根的子树中。节点x被称为这个强连通分量的根

时间戳

我们用数组dfn[]来保存节点第一次访问时间,dfn[x]即节点x第一次访问的时间戳

追溯值

数组low[]来记录每个节点出发能够访问的最早时间戳,记low[x]节点x出发能够访问的最早时间戳,即追溯值

算法原理

从深搜到Tarjan

Tarjan算法通过记录深搜遍历中每个节点的第一次访问时间来找到强连通分量的根以及其余节点。

前面已经讲了Tarjan是基于深搜的,所以我们的Tarjan实际上是在深搜遍历图的模板上加以修改而实现的

我们先给出链式前向星存图的深搜代码。(关于链式前向星,详见:一种实用的边的存储结构–链式前向星-CSDN博客)

void tarjan(int x)//深搜模板
{//链式前向星存图for (int j = head[x]; ~j; j = edges[j].nxt){int v = edges[j].v;if(vis[v]) continue;vis[v] = 1;tarjan(v);}
}

即然Tarjan算法是跟时间戳和追溯值有关的,我们如何在深搜过程中完成对二者的记录呢?

我们用全局变量tot来记录时间,由于上面深搜代码保证了进入函数的都是未访问即第一次访问的节点,所以我们进入函数体的时候记录dfn[x]为++tot,并初始化low[x]为dfn[x],则有如下代码

void tarjan(int x)//深搜模板
{dfn[x] = low[x] = ++tot;for (int j = head[x]; ~j; j = edges[j].nxt){int v = edges[j].v;if(vis[v]) continue;vis[v] = 1;tarjan(v);}
}

对于dfn的值是无可指摘的,但是对于非强连通分量根节点x的low值显然不是dfn[x],那么我们如何在遍历过程中去维护low呢?

如果相邻节点v未访问,那么我们先对v深搜,然后用v的low值更新low[x]

如果已经访问过了,直接用v的low值更新low[x],则有

void tarjan(int x)//深搜模板
{dfn[x] = low[x] = ++tot;for (int j = head[x]; ~j; j = edges[j].nxt){int v = edges[j].v;if (!dfn[v])//预先初始化的dfn全为0,时间戳为0说明还未访问{tarjan(v);low[x] = min(low[x], low[v]);}else{low[x] = min(low[x], low[v]);}}
}

我们现在似乎只是完成了深搜遍历图以及记录dfn和low罢了,还没有真正的涉及到强连通分量的求解。

对于一个强连通分量的根来讲,当它的访问完毕后,它所在的强连通分量所有节点必然都已经访问过,也就是说我们需要从根的时间戳往后的访问节点中找到其强连通分量内的其他点。

这就要求我们用一个数据结构来存储访问过的点,这里我们选择使用,因为这样能保证先访问的在栈底,强连通分量中后访问的节点在栈中位置都在根节点上方,而我们如何判断根呢?我们知道根是所在强连通分量第一个访问的点,所以一定满足dfn[root] == low[root]

我们回看最初的图和搜索树

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们在深搜的过程中标记时间戳的同时将节点入栈,那么我们按照搜索树往下走:

  • 发现最先访问完的节点是3,此时栈中元素为{1,2,3},我们结合原图来看发现1,2,3在一个强连通分量内,但是由于3不是根,不能保证所在强连通分量遍历完了,所以不做操作
  • 接下来2遍历完了,和3一样,不做操作。
  • 回到1后,接着从4往下遍历。到7的时候,7又成为了访问完的节点,我们发现dfn[7] == low[7] = 7,所以7是其所在强连通分量的根,其所在强连通分量其它点都在栈中,我们发现7在栈顶,所以7所在的强连通分量只有7,到此我们将7弹栈
  • 接着遍历,此时到8的时候又发现8也访问完了,由于8不是根,所以不操作,这样一直下去回到了5,我们发现dfn[5] == low[5] = 5,所以5是其所在强连通分量的根,其所在强连通分量其它点都在栈中,我们此时可以发现栈中5之上的正好是其所在强连通分量内的其他点,因为之前我们已经将7弹栈了,这样我们又处理了5所在强连通分量{5,6,8}
  • 接着往下走,我们仿照先前的步骤,又得到了强连通分量{1,2,3,4,9}
  • 至此所有强连通分量已经得到:{1,2,3,4,9},{5,6,8},{7}

所以我们可以发现,时间戳dfn和追溯值low可以帮我们判定强连通分量的根,从而从栈中拿出其所在强连通分量的节点,且先访问完的强连通分量已经弹栈,不会对后访问完的强连通分量造成影响(更严格的证明可以用反证法自己试着证明一下)。

而且我们也发现一点,由于low值只跟自己强连通分量内点的low值有关,所以我们上面深搜代码中维护low值的部分第二个分支判断可改为在栈中,因为如果不在栈中而且访问过了说明它所在的强连通分量已经解决了,跟当前点无关。如下:

        if (!dfn[v])//预先初始化的dfn全为0,时间戳为0说明还未访问{tarjan(v);low[x] = min(low[x], low[v]);}else if(instk[v]{low[x] = min(low[x], low[v]);}
Tarjan算法流程

我们到这里就可以得出Tarjan的算法流程:

  1. 访问x时,给时间戳、初始化low、入栈。枚举x的邻点v,分三种情况。
  2. 若v尚未访问:对v深搜。回x时,用low[v]更新low[x]。因为x是v的父节点,v能访问到的点,x也一定也能访问到
  3. 若v已访问且在栈中:说明y是祖先节点或左子树节点,用dfn[v]更新low[x]。(注意,和我们前面分析流程不一样了)
  4. 若v已访问且不在栈中:说明v已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。
  5. 离x时,记录SCC。只有遍历完一个SCC, 才可以出栈。更新low值的意义:避免SCC的节点提前出栈。

注意我们前面一直讲用low[v]更新low[x],但是上面流程中用了dfn[v]来更新,如果换成low[v]的话可以负责的讲在SCC问题中绝对正确,但是如果是无向图的双连通分量问题就会出错了,这个和割点有关,后续再写另一篇博客介绍。

Tarjan算法代码实现
#define N 10010
#define M 50010
struct edge
{int v, nxt;
} edges[M];
int head[N]{0}, idx = 0;
void addedge(int u, int v)
{edges[idx] = {v, head[u]};head[u] = idx++;
}
int dfn[N]{0}, low[N]{0}, tot = 0; // tot访问节点的时间戳编号
// dfn 时间戳 low节点所能访问的最小时间戳 tot数目
int s[N], top = 0; // 辅助栈
bitset<N> instk;   // 在栈内?
int scc[N], sz[N], cnt = 0;
// scc 节点所在SCC的编号 sz SCC的大小 cntscc数目
int ans = 0;
void tarjan(int x)
{dfn[x] = low[x] = ++tot;s[top++] = x, instk[x] = 1;for (int j = head[x]; ~j; j = edges[j].nxt){if (!dfn[edges[j].v]){tarjan(edges[j].v);low[x] = min(low[x], low[edges[j].v]);}else if (instk[edges[j].v]){// y要么是祖先要么是横叉边的点// 计算强连通dfn可以换成low,但是在计算双连通替换会出错low[x] = min(low[x], dfn[edges[j].v]);}}if (low[x] == dfn[x]){int y;cnt++;do{y = s[--top];instk[y] = 0;scc[y] = cnt;sz[cnt]++;} while (x != y);}
}

OJ练习:

P2863 [USACO06JAN] The Cow Prom S

这篇关于SCC-Tarjan算法,强连通分量算法,从dfs到Tarjan详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

一文深入详解Python的secrets模块

《一文深入详解Python的secrets模块》在构建涉及用户身份认证、权限管理、加密通信等系统时,开发者最不能忽视的一个问题就是“安全性”,Python在3.6版本中引入了专门面向安全用途的secr... 目录引言一、背景与动机:为什么需要 secrets 模块?二、secrets 模块的核心功能1. 基

一文详解MySQL如何设置自动备份任务

《一文详解MySQL如何设置自动备份任务》设置自动备份任务可以确保你的数据库定期备份,防止数据丢失,下面我们就来详细介绍一下如何使用Bash脚本和Cron任务在Linux系统上设置MySQL数据库的自... 目录1. 编写备份脚本1.1 创建并编辑备份脚本1.2 给予脚本执行权限2. 设置 Cron 任务2

一文详解如何在idea中快速搭建一个Spring Boot项目

《一文详解如何在idea中快速搭建一个SpringBoot项目》IntelliJIDEA作为Java开发者的‌首选IDE‌,深度集成SpringBoot支持,可一键生成项目骨架、智能配置依赖,这篇文... 目录前言1、创建项目名称2、勾选需要的依赖3、在setting中检查maven4、编写数据源5、开启热

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安

HTML5 搜索框Search Box详解

《HTML5搜索框SearchBox详解》HTML5的搜索框是一个强大的工具,能够有效提升用户体验,通过结合自动补全功能和适当的样式,可以创建出既美观又实用的搜索界面,这篇文章给大家介绍HTML5... html5 搜索框(Search Box)详解搜索框是一个用于输入查询内容的控件,通常用于网站或应用程

Python中使用uv创建环境及原理举例详解

《Python中使用uv创建环境及原理举例详解》uv是Astral团队开发的高性能Python工具,整合包管理、虚拟环境、Python版本控制等功能,:本文主要介绍Python中使用uv创建环境及... 目录一、uv工具简介核心特点:二、安装uv1. 通过pip安装2. 通过脚本安装验证安装:配置镜像源(可

C++ 函数 strftime 和时间格式示例详解

《C++函数strftime和时间格式示例详解》strftime是C/C++标准库中用于格式化日期和时间的函数,定义在ctime头文件中,它将tm结构体中的时间信息转换为指定格式的字符串,是处理... 目录C++ 函数 strftipythonme 详解一、函数原型二、功能描述三、格式字符串说明四、返回值五

LiteFlow轻量级工作流引擎使用示例详解

《LiteFlow轻量级工作流引擎使用示例详解》:本文主要介绍LiteFlow是一个灵活、简洁且轻量的工作流引擎,适合用于中小型项目和微服务架构中的流程编排,本文给大家介绍LiteFlow轻量级工... 目录1. LiteFlow 主要特点2. 工作流定义方式3. LiteFlow 流程示例4. LiteF

CSS3中的字体及相关属性详解

《CSS3中的字体及相关属性详解》:本文主要介绍了CSS3中的字体及相关属性,详细内容请阅读本文,希望能对你有所帮助... 字体网页字体的三个来源:用户机器上安装的字体,放心使用。保存在第三方网站上的字体,例如Typekit和Google,可以link标签链接到你的页面上。保存在你自己Web服务器上的字

MySQL存储过程之循环遍历查询的结果集详解

《MySQL存储过程之循环遍历查询的结果集详解》:本文主要介绍MySQL存储过程之循环遍历查询的结果集,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录前言1. 表结构2. 存储过程3. 关于存储过程的SQL补充总结前言近来碰到这样一个问题:在生产上导入的数据发现