应用:线性时间内求出无向图的割点与桥,双连通分量。有向图的强连通分量,必经点和必经边。

主要是求两个东西,dfn和low

时间戳dfn:就是dfs序,也就是每个节点在dfs遍历的过程中第一次被访问的时间顺序。

追溯值low:$low[x]$定义为$min(dfn[subtree(x)中的节点], dfn[通过1条不再搜索树上的边能到达subtree(x)的节点])$,其中$subtree(x)$是搜索树中以$x$为根的节点。

其实这个值表示的就是这个点所在子树的最先被访问到的节点,作为这个子树的根。

搜索树:在无向连通图中任选一个节点出发进行深度搜索遍历,每个点只访问一次,所有发生递归的边$(x,y)$构成一棵树,称为无向连通图的搜索树

low计算方法

先令$low[x] = dfn[x]$, 考虑从$x$出发的每条边$(x,y)$

若在搜索树上$x$是$y$的父节点,令$low[x]=min(low[x], low[y])$

若无向边$(x,y)$不是搜索树上的边,则令$low[x] = min(low[x], dfn[y])$

割边判定法则

无向边$(x,y)$是桥,当且仅当搜索树上存在$x$的一个子节点$y$,满足:$dfn[x] < low[y]$

这说明从$subtree(y)$出发,在不经过$(x,y)$的前提下,不管走哪条边都无法到达$x$或比$x$更早访问的节点。若把$(x,y)$删除,$subtree(y)$就形成了一个封闭的环境。

桥一定是搜索树中的边,并且一个简单环中的边一定不是桥。

 void tarjan(int x, int in_edge)
{
dfn[x] = low[x] = ++num;
int flag = ;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
if(low[y] > dfn[x]){
bridge[i] = bridge[i ^ ] = true;
}
}
else if(i != (in_edge ^ ))
low[x] = min(low[x], dfn[y]);
}
} int main()
{
cin>>n>>m;
tot = ;
for(int i = ; i <= m; i++){
int x, y;
scanf("%d%d", &x, &y);
if(x == y)continue;
add(x, y);
add(y, x);
}
for(int i = ; i <= n; i++){
if(!dfn[i]){
tarjan(i, );
}
}
for(int i = ; i < tot; i += ){
if(bridge[i])
printf("%d %d\n", ver[i ^ ], ver[i]);
}
}

割点判定法则

若$x$不是搜索树的根节点,则$x$是割点当且仅当搜索树上存在$x$的一个子节点$y$,满足:$dfn[x]\leq low[y]$

特别地,若$x$是搜索树地根节点,则$x$是割点当且仅当搜索树上存在至少两个子节点$y_1,y_2$满足上述条件。

 #include<cstdio>
#include<cstdlib>
#include<map>
#include<set>
#include<cstring>
#include<algorithm>
#include<vector>
#include<cmath>
#include<stack>
#include<queue>
#include<iostream> #define inf 0x7fffffff
using namespace std;
typedef long long LL;
typedef pair<int, int> pr; const int SIZE = ;
int head[SIZE], ver[SIZE * ], Next[SIZE * ];
int dfn[SIZE], low[SIZE], n, m, tot, num;
bool bridge[SIZE * ]; void add(int x, int y)
{
ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
} void tarjan(int x)
{
dfn[x] = low[x] = ++num;
int flag = ;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
if(low[y] >= dfn[x]){
flag++;
if(x != root || flag > )cut[x] = true;
}
}
else low[x] = min(low[x], dfn[y]);
}
} int main()
{
cin>>n>>m;
tot = ;
for(int i = ; i <= m; i++){
int x, y;
scanf("%d%d", &x, &y);
if(x == y)continue;
add(x, y);
add(y, x);
}
for(int i = ; i <= n; i++){
if(!dfn[i]){
root = i;
tarjan(i);
}
}
for(int i = ; i <= n; i++){
if(cut[i])printf("%d", i);
}
puts("are cut-vertexes");
}

双连通分量

若一张无向连通图不存在割点,则称它为“点双连通图”。若一张无向连通图不存在桥,则称他为“边双连通图”。

无向图的极大点双连通子图被称为“点双连通分量”,简记为v-DCC。无向连通图的极大边双连通子图被称为“边双连通分量”,简记为e-DCC。

