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

主要是求两个东西,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. Snapshot Array

    Implement a SnapshotArray that supports the following interface: SnapshotArray(int length) initializ ...

  2. Redis 常用命令学习三:哈希类型命令

    1.赋值与取值命令 127.0.0.1:6379> hset stu name qiao (integer) 1 127.0.0.1:6379> hset stu sex man (int ...

  3. 利用Python进行数据分析_Pandas_处理缺失数据

    申明:本系列文章是自己在学习<利用Python进行数据分析>这本书的过程中,为了方便后期自己巩固知识而整理. 1 读取excel数据 import pandas as pd import ...

  4. 【数据结构】Tournament Chart

    Tournament Chart 题目描述 In 21XX, an annual programming contest, Japan Algorithmist GrandPrix (JAG) has ...

  5. Nginx学习笔记(四):基本数据结构

    目录 Nginx的一些特点 Nginx自定义整数类型 异常机制错误处理 内存池 字符串 时间与日期 运行日志   Nginx的一些特点 高性能 采用事件驱动模型,可以无阻塞的处理海量并发连接 高稳定性 ...

  6. Optional 理解

    目录 Optional 理解 1. 含义 2. Optional 类中方法 3. Optional 对象不应该作为方法参数 Optional 理解 1. 含义 Optional 是一个容器对象,该容器 ...

  7. Ocelot + Consul的demo(二)集群部署

    把服务A和服务B接口分别部署在两个ip地址上 修改 services.json文件, { "encrypt": "7TnJPB4lKtjEcCWWjN6jSA==&quo ...

  8. HALC:用于长读取错误纠正的高吞吐量算法

    背景: 第三代PacBio SMRT长读取可以有效地解决第二代测序技术的读长问题,但包含大约15%的测序错误.已经设计了几种纠错算法以有效地将错误率降低到1%,但是它们丢弃了大量未校正的碱基,因此导致 ...

  9. JS数组抽奖程序教学实例

    数组Javascript中非常重要的知识点,为了在课堂上提高学生兴趣,教学举例的选择就比较重要了. 为了提高学生兴趣,特设计一个可输入,可控制结束的,利用JS数组实现的抽奖教学实例.代码如下:

  10. 关于将多个json对象添加到数组中的测试

    如果用数组push添加不到数组中的,这个我也不知道是为什么?然后我选择了另一种发放就是从数组出发,逆向添加 最后的数组是这样的: data1=['公司1','公司2','公司3','公司4']; ar ...