更好的阅读体验&惊喜&原文链接

感谢@yxc的腿部挂件 大佬,指出本文不够严谨的地方,万分感谢!

Tarjan无向图的割点和桥(割边)

导言

在掌握这个算法前,咱们有几个先决条件.

  • [x] DFS搜索
  • [x] DFS序
  • [x] 一张纸
  • [x] 一支笔
  • [x] 认真的大脑(滑稽)

如果您都具备了,那么您就是巨佬了,您就可以轻松解决Tarjan算法了.


初学算法

概念掌握

割点

概念定义什么的,看上去好烦好烦好烦的,怎么办呢?

Acwing小剧场开播了,门票一枚AC币.

现在Acwing推出了一款战略游戏,名为Tarjan无向图的割点和桥.

贪玩Tarjan,介个是泥从未丸过的船新版本.

整个游戏,由\(N\)个岛屿构成,而且有\(M\)条航线联络着这些岛屿.

我们发现熊熊助教y总这两个点非常重要,是整张地图的核心战略要塞.

假如说缺少了任意一个点,我们发现总会有两个岛屿,不能联系了.

因此我们得出了,核心战略要塞,就是交通联络点.

所以这些点要重要中药保护,避免被破坏,

因此我们把这些要重要保护的点,也就是交通联络点,称之为割点.


概念 定义
割点 删掉这个点和这个点有关的边,图就不是连通图,分裂成为了多个不相连的子图
割边

同样的有核心战略要塞,也就会有黄金水道.

什么是黄金水道呢?

难道是航运量大的航道?

不不不不,这个概念不一样.

如果说黄金水道被破坏了,那么将会有两个和两个以上的岛屿,不能够通航连接了.

比如说,熊熊助教y总连接的一条航道.

还有熊熊助教song666连接的航道.

这就是我们的黄金水道,也就是战略航道.

因此我们给这些战略航道定义为桥(割边).

概念 定义
删除这条边后,图就不是连通图,分裂成为多个不相连的子图.

时间戳

其实啊,我们完全可以给这些岛屿们编号.这样便于管理,有利于愉悦身心健康.

因此我们得出了下面这张图片.

我们发现删除了一些多余的边,然后每个多了一个学号.

其实我们的学号,就是每一个节点的时间戳.

什么是时间戳,就是每一个点的学号.

但是这个学号是怎么来的呢?总不能是一顿瞎排的吧.

其实这个学号,就是我们的DFS序,刚开始我们任意选择一个点开始,然后一次深度优先搜索,每新搜索到一个点后,就给这个点标记一个学号.

然后来一个gif动画看一看.

因此时间戳的定义我们就知道了.

概念 定义
时间戳 \(dfn[x]\)表示节点x第一次被访问的编号.

这就是时间戳的概念,其实就是学号编辑的过程.


追溯值

追溯,追溯,就是寻找一个节点,以他为,可以抵达的最小学号.

我们设置一个小概念

\[subtree(x)表示搜索树中以x节点为根的子树节点集合.
\]

比如说我们举一个例子.

这些红色标记节点,其实也就是熊熊助教的搜索树.

因此我们得出.

\[subtree(熊熊助教)=(熊熊助教,song666,秦淮岸,Chicago)
\]

那么我们设置一下追溯值数组.

\[low[x]定义为以下节点的时间戳的最小值.
\]

  1. \(subtree(x)\)中的节点
  2. 通过\(1\)条不在搜索树上的边,能够抵达\(subtree(x)\)中的节点.

这个第一条我们上面解释过了,那么第二条怎么解释呢,还是一样,我们再来一个解释gif.

判定法则

割边判断法则

无向边\((x,y)\)是桥,当且仅当搜索树上存在\(x\)的一个子节点\(y\),满足

\[dfn[x]<low[y]
\]

首先一个公式,很难让人理解,我们得有一点人性化理解.

桥是干什么的?

它是用来连接两个个连通块的,没有了桥,就没有连通性,就会出现世外桃源.