定理1一张无向连通图是点双连通图,当且仅当满足下列两个条件之一:

1.图的顶点数不超过2.

2.图中任意两点都同时包含在至少一个简单环中。

定理2一张无向连通图是边双连通图,当且仅当任意一条边都包含在至少一个简单环中。

边双连通分量求法

求出无向图中所有的桥,删除桥后,每个连通块就是一个边双连通分量。

用Tarjan标记所有的桥边,然后对整个无向图执行一次深度优先遍历(不访问桥边),划分出每个连通块。

 int c[SIZE], dcc;

 void dfs(int x){
c[x] = dcc;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
if(c[y] || bridge[i])continue;
dfs(y);
}
} //main()
for(int i = ; i <= n; i++){
if(!c[i]){
++dcc;
dfs(i);
}
}
printf("There are %d e-DCCs.\n", dcc);
for(int i = ; i <= n; i++){
printf("%d belongs to DCC %d.\n", i, c[i]);
}

e-DCC的缩点

把e-DCC收缩为一个节点,构成一个新的树,存储在另一个邻接表中。

 int hc[SIZE], vc[SIZE * ], nc[SIZE * ], tc;
void add_c(int x, int y){
vc[++tc] = y;
nc[tc] = hc[x];
hc[x] = tc;
} //main()
tc = ;
for(int i = ; i <= tot; i++){
int x = ver[i ^ ];
y = ver[i];
if(c[x] == c[y])continue;
add_c(c[x], c[y]);
}
printf("缩点之后的森林, 点数%d, 边数%d(可能有重边)\n", dcc, tc / );
for(int i = ; i < tc; i++){
printf("%d %d\n", vc[i ^ ], vc[i]);
}

点双连通分量的求法

桥不属于任何e-DCC,割点可能属于多个v-DCC

在Tarjan算法过程中维护一个栈,按照如下方法维护栈中的元素:

1.当一个节点第一次被访问时,该节点入栈。

2.当割点判定方法则中的条件$dfn[x]\leq low[y]$成立时,无论$x$是否为根,都要:

  (1)从栈顶不断弹出节点,直至节点$y$被弹出

  (2)刚才弹出的所有节点与节点$x$一起构成一个v-DCC

 void tarjan(int x){
dfn[x] = low[x] = ++num;
stack[++top] = x;
iff(x == root && head[x] == ){
dcc[++cnt].push_back(x);
return;
}
int flag = ;
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
if(!dfn[y]){
tarjan(y);
low[x] = min(low[x], low[y]);
if(low[y] >= dfn[x]){
flag++;
if(x != root || flag > )cut[x] = true;
cnt++;
int z;
do{
z = stack[top--];
dcc[cnt].push_back(z); }while(z != y);
dcc[cnt].push_back(x);
}
}
else low[x] = min(low[x], dfn[y]);
}
} //main()
for(int i = ; i <= cnt; i++){
printf("e-DCC #%d:", i);
for(int j = ; j < dcc[i].size(); j++){
printf(" %d", dcc[i][j]);
}
puts("");
}

v-DCC的缩点

设图中共有$p$个割点和$t$个v-DCC,新图将包含$p+t$个节点。

 //main
num = cnt;
for(int i = ; i <= n; i++){
if(cnt[i])new_id[i] = ++num;
}
tc = ;
for(int i = ; i <= cnt; i++){
for(int j = ; j < dcc[i].size(); j++){
int x = dcc[i][j];
if(cut[x]){
add_c(i, new_id[x]);
add_c(new_id[x], i);
}
else c[x] = i;
}
}
printf("缩点之后的森林, 点数%d, 边数%d\n", num, tc / );
printf("编号1~%d的为原图的v-DCC, 编号>%d的为原图割点\n", cnt, cnt);
for(int i = ; i < tc; i += ){
printf("%d %d\n", vc[i ^ ], vc[i]);
}

有向图的强连通分量

一张有向图,若对于图中任意两个节点$x,y$,既存在$x$到$y$的路径,也存在$y$到$x$的路径,则称该有向图是强连通图

有向图的极大强连通子图被称为强连通分量,简记为SCC。

一个环一定是强连通图,Tarjan算法的基本思路就是对每个点,尽量找到与它能构成环的所有节点。

Tarjan在深度优先遍历的同时维护了一个栈,当访问到节点$x$时,栈中需要保存一下两类节点:

1.搜索树上$x$的祖先节点,记为$anc(x)$

2.已经访问过,并且存在一条路径到达$anc(x)$的节点

实际上栈中的节点就是能与从$x$出发的“后向边”和“横叉边”形成环的节点。

追溯值:

定义为满足一下条件的节点的最小时间戳:

1.该点在栈中。

2.存在一条存subtree(x)出发的有向边,以该点为终点。

计算步骤:

1.当节点$x$第一次被访问时,把$x$入栈,初始化$low[x]=dfn[x]$

2.扫描从$x$出发的每条边$(x,y)$

  (1)若$y$没被访问过,则说明$(x,y)$是树枝边,递归访问$y$,从$y$回溯后,令$low[x] = min(low[x], low[y])$

  (2)若$y$被访问过且$y$在栈中,令$low[x] = min(low[x], dfn[y])$

3.从$x$回溯之前,判断是否有$low[x] = dfn[x]$。若成立,则不断从栈中弹出节点直至$x$出栈。

强连通分量判定法则

追溯值计算过程中,若从$x$回溯前,有$low[x] = dfn[x]$成立,则栈中从$x$到栈顶的所有节点构成一个强连通分量。

如果$low[x]=dfn[x]$,说明$subtree(x)$中的节点不能与栈中其他节点一起构成环。另外,因为横叉边的终点时间戳必定小于起点时间戳,所以$subtree(x)$中的节点也不可能直接到达尚未访问的节点(时间戳更大)

 const int N = , M = ;
int ver[M], Next[M], head[N], dfn[N], low[N];
int stack[N], ins[N], c[N];
vector<int>scc[N];
int n, m, tot, num, top, cnt; void add(int x, int y){
ver[++tot] = y, Next[tot] = head[x], head[x] = tot;
} void tarjan(int x){
dfn[x] = low[x] = ++num;
stack[++top] = x, ins[x] - ;
for(int i = head[x]; i; i = Next[i]){
if(!dfn[ver[i]]){
tarjan(ver[i]);
low[x] = min(low[x], low[ver[i]]);
}else if(ins[ver[i]]){
low[x] = min(low[x], dfn[ver[i]]);
}
}
if(dfn[x] == low[x]){
cnt++;
int y;
do{
y = stack[top--], ins[y] = ;
c[y] = cnt, scc[cnt].push_back(y);
}while(x != y);
}
} int main(){
cin>>n>>m;
for(int i = ; i <= m; i++){
int x, y;
scanf("%d%d", &x, &y);
add(x, y);
}
for(int i = ; i <= n; i++){
if(!dfn[i])tarjan(i);
}
}

缩点

 void add_c(int x, int y){
vc[++tc] = y, nc[tc] = hc[x], hc[x] = tc;
} //main
for(int x = ; x <= n; x++){
for(int i = head[x]; i; i = Next[i]){
int y = ver[i];
if(c[x] == c[y])continue;
add_c(c[x], c[y]);
}
}

李煜东的《图连通性若干扩展问题探讨》,有点难。

