Tarjan算法【阅读笔记】
应用:线性时间内求出无向图的割点与桥,双连通分量。有向图的强连通分量,必经点和必经边。
主要是求两个东西,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算法【阅读笔记】的更多相关文章
- 萌新学习图的强连通(Tarjan算法)笔记
--主要摘自北京大学暑期课<ACM/ICPC竞赛训练> 在有向图G中,如果任意两个不同顶点相互可达,则称该有向图是强连通的: 有向图G的极大强连通子图称为G的强连通分支: Tarjan算法 ...
- Tarjan算法 学习笔记
前排提示:先学习拓扑排序,再学习Tarjan有奇效. -------------------------- Tarjan算法一般用于有向图里强连通分量的缩点. 强连通分量:有向图里能够互相到达的点的集 ...
- 学习笔记--Tarjan算法之割点与桥
前言 图论中联通性相关问题往往会牵扯到无向图的割点与桥或是下一篇博客会讲的强连通分量,强有力的\(Tarjan\)算法能在\(O(n)\)的时间找到割点与桥 定义 若您是第一次了解\(Tarjan\) ...
- 算法学习笔记:Tarjan算法
在上一篇文章当中我们分享了强连通分量分解的一个经典算法Kosaraju算法,它的核心原理是通过将图翻转,以及两次递归来实现.今天介绍的算法名叫Tarjan,同样是一个很奇怪的名字,奇怪就对了,这也是以 ...
- 算法笔记_144:有向图强连通分量的Tarjan算法(Java)
目录 1 问题描述 2 解决方案 1 问题描述 引用自百度百科: 如果两个顶点可以相互通达,则称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连 ...
- [学习笔记]连通分量与Tarjan算法
目录 强连通分量 求割点 求桥 点双连通分量 模板题 Go around the Labyrinth 所以Tarjan到底怎么读 强连通分量 基本概念 强连通 如果两个顶点可以相互通达,则称两个顶点强 ...
- [学习笔记] Tarjan算法求桥和割点
在之前的博客中我们已经介绍了如何用Tarjan算法求有向图中的强连通分量,而今天我们要谈的Tarjan求桥.割点,也是和上篇有博客有类似之处的. 关于桥和割点: 桥:在一个有向图中,如果删去一条边,而 ...
- [学习笔记] Tarjan算法求强连通分量
今天,我们要探讨的就是--Tarjan算法. Tarjan算法的主要作用便是求一张无向图中的强连通分量,并且用它缩点,把原本一个杂乱无章的有向图转化为一张DAG(有向无环图),以便解决之后的问题. 首 ...
- Hadoop阅读笔记(一)——强大的MapReduce
前言:来园子已经有8个月了,当初入园凭着满腔热血和一脑门子冲动,给自己起了个响亮的旗号“大数据 小世界”,顿时有了种世界都是我的,世界都在我手中的赶脚.可是......时光飞逝,岁月如梭~~~随手一翻 ...
随机推荐
- webstorm对引入的css资源进行提示
- RocketMQ之三:RocketMQ集群环境搭建
1.初步理解Producer/Consumer Group 在安装RocketMQ之前我们先来理解Group概念,在RocketMQ中Group是很重要的.通过Group机制,让RocketMQ天然的 ...
- 【AtCoder】diverta 2019 Programming Contest 2
diverta 2019 Programming Contest 2 A - Ball Distribution 特判一下一个人的,否则是\(N - (K - 1) - 1\) #include &l ...
- Win10 收件箱添加QQ邮箱(2019年5月19日)
Emmm弄的时候没截图,就语言描述吧,非常简单. 登录到网页端QQ邮箱.点我登录 登录之后,界面上端的Logo右边有个"设置"(字有点小).点它 邮箱设置下面有一堆标签,点击&qu ...
- 位带操作—GPIO输出和输入
GPIOC->ODR |=(0<<2); // 总线操作,即操作整个寄存器. 在51单片机中 P0=0xFE; //总线操作. sbit LED1=P0^0; //位操作,即 ...
- linux 安装xdebug
一.安装了 xdebug php -m | grep 'xdebug' 如果没有安装就执行 首先根据 phpinfo() 信息 下载对应的版本,具体看参数: 下载地址:https://xdebug.o ...
- 位、字,字节与KB的关系?
位:我们常说的bit,位就是传说中提到的计算机中的最小数据单位:说白了就是0或者1:计算机内存中的存储都是01这两个东西. 字节:英文单词:(byte),byte是存储空间的基本计量单位.1byte ...
- Scratch-介绍“克隆”
上次我们模仿一个扔小球的运动, 用到了Scratch的“克隆”. 用Scratch模仿扔小球 “克隆”命令 Scratch“克隆”有三个命令积木. 区分“本体”和“克隆体” 使用“克隆”命令, 我们发 ...
- Eureka常见问题
一 Eureka注册慢问题默认情况下,服务注册到Eureka Server过程较慢.在开发或测试时,常常希望加速这一过程,从而提高工作效率.服务注册涉及到周期性心跳,默认30秒一次.只有当实例.服务端 ...
- (一)weblogic11g的安装配置
一.安装 找到weblogic安装包,小编这里用的是wls1034_win32.exe版本,双击打开 完成后运行快速启动,打开快速启动界面,配置weblogic.如果没有打开,还可以在开始菜单中找到q ...