什么是世外桃源,就是自成一派,与外人无往来.

我们需要知道追溯值,是什么.

就是一个节点可以在自己子树子树可以拓展的节点中找到一个最小的编号.

删掉了桥,那么在世外桃源,请问对于任何一个节点而言,他们存在,一个可以往外面拓展的节点吗?

没有,因为他们是世外桃源,不与外人有任何连接.

于是世外桃源内所有的节点,他们的最小追溯值一定不会小于宗主的编号.

咱们要知道,自给自足是很难成功的,总得有一个人出去买加碘海盐,那么这个人就是吃货宗的宗主

我们认为宗主就是所有节点中编号最小的节点,也就是有可能与外人有所连接的节点.

换句话说,也就是\((x,y)\)这个桥两端中,在世外桃源内的节点就是宗主\(y\).

正经语言说一说就是.

因此当\(dfn[x]<low[y]\)的时候

  1. 我们发现从\(y\)节点出发,在不经过\((x,y)\)的前提下,不管走哪一条边,我们都无法抵达\(x\)节点,或者比\(x\)节点更早出现的节点
  2. 此时我们发现\(y\)所在的子树似乎形成了一个封闭圈,那么\((x,y)\)自然也就是桥了.

割点判断法则

其实和上面的判断,只有一点修改.

若\(x\)不是搜索树的根节点,若\(x\)节点是割点,那么当且仅当搜索树上存在\(x\)的一个儿子节点\(y\),满足

\[dfn[x] \le low[y]
\]

宗主节点,是所有人中实力最强大的,所以肯定是最先学习的.

既然如此,那么显然他的\(dfn\),就是代表学习的开始时间,必然就是最小的.

而且割点是一个世外桃源和外界的唯一通道,所有的儿子节点的\(dfn\)都必须大于等于它,不可以小于它,因此证毕.

其实证明和上面的正经证明是一模一样的,只不过多了一个等于号罢了.

特殊定义:请记住根是不是割点,必须保证有至少两个儿子节点,否则不叫作割点.


代码解析

割边模板
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num;
bool bridge[N<<1];
void add_edge(int a,int b)
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x,int Edge)
{
dfn[x]=low[x]=++num;//DFS序标记
for(int i=head[x]; i; i=Next[i])//访问所有出边
{
int y=edge[i];//出边
if (!dfn[y])//不曾访问过,也就是没有标记,可以认为是儿子节点了
{
Tarjan(y,i);//访问儿子节点y,并且设置边为当前边
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定义中的,subtree(x)中的节点最小值为low[x]
if (low[y]>dfn[x]) //这就是桥的判定
bridge[i]=bridge[i^1]=true;//重边也是桥
} else if (i!=(Edge^1))
low[x]=min(low[x],dfn[y]);//第二类定义,也就是通过1条不在搜索树上的边,能够抵达subtree(x)的节点
}
}
int main()
{
scanf("%d%d",&n,&m);
tot=1;//边集从编号1开始
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1;i<=n;i++)
if (!dfn[i])//一个无向图,可能由多个搜索树构成
Tarjan(i,0);
for(int i=2;i<=tot;i+=2)//无向边不要管,直接跳2格
if (bridge[i])
printf("%d %d\n",edge[i^1],edge[i]);//桥的左右两端
return 0;
}
割点模板
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+20;
int head[N],edge[N<<1],Next[N<<1],tot;
int dfn[N],low[N],n,m,num,root,ans;
bool cut[N];
void add_edge(int a,int b)
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x)
{
dfn[x]=low[x]=++num;//DFS序标记
int flag=0;
for(int i=head[x]; i; i=Next[i])//访问所有出边
{
int y=edge[i];//出边
if (!dfn[y])//不曾访问过,也就是没有标记,可以认为是儿子节点了
{
Tarjan(y);//访问儿子节点y,并且设置边为当前边
low[x]=min(low[x],low[y]);//看看能不能更新,也就是定义中的,subtree(x)中的节点最小值为low[x]
if (low[y]>=dfn[x]) //这就是割点的判定
{
flag++;//割点数量++
if (x!=root || flag>1)//不能是根节点,或者说是根节点,但是有至少两个子树节点是割点
cut[x]=true;
}
}
else low[x]=min(low[x],dfn[y]);//第二类定义,也就是通过1跳不在搜索树上的边,能够抵达subtree(x)的节点
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(cut,false,sizeof(cut));
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
add_edge(a,b);
add_edge(b,a);
}
for(int i=1; i<=n; i++)
if (!dfn[i])//一个无向图,可能由多个搜索树构成
root=i,Tarjan(i);
for(int i=1; i<=n; i++) //统计割点个数
if (cut[i])
ans++;
printf("%d\n",ans);
for(int i=1; i<=n; i++) //顺序遍历,康康哪些点是割点
if (cut[i])
printf("%d ",i);
return 0;
}