Tarjan算法【阅读笔记】的更多相关文章

  1. 萌新学习图的强连通(Tarjan算法)笔记

    --主要摘自北京大学暑期课<ACM/ICPC竞赛训练> 在有向图G中,如果任意两个不同顶点相互可达,则称该有向图是强连通的: 有向图G的极大强连通子图称为G的强连通分支: Tarjan算法 ...

  2. Tarjan算法 学习笔记

    前排提示:先学习拓扑排序,再学习Tarjan有奇效. -------------------------- Tarjan算法一般用于有向图里强连通分量的缩点. 强连通分量:有向图里能够互相到达的点的集 ...

  3. 学习笔记--Tarjan算法之割点与桥

    前言 图论中联通性相关问题往往会牵扯到无向图的割点与桥或是下一篇博客会讲的强连通分量,强有力的\(Tarjan\)算法能在\(O(n)\)的时间找到割点与桥 定义 若您是第一次了解\(Tarjan\) ...

  4. 算法学习笔记:Tarjan算法

    在上一篇文章当中我们分享了强连通分量分解的一个经典算法Kosaraju算法,它的核心原理是通过将图翻转,以及两次递归来实现.今天介绍的算法名叫Tarjan,同样是一个很奇怪的名字,奇怪就对了,这也是以 ...

  5. 算法笔记_144:有向图强连通分量的Tarjan算法(Java)

    目录 1 问题描述 2 解决方案 1 问题描述 引用自百度百科: 如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连 ...

  6. [学习笔记]连通分量与Tarjan算法

    目录 强连通分量 求割点 求桥 点双连通分量 模板题 Go around the Labyrinth 所以Tarjan到底怎么读 强连通分量 基本概念 强连通 如果两个顶点可以相互通达,则称两个顶点强 ...

  7. [学习笔记] Tarjan算法求桥和割点

    在之前的博客中我们已经介绍了如何用Tarjan算法求有向图中的强连通分量,而今天我们要谈的Tarjan求桥.割点,也是和上篇有博客有类似之处的. 关于桥和割点: 桥:在一个有向图中,如果删去一条边,而 ...

  8. [学习笔记] Tarjan算法求强连通分量

    今天,我们要探讨的就是--Tarjan算法. Tarjan算法的主要作用便是求一张无向图中的强连通分量,并且用它缩点,把原本一个杂乱无章的有向图转化为一张DAG(有向无环图),以便解决之后的问题. 首 ...

  9. Hadoop阅读笔记(一)——强大的MapReduce

    前言:来园子已经有8个月了,当初入园凭着满腔热血和一脑门子冲动,给自己起了个响亮的旗号“大数据 小世界”,顿时有了种世界都是我的,世界都在我手中的赶脚.可是......时光飞逝,岁月如梭~~~随手一翻 ...

随机推荐

  1. springboot集成elk 一: springboot + Elasticsearch

    1.ELK介绍 1> Elasticsearch是实时全文搜索和分析引擎, 提供搜集.分析.存储数据三大功能: 是一套开放REST和JAVA API等结构提供高效搜索功能,可扩展的分布式系统. ...

  2. (十二)Sun公司的Jstl标签库详细介绍

    JSP 本身提供的标签很少,不能满足我们日常开发需要,好在 Sun 公司自己提供了一套标签库: JSTL标签库快速入门(可点击) 目录 < c : out > 标签 < c : se ...

  3. 【Pandas数据分析案例】2018年北京积分入户情况分析

    据说,北京落户的难度比加入美国国籍还高.而北京2018年首次实行积分入户制,让我们来分析一下首批通过积分入户拿到北京户口的数据. 首先从北京积分落户官网下载公示名单: 根据表格中的信息,我们主要从以下 ...

  4. Redis慢日志取出来

    http://blog.chinaunix.net/uid-31396856-id-5758295.htmlhttps://blog.51cto.com/legehappy/2151986?sourc ...

  5. MVC——三层架构笔记、1

    三层架构MVC笔记1. DAL——数据访问层:(专门与数据库交互,增删查改的方法都在这:需引用MODEL层) BLL——业务逻辑层:(页面与数据库之间的桥梁:需引用DAL.MODEL层) MODEL— ...

  6. Kconfig语法简介

    背景: 上篇文章<添加自己的驱动程序到Linux内核树中>简单介绍了在Linux内核配置中添加自己的驱动选项.但是仅靠如此简单的配置有时候不能满足我们的要求. Target :hi3531 ...

  7. 使用RabbitMQ实现分布式事务

    RabbitMQ解决分布式事务思路: 案例: 经典案例,以目前流行点外卖的案例,用户下单后,调用订单服务,让后订单服务调用派单系统通知送外卖人员送单,这时候订单系统与派单系统采用MQ异步通讯. Rab ...

  8. Java Web 深入分析(6) Tomcat

    tomcat是什么:汤姆猫?Javaweb服务器? Tomcat是Apache 软件基金会(Apache Software Foundation)的Jakarta 项目中的一个核心项目,由Apache ...

  9. 初学java1 数据类型

    java数据类型 分为8种 整型 byte 8位 short 16位 int 32位 long 64位 字符型 char 必需为单引号'' 且只能有一个字符 浮点型 float double 布尔类型 ...

  10. springboot启动流程(一)构造SpringApplication实例对象

    所有文章 https://www.cnblogs.com/lay2017/p/11478237.html 启动入口 本文是springboot启动流程的第一篇,涉及的内容是SpringApplicat ...