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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/284227.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【mybatis】mapper.xml映射文件

目录 一.概述 二.了解mapper.xml文件 namespaceidresultType指定映射文件的路径 一.概述 mapper.xml 是一个 MyBatis 的映射文件&#xff0c;用于定义 SQL 语句和结果映射。它是一个 XML 文件&#xff0c;通常放置在项目的资源目录下。 随着mybatis框架的发展&#xff0c;myb…

SpringBoot的多环境开发

&#x1f648;作者简介&#xff1a;练习时长两年半的Java up主 &#x1f649;个人主页&#xff1a;程序员老茶 &#x1f64a; ps:点赞&#x1f44d;是免费的&#xff0c;却可以让写博客的作者开心好久好久&#x1f60e; &#x1f4da;系列专栏&#xff1a;Java全栈&#xff0c;…

JupyterHub 如何切换 conda 小环境

JupyterHub 如何切换 conda 小环境 服务器已经部署好 JupyterHub &#xff0c;相关端口请看对应答疑群群公告。在Jupyterhub 中使用 conda 创建的小环境&#xff0c;首先 ssh 登录上服务器或者在 JupyterHub 网页端打开终端 terminal。然后安装 conda &#xff0c;方法请见 Q4&…

Android 一分钟使用RecyclerView完美实现瀑布

【免费】安卓RecyclerView瀑布流效果实现资源-CSDN文库 1.WaterfallFlowActivity 主函数代码&#xff1a; package com.example.mytestapplication;import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; import android.widget.Toast;im…

【亲测好用】DevC++编译出现‘项目没有调试信息,您想打开项目调试选项并重新生成吗’完美解决

DevC不能正常编译 问题描述问题解决 问题描述 问题解决 工具->编译选项 编译器 添加 -g3 在下面命令框 代码生成/优化->链接器->将下面产生调试信息改为Yes 打开调试信息显示&#xff08;工具->环境选项->浏览Debug变量打开&#xff09; 最后一定一定要重新点击…

C语言学习第二十六天(算法的时间复杂度和空间复杂度)

1、算法效率 衡量一个算法的好坏&#xff0c;是从时间和空间两个方面来衡量的&#xff0c;换句话说就是从时间复杂度和空间复杂度来衡量的 这里需要补充一点&#xff1a;时间复杂度是衡量一个算法的运行快慢&#xff0c;空间复杂度是主要衡量一个算法运行所需要的额外空间。 …

Kafka消息延迟和时序性详解(文末送书)

目录 一、概括1.1 介绍 Kafka 消息延迟和时序性1.1.1 什么是 Kafka 消息延迟&#xff1f;1.1.2 为什么消息延迟很重要&#xff1f;1.1.3 什么是 Kafka 消息时序性&#xff1f;1.1.4 消息延迟和时序性的关系 1.2 延迟的来源1.2.1 Kafka 内部延迟 二、衡量和监控消息延迟2.1 延迟…

15.VLAN

VLAN 虚拟局域网 路由器可以隔离广播&#xff0c;但是二层设备交换机就不行了 划分VLAN缩小安全范围 例如发送ARP报文的时候&#xff0c;就只会发送给同一VLAN的设备&#xff0c;而不是整个网络中的所有设备 同一VLAN的用户可以互访&#xff0c;不同VLAN的用户默认是不能互…

安全芯片是什么?为什么可以应用在加密卡上?

安全芯片是指芯片内带有微处理器CPU、随机数发生器、硬件密码算法、存储单元&#xff08;包括随机存储器RAM、程序存储器ROM&#xff08;FLASH&#xff09;、用户数据存储器EEPROM&#xff09;以及芯片操作系统COS的智能芯片&#xff0c;相当于一台微型计算机&#xff0c;不仅具…

活动 | Mint Blockchain 将于 2024 年 1 月 10 号启动 MintPass 限时铸造活动

MintPass 是由 Mint Blockchain 官方发行的 Mint 网络和社区的 NFT 通行证&#xff0c;将在 2024 年 1 月份启动限时铸造活动。今天这篇文章会着重向大家介绍即将举办的 MintPass 活动的基础信息。 MintPass 有 2 种类型&#xff1a; 类型 1&#xff1a;Mint Genesis NFT Mint…

如何对大模型进行评估下

如果从实现评估的纬度来分&#xff0c;可以将不同类型的评估分为三类&#xff0c;具体如下所示。更多理论的详细信息可以参见博客《如何对大模型进行评估上》。接下来就从第一种类型出发&#xff0c;看看评估脚本是如何实现的。这里分析的源代码是Qwen的评估脚本。 如何使用选择…

字符设备驱动框架的编写

一. 简介 我们在学习裸机或者 STM32 的时候关于驱动的开发就是初始化相应的外设寄存器&#xff0c;在 Linux 驱动开发中&#xff0c;肯定也是要初始化相应的外设寄存器。 只是在 Linux 驱动开发中&#xff0c; 我们需要按照其规定的框架来编写驱动&#xff0c;所以说学 …