经典题目

第一题 B城

原题连接

题目描述

B城有 \(n\) 个城镇,\(m\) 条双向道路。

每条道路连结两个不同的城镇,没有重复的道路,所有城镇连通。

把城镇看作节点,把道路看作边,容易发现,整个城市构成了一个无向图。

输入格式

第一行包含两个整数 \(n\) 和 \(m\)。

接下来\(m\)行,每行包含两个整数 \(a\) 和 \(b\),表示城镇 \(a\) 和 \(b\) 之间存在一条道路。

输出格式

输出共\(n\)行,每行输出一个整数。

第 \(i\) 行输出的整数表示把与节点 \(i\) 关联的所有边去掉以后(不去掉节点 \(i\) 本身),无向图有多少个有序点\((x,y)\),满足$ x$ 和$ y$ 不连通。

数据范围

\[n \le 100000 \\\\
m \le 500000
\]

输入样例:

5 5
1 2
2 3
1 3
3 4
4 5

输出样例:

8
8
16
14
8

解题报告

题意理解

一张图,每次删除一个节点,包括它和其他节点的边,问删除这个节点过后,会有多少个有序节点\((x,y)\)之间无法连通.

Hint:有序节点\((x,y)\)和有序节点\((y,x)\)不是同样的节点对.我们要计算两次.


算法解析

删除一个节点,使得图不连通,这不就是割点的定义吗?

  1. 假如删掉的节点不是割点.

此时我们发现,除了这个节点与其他节点都不连通,其他节点都是连通的.

因此答案为.

\[ans=2 \times (n-1)
\]

也就是除了当前节点,其余的\((n-1)\)个节点都与当前节点不连通.(自己和自己当然是连通的)

然后答案要计算两遍,因此\((n-1) \times 2\)

  1. 假如说删除的节点是割点

删除割点,会使得图变得四分五裂,成为了若干个连通块.

那么连通块本身内部的节点,当然还是互相连通的.

但是两个不同的连通块的节点,显然就不连通了.

比如说\(a\)节点属于\(1\)号连通块.

然后\(b\)节点属于\(2\)号连通块.

请问他们连通吗?

不连通!

所以答案+1.

那么我们从特解到通解.

假如说此时有\(1,2,3,4,5\)这五个连通块.

我们提出一个概念.

\[size[i]表示i连通块的节点数
\]

而且\(s\)节点属于\(1\)号连通块.

那么除了自己所在\(1\)号连通块内部节点与自己连通,其他连通块节点和自己都没有任何关系.

因此我们得出如下公式.

显然与\(s\)节点相连的节点个数有

\[size[x]-1
\]

那么与\(s\)节点不连通的节点个数有

\[n-(size[x]-1)-1=n-size[x] \\\\
总节点-与s节点相通的节点总数-s自身节点=与s不连通的节点数
\]

那么\(s\)节点的贡献是什么呢?

\[s号节点的贡献=(n-size[1])
\]

因此我们推出一个连通块贡献的价值.

\[size[s_1] \times (n-size[s_1])
\]

但是一个割点显然不会只有1个连通块,我们假设它有

\[s_1,s_2,s_3,s_4,......,s_k连通块.
\]

因此一个割点的子连通块贡献了.

\[size[s_1] \times (n-size[s_1])+size[s_2] \times(n-size[s_2])+...+size[s_k] \times (n-size[s_k])
\]

所有儿子连通块+自身,一共有多少个节点呢,我们算一下.

\[1+\sum_{i=1}^ksize[s_i]
\]

但是根据搜索树定义,割点有自己的儿子连通块,当然也就有不是儿子连通块.

因此我们得出不是儿子连通块的个数为.

\[(n-(1+\sum_{i=1}^ksize[s_i]))
\]

其实也就是.

\[n-(1+(size[s_1]+size[s_2]+...+size[s_k])) \\\\
-1,是因为还要抛去自身这个节点 \\\\
Hint:一棵树等于子树节点+根节点.
\]

那么这些不是儿子连通块,显然与是儿子联通块是不连通了,因为删除这个割点,所以贡献代价为

\[(n-(1+\sum_{i=1}^ksize[s_i])) \times (1+\sum_{i=1}^ksize[s_i])
\]

此时我们还要知道,节点自身也是有贡献的,毕竟和其他节点都不连通.

\[(n-1) \times 1
\]

那么总和上面贡献,就是最终答案了.我就不打了,上面很清晰了,下面也有代码解释.


代码解析

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=100000*2+200;
long long head[N],edge[N],Next[N],tot,num,ans[N],root;
long long n,m,size[N],dfn[N],low[N];
bool cut[N];
void add_edge(int a,int b)//加边
{
edge[++tot]=b;
Next[tot]=head[a];
head[a]=tot;
}
void Tarjan(int x)
{
dfn[x]=low[x]=++num;//编号
size[x]=1;//初始时候就自己这一个孤寡老人
int flag=0,sum=0;
for (int i=head[x]; i; i=Next[i])
{
int y=edge[i];//儿子节点
if (!dfn[y])//不曾访问
{
Tarjan(y);//访问
size[x]+=size[y];//儿子节点贡献的子树大小
low[x]=min(low[x],low[y]);//更新一下
if (low[y]>=dfn[x])//发现了割点
{
flag++;
ans[x]+=size[y]*(n-size[y]);//加上y儿子连通块带来的贡献
sum+=size[y];//这里是统计儿子连通块总节点数
if (x!=root || flag>1)//是割点
cut[x]=true;
}
}
else
low[x]=min(low[x],dfn[y]);//更新
}
if (cut[x])//是割点
ans[x]+=(n-sum-1)*(sum+1)+(n-1);//非儿子连通块的贡献+自身节点贡献
else
ans[x]=2*(n-1);//不是割点
}
signed main()
{
scanf("%lld%lld",&n,&m);
memset(cut,false,sizeof(cut));//刚开始都不是割点
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%lld%lld",&a,&b);
if (a==b)//无用边
continue;
add_edge(a,b);
add_edge(b,a);
}
root=1;//根节点为1
Tarjan(1);
for(int i=1; i<=n; i++)
printf("%lld\n",ans[i]);
return 0;
}

Tarjan无向图的割点和桥(割边)全网详解&算法笔记&通俗易懂的更多相关文章

  1. 求 无向图的割点和桥,Tarjan模板

    /* 求 无向图的割点和桥 可以找出割点和桥,求删掉每个点后增加的连通块. 需要注意重边的处理,可以先用矩阵存,再转邻接表,或者进行判重 */ const int MAXN = 10010; cons ...

  2. Cloud Alibabab笔记问世,全网详解仅此一份手慢无

    转: Cloud Alibabab笔记问世,全网详解仅此一份手慢无 什么是Spring cloud alibaba Spring Cloud Alibaba 是阿里巴巴提供的微服务开发一站式解决方案, ...

  3. 求无向图的割点和桥模板(tarjan)

    一.基本概念 1.桥:若无向连通图的边割集中只有一条边,则称这条边为割边或者桥 (离散书上给出的定义.. 通俗的来说就是无向连通图中的某条边,删除后得到的新图联通分支至少为2(即不连通: 2.割点:若 ...

  4. tarjan算法--求无向图的割点和桥

    一.基本概念 1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥无向连通图中,如果删除某边后,图变成不连通,则称该边为桥. 2.割点:无向连通图中 ...

  5. tarjan算法--求解无向图的割点和桥

    1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥 也就是说 无向连通图中,如果删除某边后,图变成不连通,则称该边为桥 2.割点:无向连通图中,如 ...

  6. 无向图的割点和桥 tarjan 模板

    #include <bits/stdc++.h> using namespace std; const int MAXN = 20005; const int MAXM = 100005; ...

  7. Android调试桥 adb安装详解

    Android调试桥(adb) 一.简介 Android 调试桥 (adb) 是一种功能多样的命令行工具,可让您与设备进行通信.adb 命令便于执行各种设备操作(例如安装和调试应用),并提供对 Uni ...

  8. Tarjan求无向图割点、桥详解

    tarjan算法--求无向图的割点和桥   一.基本概念 1.桥:是存在于无向图中的这样的一条边,如果去掉这一条边,那么整张无向图会分为两部分,这样的一条边称为桥无向连通图中,如果删除某边后,图变成不 ...

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

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

随机推荐

  1. vue-cli的项目中关于axios的全局配置,结合element UI,配置全局loading,header中做token传输

    在src目录中建立plugins文件夹,在文件夹内建立axios.js文件 "use strict"; import Vue from 'vue'; import axios fr ...

  2. 安装heat

    在控制节点上执行 controller-heat(){mysql -uroot -p${MYSQL_PASSWD} << EOF DROP DATABASE IF EXISTS heat; ...

  3. 《Java语言程序设计》第三讲类与对象“动手动脑”

    一.以下代码为何无法通过编译?哪儿出错了? 答: 如果类提供了一个自定义的构造方法,将导致系统不再提供默认构造方法. 二. (1)以下代码输出结果是什么? package xy; public cla ...

  4. PHP学习(9)——错误和异常处理

    1.Exception类 这个类是PHP为异常处理提供的内置类.构造函数的两个参数分别是错误消息和错误代码. 除了构造函数之外,该类还提供了如下的内置方法: · getCode() 返回传递给构造函数 ...

  5. @Transactional事务总结

    一:加了注解@Transactional就能起作用的原理总结: 1:首先是由类:JdkDynamicAopProxy,在invoke方法里面创建动态代理类,同时由拦截类进行拦截,代码如下所示: Lis ...

  6. Excel小技巧(生成数字篇)

    1. 自动生成1-1000: =ROW() 2.随机生成 1-100 : =RANDBETWEEN(1,100) // 若要包含小数点n位,就把(MIN,MAX)改成 (MIN*10^n,MAX*10 ...

  7. ubuntu 16.04主题美化

    目录 numix图标 Flatabulous主题 参考: Unity-tweak-tool插件 numix图标 sudo apt-add-repository ppa:numix/ppa sudo a ...

  8. Shell脚本中计算字符串长度的5种方法

    有时在Linux操作系统中需要计算某个字符串的长度,通过查询资料整理了下目前Shell中获取字符串的长度的多种方法,在这里分享给大家,方法如下: 方法1: 使用wc -L命令wc -L可以获取到当前行 ...

  9. Oracle-DQL 2- 限定和排序

    1.where子句--查询30号部门员工的姓名,职位和工资SELECT ename,job,sal,deptno FROM empWHERE deptno = 30; --查询职位是manager的员 ...

  10. extern int PASCAL

    表示声明一个变量,这个变量在其他地方已经定义,但是这里因为要使用,所以声明下. 写成下面: extern “C” int PASCAL: 说明PASCAL是在一个C文件下定义的.如果不是在C下就不用加 ...