图论 II

本篇博客主要讲解:无向图的双联通分量,有向图的强联通分量,欧拉回路。

定义与记号

涉及常见或可能用到的概念的定义。关于更多,见参考资料。

基本定义

  • 图:一张图 \(G\) 由若干个点和连接这些点的边构成。点的集合称为 点集 \(V\),边的集合称为 边集 \(E\),记 \(G = (V, E)\)
  • 阶:图 \(G\) 的点数 \(|V|\) 称为 ,记作 \(|G|\)
  • 无向图:若 \(e\in E\) 没有方向,则 \(G\) 称为 无向图。无向图的边记作 \(e = (u, v)\)\(u, v\) 之间无序。
  • 有向图:若 \(e\in E\) 有方向,则 \(G\) 称为 有向图。有向图的边记作 \(e = u\to v\)\(e = (u, v)\)\(u, v\) 之间有序。无向边 \((u, v)\) 可视为两条有向边 \(u\to v\)\(v\to u\)
  • 重边:端点和方向(有向图)相同的边称为 重边
  • 自环:连接相同点的边称为 自环

相邻

  • 相邻:在无向图中,称 \(u, v\) 相邻 当且仅当存在 \(e = (u, v)\)
  • 邻域:在无向图中,点 \(u\)邻域 为所有与之相邻的点的集合,记作 \(N(u)\)
  • 邻边:在无向图中,与 \(u\) 相连的边 \((u, v)\) 称为 \(u\)邻边
  • 出边 / 入边:在有向图中,从 \(u\) 出发的边 \(u\to v\) 称为 \(u\)出边,到达 \(u\) 的边 \(v\to u\) 称为 \(u\)入边
  • 度数:一个点的 度数 为与之关联的边的数量,记作 \(d(u)\)\(d(u) = \sum_{e\in E} ([u = e_u] + [u = e_v])\)。点的自环对其度数产生 \(2\) 的贡献。
  • 出度 / 入度:在有向图中,从 \(u\) 出发的边数称为 \(u\)出度,记作 \(d ^ +(u)\);到达 \(u\) 的边数称为 \(u\)入度,记作 \(d ^ -(u)\)

路径

  • 途径:连接一串相邻结点的序列称为 途径,用点序列 \(v_{0..k}\) 和边序列 \(e_{1..k}\) 描述,其中 \(e_i = (v_{i - 1}, v_i)\)。常写为 \(v_0\to v_1\to \cdots \to v_k\)
  • 迹:不经过重复边的途径称为
  • 回路:\(v_0 = v_k\) 的迹称为 回路
  • 路径:不经过重复点的迹称为 路径,也称 简单路径。不经过重复点比不经过重复边强,所以不经过重复点的途径也是路径。注意题目中的简单路径可能指迹。
  • 环:除 \(v_0 = v_k\) 外所有点互不相同的途径称为 ,也称 简单环

连通性

  • 连通:对于无向图的两点 \(u, v\),若存在途径使得 \(v_0 = u\)\(v_k = v\),则称 \(u, v\) 连通
  • 弱连通:对于有向图的两点 \(u, v\),若将有向边改为无向边后 \(u, v\) 连通,则称 \(u, v\) 弱连通
  • 连通图:任意两点连通的无向图称为 连通图
  • 弱连通图:任意两点弱连通的有向图称为 弱连通图
  • 可达:对于有向图的两点 \(u, v\),若存在途径使得 \(v_0 = u\)\(v_k = v\),则称 \(u\) 可达 \(v\),记作 \(u \rightsquigarrow v\)
  • 关于点双连通 / 边双连通 / 强连通,见对应章节。

特殊图

  • 简单图:不含重边和自环的图称为 简单图
  • 基图:将有向图的有向边替换为无向边得到的图称为该有向图的 基图
  • 有向无环图:不含环的有向图称为 有向无环图,简称 DAG(Directed Acyclic Graph)。
  • 完全图:任意不同的两点之间恰有一条边的无向简单图称为 完全图\(n\) 阶完全图记作 \(K_n\)
  • 树:不含环的无向连通图称为 ,树上度为 \(1\) 的点称为 叶子。树是简单图,满足 \(|V| = |E| + 1\)。若干棵(包括一棵)树组成的连通块称为 森林。相关知识点见 “树论”。
  • 稀疏图 / 稠密图: \(|E|\) 远小于 \(|V| ^ 2\) 的图称为 稀疏图\(|E|\) 接近 \(|V| ^ 2\) 的图称为 稠密图。用于讨论时间复杂度为 \(\mathcal{O}(|E|)\)\(\mathcal{O}(|V| ^ 2)\) 的算法。

子图

  • 子图:满足 \(V'\subseteq V\)\(E'\subseteq E\) 的图 \(G' = (V', E')\) 称为 \(G = (V, E)\)子图,记作 \(G'\subseteq G\)。要求 \(E'\) 所有边的两端均在 \(V'\) 中。
  • 导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的 导出子图。导出子图的形态仅由选择的点集 \(V'\) 决定,记作 \(G[V']\)
  • 生成子图:\(|V'| = |V|\) 的子图称为 生成子图
  • 极大子图(分量):在子图满足某性质的前提下,子图 \(G'\) 称为 极大 的,当且仅当不存在同样满足该性质的子图 \(G''\)\(G'\subsetneq G''\subseteq G\)\(G'\) 称为满足该性质的 分量。例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块。

1. 无向图连通性:双连通分量

无向图连通性中的双连通分量和有向图可达性中的强连通分量是图论的重要部分,分两章节介绍。

本章研究双连通分量相关知识点,研究对象为无向连通图。默认图为 无向连通图

1.1 相关定义

无向图连通性,主要研究割点和割边。

  • 割点:在无向图中,删去后使得连通分量数增加的点称为 割点
  • 割边:在无向图中,删去后使得连通分量数增加的边称为 割边,也称

孤立点和孤立边的两个端点都不是割点,但孤立边是割边。非连通图的割边为其每个连通分量的割边的并。

为什么割点和割边这么重要?对于无向连通图上的非割点,删去它,图仍然连通,但删去割点后图就不连通了。因此割点相较于非割点对连通性有更大的影响。割边同理。

  • 点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
  • 边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
  • 点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双
  • 边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双

将某种类型的连通分量根据等价性或独立性缩成一个点的操作称为 缩点,原来连接两个不同连通分量的边在缩点后的图上连接对应连通分量缩点后形成的两个点。根据连通分量的类型不同,缩点可分为无向图上的点双连通分量缩点(圆方树,见 “图论 II”),边双连通分量缩点(本章),以及有向图上的强连通分量缩点(下一章)。

边双和点双缩点后均得到一棵树,而强连通分量缩点后得到一张有向无环图。

  • 点双连通:若 \(u, v\) 处于同一个点双连通分量,则称 \(u, v\) 点双连通。一个点和它自身点双连通。由一条边直接相连的两点也是点双连通的。
  • 边双连通:若 \(u, v\) 处于同一个边双连通分量,则称 \(u, v\) 边双连通。一个点和它自身边双连通,但由一条边直接相连的两点不一定边双连通。

点双连通和边双连通是无向图连通性相关最基本的两条性质。

:点双连通和边双连通有若干等价定义,本文选取的定义并非最常见的定义。其它定义将会作为连通性性质在下文介绍。

1.2 双连通的基本性质

研究双连通的性质时,最重要的是把定义中的基本元素 —— 割点和割边的性质理清楚。然后从整体入手,考察对应分量在原图上的分布形态,再深入单个分量,考察分量内两点之间的性质。以求对该连通性以及连通分量有直观的印象,思考时有清晰的图像作为辅助,做题思路更流畅。

边双连通比点双连通简单,所以先介绍边双连通。

1.2.1 边双连通

考虑割边两侧的两个点 \(u, v\)(不是割边的两端)。因为删去割边后 \(u, v\) 不连通,所以考虑任何一条 \(u\)\(v\) 的迹,这条割边一定在迹上 —— 否则删去后 \(u, v\) 仍连通。将这样的边称为必经边:从 \(u\)\(v\) 必须要经过的边。两点之间的所有必经边,就是连接它们所有迹的边集的交。在研究必经边时,重复经过一条边是不优的,所以只需考虑两点之间的所有迹。

结论

两点之间任意一条迹上的所有割边,就是两点之间的所有必经边。

可以看出割边就是必经边的代名词。必经边需要两个点才有定义,而割边直接由原图定义,且边双缩点后可根据树边(割边)直接求出两点之间的必经边。

断开一条割边,整张图会裂成两个连通块。断开所有割边,整张图会裂成割边条数 \(+1\) 个连通块。每个连通块内部不含割边且不能再扩大(再扩大就包含割边了),是原图的边双连通分量。这说明边双连通分量由割边连接,且形成一棵树的形态。将边双缩成一个点,得到边双缩点树,每条边对应原图的一条割边,每个点对应原图的一个边双连通分量,如下图。

添加任意一条边 \(u, v\)。考虑边双形成的树,\(u, v\) 所在边双的树上简单路径上的所有边双会被缩成一个大边双:由于添加了 \(u, v\),连接这些边双的割边变成了非割边。如下图,加入橙色边后,原来的三个边双形成了橙色轮廓的大边双。

不同边双没有公共点,即每个点恰属于一个边双,所以:

边双连通的传递性

\(a\)\(b\) 边双连通,\(b\)\(c\) 边双连通,则 \(a\)\(c\) 边双连通。

边双内部不含割边,所以:

结论

\(u, v\) 边双连通当且仅当 \(u, v\) 之间没有必经边。

考虑边双中的一条边 \((u, v)\)。将其删去后,其两端仍连通,将连接它们的路径和 \(u, v\) 拼起来,得到不经过重复边的回路。

结论

对于边双内任意一条边 \((u, v)\),存在经过 \((u, v)\) 的回路。

结论

对于边双内任意一点 \(u\),存在经过 \(u\) 的回路。

这个结论可以继续加强,见 1.2.3 小节 Menger 定理。

1.2.2 点双连通

删去割点后不连通的两个点之间任意一条路径必然经过该割点,称这样的点为必经点:从 \(u\)\(v\) 必须要经过的点。在研究必经点时,重复经过一个点是不优的,所以只需考虑两点之间的所有路径。

错误结论

两点之间任意一条路径上的所有割点,就是两点之间的所有必经点。

错误原因是对于割边,经过割边一定会从删去割边的一个连通块走到另一个连通块,但经过割点不一定从删去割点的一个连通块走到另一个连通块。如图 \(G = \{(1, 2), (1, 3), (2, 3), (3, 4)\}\)\(3\) 是割点,也出现在 \(1\rightsquigarrow 2\) 的路径上,但不是 \(1\rightsquigarrow 2\) 的必经点。

关于两点之间割点的刻画,见 “图论 II” 广义圆方树。

与边双不同的是,两个点双之间可能有交:考虑 “\(8\)” 型结构,中间的交点在两个点双都出现了。因此 点双连通不具有传递性。但若两个点双有交,交点一定唯一,否则两点双可以合并为更大的点双。证明点双相关性质时,点双的极大性通常是导出矛盾的关键。

进一步地,若两点双有交,交点一定阻碍了它们继续扩大:如果删去该点之后两点双仍连通,同样地,两点双可以合并为更大的点双。

结论:若两点双有交,那么交点一定是割点。

区分:称两点属于同一个边双,即两点边双连通,就是判断它们所在边双是否相同。称两点属于同一个点双,即两点点双连通,是检查是否存在点双同时包含这两个点。因为两点双至多有一个交点,所以若两点点双连通,那么包含这两点的点双唯一。

说明:点双包含原图割点,但在只关心该点双时,原图割点变成了非割点。删去割点后,原图分裂为若干连通分量,但该点双仍连通。

现在我们知道点双交点是割点,那么割点一定是点双交点吗?删去一个割点,将整张图分成若干连通块。存在割点的两个邻居 \(u, v\) 不连通。\(u, v\) 不会处于同一个点双。假设存在这样的点双,则删去割点后 \(u, v\) 不连通,矛盾。又因为 “一边连两点” 是点双连通图,所以割点和 \(u, v\) 点双连通。这说明割点属于超过一个点双。
边双连通

结论

一个点是割点当且仅当它属于超过一个点双。

结论

由一条边直接相连的两点点双连通。

结合上述性质,得:

推论

一条边恰属于一个点双。

可知割点是连接点双的桥梁,正如割边是连接边双的桥梁。用一个点代表一个点双,并将点双代表点向它包含的割点连边,得到 块割树(Block-Cut Tree,“点双连通块 - 割点” 树,和圆方树有细微差别)。

考虑点双中的一个点 \(x\)。将其删去后,剩下所有点连通。因此,若 \(d(x) > 1\),考虑其任意两个邻居 \(u\neq v\),将新图 \(u, v\) 之间的路径和 \(x\) 连起来,得到经过 \(x\) 的简单环。若 \(d(x) = 1\),则整个点双为 “一边连两点” 的平凡形态。

结论

对于 \(n\geq 3\) 的点双内任意一点 \(u\),存在经过 \(u\) 的简单环。

关于更多点双连通的性质和点双缩点,见 1.2.3 小节 Menger 定理和 “图论 II” 广义圆方树。

1.2.3 Menger 定理及其推论

前置知识:最大流最小割定理。

Menger 定理是研究图连通性时相当重要的定理。从 Menger 定理出发,能得到大量双连通问题的关键结论。

让我们先回顾一下最大流最小割定理:在一张网络上,\(s\to t\) 的最大流等于 \(s, t\) 之间的最小割。现在将该定理应用在一般无向图上:

  • \(s\to t\) 的最大流等于 \(f\) 表示存在 \(f\)\(s\rightsquigarrow t\) 的边不相交的迹,且 \(f\) 不能更大。
  • \(s, t\) 之间的最小割等于 \(c\) 表示存在 \(c\) 条边使得断开这些边后 \(s, t\) 不连通,且 \(c\) 不能更小。

聪明的读者已经想到,如果将定理应用在边双连通图上,对于任意 \(u\neq v\),都需要断开至少两条边才能使它们不连通,所以它们之间至少存在两条边不相交的迹(可以在结点处相交),继而得到边双最重要的性质之一。

结论

对于边双内任意两点 \(u, v\),存在经过 \(u, v\) 的回路。

对于点双,目标是割掉(删去)若干个点使得 \(s, t\) 不连通。应用网络流点边转化的技巧,建新图 \(G'\),将除了 \(s, t\) 的每个点 \(u\) 拆成两个点 \(u_{in}\)\(u_{out}\)\(u\) 的所有邻居 \(v\in N(u)\)\(u_{in}\) 连边容量为 \(+\infty\)\(u_{out}\) 向所有 \(v\in N(u)\) 连边容量为 \(+\infty\),再从 \(u_{in}\)\(u_{out}\) 连边容量 \(1\)。连容量为 \(+\infty\) 的边是为了防止割掉这些边,因为它们对应原图的边,而希望割掉的是点。

  • \(G'\)\(s\to t\) 的最大流等于 \(f\) 表示存在 \(f\)\(s\rightsquigarrow t\) 的点不相交(不在除了 \(s, t\) 以外的点相交)的路径,且 \(f\) 不能更大。
  • \(G'\)\(s, t\) 之间的最小割等于 \(c\) 表示存在 \(c\) 个点使得删去这些点后 \(s, t\) 不连通,且 \(c\) 不能更小。

特别地,当 \(s, t\) 相邻时,\(f = +\infty\),表示无论删去多少个点都无法使得 \(s, t\) 不连通。

结论

对于点双内任意两点 \(u, v\),存在经过 \(u, v\) 的简单环。

结论

对于 \(n\geq 3\) 的点双内任意两点,存在经过 \(u, v\) 的长度不小于 \(3\) 的简单环。

证明

\(u, v\) 不直接相邻,使用上述结论。若 \(u, v\) 直接相邻,因为删去 \((u, v)\) 后整张图仍连通(否则 \(u, v\) 至少有一个是割点),所以将 \((u, v)\) 接在图上 \(u, v\) 之间的路径即可。\(\square\)

通过上述铺垫,可以很自然地推出 Menger 定理:

边形式

对于无向图 \(G\) 上任意不同的两点 \(u, v\),使得 \(u, v\) 不连通所需删去的边的数量的最小值,等于 \(u, v\) 之间边不相交的迹的数量的最大值。

点形式

对于无向图 \(G\) 上任意不同且不相邻的两点 \(u, v\),使得 \(u, v\) 不连通所需删去的点的数量的最小值,等于 \(u, v\) 之间点不相交(不在除了 \(u, v\) 以外的点相交)的路径数量的最大值。

根据 Menger 定理,对于无向图 \(G\) 上任意两点 \(u\neq v\),可以定量描述它们关于边的连通性和关于点的连通性。

  • 局部边连通度:使得 \(u, v\) 不连通所需删去的边的数量的最小值为 \(u, v\)局部边连通度,记作 \(\lambda(u, v)\)。它等于 \(u, v\) 之间边不相交的迹数量的最大值。
  • 局部点连通度:对于 \((u, v)\notin E\),使得 \(u, v\) 不连通所需删去的点的数量的最小值为 \(u, v\)局部点连通度,记作 \(\kappa(u, v)\)。它等于 \(u, v\) 之间点不相交的路径数量的最大值。
  • \(k\)-边连通:若 \(\lambda(u, v)\geq k\),则称 \(u, v\) 之间是 k-边连通 的。
  • \(k\)-点连通:若 \(\kappa(u, v)\geq k\),则称 \(u, v\) 之间是 k-点连通 的。

在此基础上,可以定量描述整张图的连通性。

  • 全局边连通度:使得存在两个点不连通所需删去的边的数量的最小值为 \(G\)全局边连通度,记作 \(\lambda(G)\)。它等于任意两点局部边连通度的最小值,即 \(\lambda(G) = \min_{u \neq v} \lambda(u, v)\)。特殊定义 \(\lambda(K_1) = +\infty\)

  • 全局点连通度:使得存在两个点不连通所需删去的点的数量的最小值为 \(G\)全局点连通度,记作 \(\kappa(G)\)。它等于任意不相邻两点局部点连通度的最小值,即 \(\kappa(G) = \min_{u\neq v\land (u, v)\notin E} \kappa(u, v)\)。因 \(K_n\) 不存在不相邻的两点,特殊定义 \(\kappa(K_n) = n\)

  • \(k\)-边连通图:对于 \(k\in \mathbb N\),若 \(\lambda(G)\geq k\),则称 \(G\)k-边连通图,其性质称为 k-边连通性。它可以等价表述为:删去任意不超过 \(k - 1\) 条边,整张图仍然连通。

  • \(k\)-点连通图:对于 \(k\in \mathbb N\),若 \(\kappa(G)\geq k\),则称 \(G\)k-点连通图,也称 k-连通图,其性质称为 k-点连通性。它可以等价表述为:\(|G|\geq k\) 且删去任意不超过 \(k - 1\) 个点,整张图仍然连通。

将 Menger 定理推广至任意点对,得到以下结论:

边形式推广

一张图是 k-边连通图当且仅当每对点之间有 \(k\) 条边不相交的迹。

点形式推广

一张图是 k-连通图当且仅当 \(|G|\geq k\) 且每对点之间有 \(k\) 条点不相交的路径。

1.2.4 双连通总结

现在我们知道为什么不存在割边(点)的图叫作边(点)双连通图了:不存在割边说明 \(\lambda(G) \geq 2\),即不存在割边的图为 2-边连通图。不存在割点说明 \(\kappa(G) \geq 2\),即不存在割点的图为 2-点连通图。

根据 Menger 定理,一张图不存在割边等价于对任意两点 \(u, v\),存在经过 \(u, v\) 的回路。从一条性质出发可以推出另一条性质,称前者为边双连通图的定义,后者为边双连通图的基本性质。根据边双连通的传递性,每条边属于至少一个回路的图也是边双连通图。类似地,一张图不存在割点等价于对任意两点 \(u, v\),存在经过 \(u, v\) 的环。称前者为点双连通图的定义,后者为点双连通图的基本性质。

目前为止我们都是在单独研究边双和点双,接下来探究它们的联系。下文讨论的点双忽略了 “一边连两点” 等平凡情况,默认 \(n\geq 3\)

如果一张点数大于 \(2\) 的图没有割点,那么它一定没有割边。假设存在割边,那么删去割边后大小大于 \(1\) 的连通块对应的割边端点一定是割点。因此,任何边双满足的性质,点数大于 \(2\) 的点双一定满足(这里的逻辑有点绕,可以这么想:\(p\) 能推出 \(q\),那么 \(q\) 推出的性质也能由 \(p\) 推出)。也就是说,点双连通的性质比边双连通更强。这一点在它们的基本性质中也有体现:不经过重复点一定不经过重复边,两条无重复点的路径一定是两条无重复边的迹,环一定是回路,所以 点双连通推出边双连通

  • 根据基本性质,一般 “不经过重复边” 的问题借助边双解决,而 “不经过重复点” 的问题借助点双解决。
  • 每个点恰属于一个边双,每条边可能恰属于一个边双(非割边),也可能不属于任何边双(割边);每条边恰属于一个点双,每个点可能属于一个点双(非割点),也可能属于多个点双(割点)。

1.2.5 点双连通的更多性质

以下假设图无自环

在点双基本性质的基础上,可以推出一些点双的常用性质。

基本性质:对于 \(n\geq 3\) 的点双中任意两点 \(x\neq y\),存在经过 \(x, y\) 的简单环。

性质 \(1\)

对于 \(n \geq 3\) 的点双中任意一点 \(x\) 与一边 \(e\),存在经过 \(x, e\) 的简单环。

证明

\(e = (u, v)\) 拆成 \((u, w)\)\((w, v)\) 不影响点双连通性。根据基本性质,存在经过 \(x, w\) 的简单环。因 \(w\) 仅与 \(u, v\) 相连,故 \((u, w), (w, v)\) 在环上,将这两条边替换为 \((u, v)\) 得经过 \(x, e\) 的简单环。

不影响点双连通性的证明:

  • 删去 \(u\)\(v\),由 \(w\)\(v\)\(u\) 连通且删去 \(u\)\(v\) 后原图连通可知 \(u, v\) 不是割点。这一步要求 \(u\neq v\),用到了无自环的条件。
  • 删去 \(u, v, w\) 以外的点,将 \((u, w)\)\((w, v)\) 视为 \((u, v)\),原图连通。
  • 删去 \(w\),相当于删去 \((u, v)\),若原图不连通则 \((u, v)\) 为割边,当 \(n\geq 3\)\(u\)\(v\) 为割点,矛盾。\(\square\)

实际上,钦定经过一条边和一个点是几乎等价的(除非钦定经过 \(x\)\(y\)),它们可以相互归约:若证明了可以钦定经过一条边,则钦定经过某点时,钦定经过该点任意邻边即可。若证明了可以钦定经过一个点,则钦定经过某条边时,将该边像上述证明一样拆成两条边,并钦定经过中间点即可。

性质 \(2\)

对于 \(n \geq 3\) 的点双中任意不同两点 \(x\neq y\) 与一边 \(e\),存在 \(x \rightsquigarrow e \rightsquigarrow y\) 的简单路径。

证明

由性质 \(1\),存在经过 \(x, e\) 的简单环 \(C\),若 \(y\in C\) 则结论成立。否则令 \(P\) 为任意 \(y\rightsquigarrow x\) 的有向路径,考虑 \(P\) 上第一个属于 \(C\) 的交点 \(z\),存在使得 \(z \neq x\)\(P\),否则 \(x\) 为割点:删去 \(x\)\(y\) 无法到达 \(C\) 的剩余结点。令路径 \(Q\)\(P\)\(y \rightsquigarrow z\) 的部分,接上 \(z\) 通过 \(C\) 上有 \(e\) 的一侧到 \(x\) 的路径,则 \(Q\) 即为所求。\(\square\)

性质 \(3\)

对于 \(n\geq 3\) 的点双中任意不同三点 \(x, y, z\),存在 \(x \rightsquigarrow z\rightsquigarrow y\) 的简单路径。

证明

考虑某条以 \(z\) 为端点的边 \(e\)。由性质 \(2\),存在 \(x\to e\to y\),因此存在 \(x\to z\to y\)\(\square\)

1.3 Tarjan 求割点

前置知识:DFS 树,DFS 序。

注意区分:DFS 序表示对一张图 DFS 得到的结点序列,而时间戳 dfn 表示每个结点在 DFS 序中的位置。

\(x\) 的子树为 \(x\) 在 DFS 树上的子树,包含 \(x\) 本身,记作 \(T(x)\)。记 \(T'(x) = V\backslash T(x)\),即整张图除了 \(T(x)\) 以外的部分。

不妨认为 \(G\) 是无向连通图。对于非连通图,对每个连通分量分别求割点。

笔者希望提出一种新的理解 Tarjan 算法的方式。网上大部分博客讲解 Tarjan 算法时 low 数组凭空出现,抽象的定义让很多初学者摸不着头脑,从提出问题到解决问题的逻辑链的不完整性让我们无法感受到究竟是怎样的灵感启发了这一算法的诞生。

1.3.1 非根结点的割点判定

\(x\) 不为 DFS 树的根,则 \(T'(x)\) 非空。

\(x\) 是割点,则删去 \(x\) 之后,对于 \(z\in T'(x)\),存在 \(y\) 和它不连通。而删去 \(x\) 之后 \(T'(x)\) 通过树边仍然连通,所以 \(y\in T(x)\)。而如果 \(y\)\(z\) 不连通,又因为 \(T'(x)\) 连通,那么 \(y\) 和所有 \(T'(x)\) 的点均不连通。

反之,若删去 \(x\) 之后存在 \(y\in T(x)\)\(T'(x)\) 的点均不连通,那么 \(x\) 显然是割点。这说明 \(x\) 是割点当且仅当存在 \(y\in T(x)\) 不经过 \(x\) 能到达的所有点均属于 \(T(x)\)

现在要刻画 “不经过 \(x\) 能到达的所有点均属于 \(T(x)\)”。

注意到,如果 \(y\in T(x)\) 不经过 \(x\) 就和 \(T'(x)\) 连通,那么存在 \(y\)\(v\in T'(x)\) 的路径,满足 \(v\) 是路径上第一个属于 \(T'(x)\) 的结点。设路径上倒数第二个点为 \(u\),则 \(u\in T(x)\)。如果 \((u, v)\) 是树边,那么 \(u = x\),矛盾。因此 \((u, v)\) 是非树边,那么 \(v\)\(u\) 的祖先(祖先后代性)。又因为 \(x\)\(u\) 的祖先且 \(v\)\(x\) 的子树外,所以 \(v\)\(x\) 的祖先。如下图。

进一步地,因为 \(x\) 的不同儿子子树之间没有非树边(子树独立性),设 \(x\) 的儿子 \(y'\) 的子树包含 \(y\),那么 \(u\in T(y')\)

因此,如果 \(y\) 不经过 \(x\)\(T'(x)\) 连通,即 \(x\) 不是割点,那么存在 \(u\in T(y')\) 使得 \(u\) 可以通过一条非树边到达 \(x\) 的祖先。设 \(f_x\) 表示与 \(x\) 通过 非树边 相连的所有点的时间戳的最小值,则条件可写为 \(f_u < d_x\)

  • 对于 \(T(y')\),如果存在 \(u\in T(y')\) 满足 \(f_u < d_x\),那么删去 \(x\)\(T(y')\) 的每个点和 \(T'(x)\) 均连通:\(T(y')\) 内所有点通过树边连通,且 \(u\)\(T'(x)\) 某点直接相连。

  • 反之,如果 \(T(y')\) 内所有点的 \(f\) 值均不小于 \(d_x\),那么删去 \(x\)\(T(y')\) 的每个点和 \(T'(x)\) 均不连通。因为如果连通,那么总得有一个点能一步连通。

这样,我们得到了非根结点的割点判定法则:

\(x\) 是割点当且仅当存在树边 \(x\to y'\),使得 \(y'\) 子树 不存在\(u\) 使得 \(f_u < d_x\)

这等价于存在 \(x\) 的儿子 \(y'\),满足 \(\min_{u\in T(y')} f_u \geq d_x\)

\(g_x\) 表示 \(x\) 的子树内所有点 \(u\in T(x)\)\(f_u\) 的最小值(low 的真正含义),根据树形 DP,有

\[g_x = \min\left(\min_{y'\in \mathrm{son}(x)} g_{y'}, \min_{(x, y) \in E\land (x, y)\notin T} d_y\right) \]

对于后半部分,忽略 \((x, y)\) 必须是非树边的条件不会导致错误:如果用儿子更新,显然没有问题。如果用父亲更新,即用 \(d_x\) 更新 \(g_y\),也不会导致错误,因为判定是 \(g_y\geq d_x\),有等号。但注意求解割边时不能忽略,因为判定是 \(g_y > d_x\)

特别地,若使用树边更新,则 \(g_y\) 会被 \(d_x\) 更新到,使得 \(g_y \leq d_x\)。此时判定条件等价于 \(g_y = d_x\)

说明:将 \(g_x\) 初始化为 \(d_x\) 显然不会导致错误。

应用:研究删去 \(x\) 后整张图的形态。删去 \(x\) 后,每个判定 \(x\) 为割点的 \(y'\)\(T(y')\) 单独形成一个连通块,剩余部分(其它所有 \(T(y')\)\(T'(x)\))形成一个连通块。因为判定割点的准则就是删去 \(x\)\(y'\) 是否与 \(T'(x)\) 连通。

  • 一个小结论:由 Tarjan 的过程可知只保留最浅的返祖边不改变图的点双连通性。在双极定向时有用(见图论 II)。

1.3.2 根的割点判定、代码

\(x\) 为 DFS 树的根。

\(x\) 在 DFS 树上有大于一个儿子,根据子树独立性,删去 \(x\) 后各儿子子树不连通,所以 \(x\) 是割点。反之删去 \(x\) 后剩余部分通过树边连通,\(x\) 不是割点。

综上,使用 Tarjan 算法求无向图 \(G\) 的所有割点的时间复杂度为 \(\mathcal{O}(n + m)\)

再次强调,以下代码仅在求解割点时正确。求解割边需要额外的特判。

模板题 代码。

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e5 + 5;
int n, m, R;
int dn, dfn[N], low[N], cnt, buc[N]; // dfn 是时间戳 d, low 是 g
vector<int> e[N];
void dfs(int id) {
  dfn[id] = low[id] = ++dn; // 将 low[id] 初始化为 dn 不会导致错误, 一般都这么写
  int son = 0;
  for(int it : e[id]) {
    if(!dfn[it]) {
      son++, dfs(it), low[id] = min(low[id], low[it]);
      if(low[it] >= dfn[id] && id != R) cnt += !buc[id], buc[id] = 1; // 写 low[it] == dfn[id] 也可以
    }
    else low[id] = min(low[id], dfn[it]);
  }
  if(son >= 2 && id == R) cnt += !buc[id], buc[id] = 1;
}
int main() {
  cin >> n >> m;
  for(int i = 1; i <= m; i++) {
    int u, v;
    cin >> u >> v;
    e[u].push_back(v), e[v].push_back(u);
  }
  for(int i = 1; i <= n; i++) if(!dfn[i]) R = i, dfs(i);
  cout << cnt << endl;
  for(int i = 1; i <= n; i++) if(buc[i]) cout << i << " ";
  return 0;
}

例题:P3469。

1.4 割边的求法

1.4.1 Tarjan

Tarjan 求割边的思路和求割点的思路类似。

探究一条边是割边的充要条件。

首先,树边使得整张图连通,所以割掉非树边不影响连通性。因此 \(e = (u, v)\) 是割边的 必要条件\(e\) 为树边。

不妨设 \(v\)\(u\) 的儿子。若割掉 \(e\) 后图不连通,那么因为 \(T(v)\)\(T'(v)\) 内部通过树边连通,所以图只能分裂为 \(T(v)\)\(T'(v)\) 两部分。这说明 \(T(v)\) 的所有结点必须通过 \(e\) 才能到达 \(T'(v)\),即 \(e\) 是连通 \(T(v)\) 内外的 “桥”。

考虑如何判定这种情况。根据求解割点的经验,不难得出 \(e\) 为割边当且仅当 \(g_v > d_u\)。对比割点判定法则,没有等号的原因为:删去的是边而非结点,所以只要子树内结点能绕过 \(e\) 到达 \(T'(v)\),包括 \(u\) 本身,那么 \(e\) 就不是割边。

Tarjan 求割边有个细节,就是判断非树边。对于当前边 \(u\to v\),若 \(v\)\(u\) 的父亲,那么跳过这条边。这样做在有重边时会出现错误,因为会将树边的重边也判为树边。

解决方法是记录边的编号。对于 vector,在 push_back 时将当前边的编号一并压入。对于链式前向星,使用成对变换技巧:初始化 cnt = 1,每条边及其反边在链式前向星中存储的编号分别为 \(2k\)\(2k + 1\),将当前边编号异或 \(1\) 即得反边编号。

算法的时间复杂度为 \(\mathcal{O}(n + m)\)

1.4.2 树上差分法

求出 \(G\) 的任意一棵 DFS 树 \(T\),割边只能是树边。容易发现,一条树边是割边当且仅当不存在非树边 \((u, v)\) 使得该树边属于 \(u, v\)\(T\) 上的简单路径。

  • 如果存在,那么删去树边后 \(T\) 分裂出的两部分通过 \((u, v)\) 连接,树边不是割边。
  • 否则,删去后 \(T\) 分裂出的两部分不连通,树边是割边。

对于非树边 \((u, v)\),将 \(u, v\) 之间所有边标记为非割边。根据非树边的祖先后代性,树上差分即可。

算法的时间复杂度为 \(\mathcal{O}(n + m)\)

1.5 边双连通分量缩点

1.2.1 小节有结论:将 \(G\) 的割边删去,剩余每个连通分量是原图的边双连通分量。得到原图边双连通分量之后,记录每个点所在边双编号,容易对 \(G\) 边双缩点。

接下来介绍 Tarjan 算法实现的边双连通分量缩点,类似方法可应用在点双缩点上。关于点双缩点,见 “图论 III” 广义圆方树。

可以先 Tarjan 找出所有割边,再 DFS 找出所有边双连通分量,但太麻烦。我们希望一遍 Tarjan 就可以求出所有割边和边双连通分量。为此,需要对算法进行一些改进。

考虑 \(G\) 的 DFS 树 \(T\),找到其中任意一条割边,满足子树内没有割边。这样,该割边的子树内所有结点形成边双连通分量。将这些结点从图中删去,包括与它们相邻的所有边。重复该过程直到整张图不含割边,则剩下来的图也是原图的边双连通分量。整个过程可以视为在边双缩点树上 剥叶子,不断地断开一条与叶子相连的割边,剥下一个边双连通分量。如果有 \(c\) 条割边,那么有 \(c + 1\) 个边双连通分量。

考虑将上述操作与 Tarjan 算法相结合:回溯时,若判定 \(u\to v\) 为割边,则 \(v\) 的子树内还没有被删去的点形成一个边双,将它们全部删去。具体地,维护栈 \(S\),表示已经访问过且还未被删去的所有结点。每次 DFS 进入一个结点时,将其压入栈,那么从栈底到栈顶时间戳依次递增。遇到割边 \(u\to v\) 时,从栈顶到 \(v\) 的所有结点构成一个边双,将它们全部弹出。

最后栈内剩下一些点,它们单独形成一个边双,且包含根结点。不要忘记将它们弹出。

正确性说明(直观理解,非严格证明):设原图割掉 \((u, v)\) 后形成的连通块分别为 \(u\in U\)\(v\in V\)。由于判定 \(u\to v\) 为割边是在回溯时进行的,所以 \(V\) 中所有结点全部被访问过,且全部在 \(v\) 之后访问,即栈中从 \(v\) 往上一直到栈顶均属于 \(V\),这些结点就是我们要删除的。对于 \(V\) 内部的所有割边(下图的虚边),我们在判定它们为割边时就已经处理掉了它们对应的边双,所以弹出结点内部也不会有割边。

上述思想在点双缩点和强连通分量缩点时也会用到。

算法的时间复杂度 \(\mathcal{O}(n + m)\)

模板题 代码。

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 5e5 + 5;
int n, m;
vector<pair<int, int>> e[N];
vector<vector<int>> ans;
int dn, dfn[N], low[N], stc[N], top;
void form(int id) {
  vector<int> S;
  for(int x = 0; x != id; ) S.push_back(x = stc[top--]);
  ans.push_back(S);
}
void tarjan(int id, int eid) {
  dfn[id] = low[id] = ++dn;
  stc[++top] = id;
  for(auto _ : e[id]) {
    if(_.second == eid) continue;
    int it = _.first;
    if(!dfn[it]) {
      tarjan(it, _.second);
      low[id] = min(low[id], low[it]);
      if(low[it] > dfn[id]) form(it);
    }
    else low[id] = min(low[id], dfn[it]);
  }
}
int main() {
  ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
  cin >> n >> m;
  for(int i = 1; i <= m; i++) {
    int u, v;
    cin >> u >> v;
    e[u].push_back({v, i});
    e[v].push_back({u, i});
  }
  for(int i = 1; i <= n; i++) {
    if(!dfn[i]) tarjan(i, 0), form(i);
  }
  cout << ans.size() << "\n";
  for(auto S : ans) {
    cout << S.size() << " ";
    for(int it : S) cout << it << " ";
    cout << "\n";
  }
  return 0;
}

例题:P2860,CF51F。

1.6 例题

P3469 [POI2008] BLO-Blockade

一道 Tarjan 求割点的练手题。

设删去与结点 \(u\) 相连的所有边之后形成的连通块大小分别为 \(s_{1\sim k}\),则答案为 \(\sum_{i = 1} ^ k s_i (n - s_i)\)。注意,不要忘记 \(u\) 没有被删去,它本身是一个大小为 \(1\) 的连通块。

因为 \(v\) 判定 \(u\) 为割点当且仅当封锁 \(u\) 之后 \(v\) 及其子树与整张图剩余部分不连通,所以考虑所有判定 \(x\) 为割点的 \(y_i\),它们的子树分别单独形成连通块。除去这些结点后,还有一个大小为 \(n - 1 - \sum size(y_i)\) 的连通块(可能为空,但不影响答案)。

时间复杂度 \(\mathcal{O}(n)\)代码

双倍经验:SP15577 Blockade

P2860 [USACO06JAN] Redundant Paths G

题目相当于添加最少的边使得整张图变成一个边双。

考虑边双缩点得到缩点树 \(T\),其上所有边都是原图割边。显然,如果在 \(u, v\) 之间加一条边,设 \(U\)\(u\) 所在边双对应的缩点树上的结点,\(V\) 同理,则相当于将 \(U, V\) 简单路径上的所有边变成非割边。

我们希望用最少的路径覆盖 \(T\) 的每一条边。对此有经典结论:最小路径数为 \(T\) 的叶子个数除以 \(2\) 上取整。

证明这是答案下界非常容易,因为每条链至多覆盖两个叶子到它唯一相邻的点的边。当只有两个点的时候特殊讨论。接下来给出一个达到该下界的构造方法。

称两个叶子匹配表示在最终方案中,存在一条连接它们的链。

首先,当叶子个数为奇数时,考虑任意一个叶子 \(u\),从它出发找到第一个度数 \(\geq 3\) 的结点 \(v\),令 \(u, v\) 匹配,转化为叶子个数为偶数的情况。如果不存在度数 \(\geq 3\) 的结点,则 \(T\) 为一条链,与叶子个数为奇数矛盾。

先将所有叶子任意两两匹配,再调整。设在当前方案中,存在一条边 \((u, v)\) 没有被覆盖,设断开 \((u, v)\)\(u, v\) 分别属于连通块 \(U, V\)

\(U\)\(V\) 不可能只有一个原树的叶子,否则该叶子和另外某个叶子匹配时必然经过 \((u, v)\),与 \((u, v)\) 没有被覆盖矛盾。同理可证 \(U\)\(V\) 不可能有奇数个叶子。

因此,当前方案必然是 \(U\) 的所有偶数个叶子两两匹配,\(V\) 的所有偶数个叶子两两匹配。设 \(U\) 的某两个匹配的叶子为 \(u_1, u_2\),它们在以 \(u\) 为根时的 LCA 为 \(u_d\)。对于 \(V\),类似定义 \(v_1, v_2\)\(v_d\)

当前方案覆盖了 \(u_1\rightsquigarrow u_d \rightsquigarrow u_2\)\(v_1 \rightsquigarrow v_d \rightsquigarrow v_2\) 的所有边,但令 \(u_1\)\(v_1\)\(u_2\)\(v_2\) 匹配,\(u_i\rightsquigarrow u_d \rightsquigarrow u\to v \rightsquigarrow v_d \rightsquigarrow v_i\)\(i = 1, 2\))的所有边被覆盖。原来被覆盖的边仍被覆盖,同时 \((u, v)\) 也被覆盖了。

因此,对于当前方案,若某条边没有覆盖,通过上述调整一定能不改变链的条数使得原来被覆盖的边仍被覆盖,且该边也被覆盖。答案上界得证。

时间复杂度 \(\mathcal{O}(n + m)\)代码

CF51F Caterpillar

题目要求不能存在环,所以首先将所有边双缩成一个点。缩点后整张图会变成一棵森林,先处理每一棵树,再用连通块数量 \(-1\) 次操作将它们连起来。

不妨认为原图连通。

考虑确定主链后如何用最少的操作使得一棵树变成毛毛虫。

对于除了主链以外的结点,考虑它是否作为最终挂在主链旁边的叶子。将主链看成根,具有祖先后代关系的点对不能同时被选中作为叶子,因为此时后代和主链之间隔了一个祖先,说明它到主链的距离 \(\geq 2\),与限制不符。

问题转化为选择最多的点使得它们之间没有祖先后代关系。我们的决策是选择所有叶子,因为若一个非叶子被选中,我们一定可以取消选择它,并选它的所有儿子。

因此,最终保留的结点为主链上的结点和我们选中作为叶子的结点。这说明对于路径 \(P\),它作为主链时对应的最小操作次数为 \(n - |P| - L\),其中 \(n\) 是树的大小,\(L\) 是主链外叶子的数量。

\(P\) 的两端不是叶子时,总可以调整使得 \(P\) 的两端是叶子,且 \(n - |P| - L\) 不变或变小:向任意方向延伸,每次 \(|P|\) 增加 \(1\),且 \(L\) 要么不变(当延伸的点不是叶子),要么减少 \(1\)(当延伸的点是叶子)。

设缩点树的叶子数量为 \(K\),则操作次数为 \(n - K + 2 - |P|\)\(n - K + 2\) 是定值,我们希望 \(|P|\) 最大,自然是选取树的直径,同时恰好满足了 \(P\) 的两端是叶子的要求。

时间复杂度 \(\mathcal{O}(n + m)\)代码

2. 有向图可达性:强连通分量

研究有向图可达性时,强连通分量是最基本的结构。

本章研究强连通分量。默认图为 有向弱连通图

2.1 相关定义

  • 强连通:对于有向图的两点 \(u, v\),若它们相互可达,则称 \(u, v\) 强连通,这种性质称为 强连通性

显然,强连通是等价关系,强连通性具有传递性

  • 强连通图:满足任意两点强连通的有向图称为 强连通图。它等价于图上任意点可达其它所有点。

  • 强连通分量:有向图的极大强连通子图称为 强连通分量(Strongly Connected Component,SCC)。

强连通分量在求解与有向图可达性相关的题目时很有用,因为在只关心可达性时,同一强连通分量内的所有点等价。

有了使用 Tarjan 算法求割点,割边和边双缩点的经验,我们可以自然地将这些方法进行调整后用于强连通分量缩点。不过在此之前,我们需要研究有向图 DFS 树的性质,毕竟整个过程离不开 DFS。

2.2 有向图 DFS 树

不同于无向图,对于弱连通图 \(G\),从某一点出发 DFS,不一定能访问到图上的所有结点。一般而言,有向图的 “DFS 树” 是一个森林:按任意顺序遍历每个结点,若当前点没有 DFS 过,则从该点开始 DFS,并按构建无向图 DFS 树的方式构建以该点为根的有向图 DFS 树。这会形成若干棵 DFS 树,在研究强连通性时,它们之间相互独立。对于任意强连通的两点 \(u, v\),第一次访问它们所在的强连通分量的 DFS 树一定同时包含 \(u, v\)。因此不同 DFS 树之间的任意两点不强连通,只需考虑一棵 DFS 树。

考察有向图 DFS 树 \(T\) 的形态,从而得到判定 SCC 的准则。

除了有向树边以外,\(T\) 还有一些边:

  • 从祖先指向后代的非树边,称为 前向边
  • 从后代指向祖先的非树边,称为 返祖边
  • 两端无祖先后代关系的非树边,称为 横叉边

前向边和返祖边由无向图 DFS 树的非树边的两种定向得到,而横叉边则是无向图 DFS 树所不具有的,因为无向图 DFS 树的非树边具有祖先后代性。

为什么有向图 DFS 树会出现横叉边?因为一个后访问到的结点可以向之前访问的结点连边。例如 \(G = (\{1, 2, 3\}, \{1\to 2, 1\to 3, 3\to 2\})\),如果我们在 DFS 时先访问结点 \(2\),那么在访问结点 \(3\) 时就会找到横叉边 \(3\to 2\)

接下来讨论有向边 \(u\to v\),设 \(d = lca(u, v)\)\(d\to u\) 的第一个结点为 \(u'\)(若存在),\(v'\) 同理。设 \(fa(i)\) 表示 \(i\)\(T\) 上的父亲。设 \(T(i)\) 表示 \(i\) 的子树。设 \(dfn(i)\) 表示点 \(i\) 的时间戳。

尽管子树的独立性变弱了,但我们依然可以发现一些性质:

  • 对于前向边 \(u\to v\)\(u\) 本身就能通过树边到达 \(v\),删去后没有任何影响。它甚至不影响可达性。
  • 对于返祖边 \(u\to v\),它使 \(T\) 上从 \(v\)\(u\) 的路径上的所有点之间强连通。多个返祖边结合可以形成更大更复杂的强连通结构。
  • 对于横叉边 \(u\to v\)\(dfn(v) < dfn(u)\)。否则 \(u\) 先被访问,那么无论是 \(u\) 的儿子访问到了 \(v\),还是 \(u\) 本身直接访问 \(v\),离开 \(u\) 之前 \(v\) 一定被访问过了,即 \(v\in T(u)\)。那么 \(u\to v\) 为前向边,矛盾。

综合上述三条性质,可知返祖边和横叉边均减小时间戳,只有树边增大时间戳。前向边不影响强连通性,所以忽略掉所有前向边。

考虑横叉边 \(u\to v\),则 \(dfn(v) < dfn(u)\)。为了探究横叉边对强连通性的影响,对于 \(dfn(v) < dfn(u)\) 的点对 \((u, v)\),我们希望知道 \(v\) 可达 \(u\) 的充要条件。

\(v\) 可达 \(u\),那么 \(v\rightsquigarrow u\) 的路径上存在一条边 \(x\to y\) 使得 \(dfn(x) < dfn(u) \leq dfn(y)\)。因为 \(dfn(x) < dfn(y)\),所以 \(x\to y\) 是树边。当 \(y = u\) 时,显然 \(u\in T(x)\);否则 \(y\neq u\),则 \(u\)\(x\) 之后,\(y\) 之前被访问,则 \(u\in T(x)\)。因此,若 \(dfn(v) < dfn(u)\)\(v\) 可达 \(u\),则 \(v\) 可达 \(fa(u)\)。对 \(u, u'\) 的树上路径上的所有点依次使用该结论,推出 \(v\) 可达 \(d\)

反之,若 \(v\) 可达 \(d\),显然 \(v\) 可达 \(u\)

结论

\(dfn(v) < dfn(u)\),则 \(v\) 可达 \(u\) 当且仅当 \(v\) 可达 \(d\)

  • 一个更深刻的结论和理解见图论 II 的 4.3 小节引理 2(一般图支配树)。

因此,对于横叉边 \(u\to v\),它对强连通性产生影响当且仅当 \(v\) 可达 \(d\)。此时 \(u, v\) 强连通。称这些边为有用的横叉边。

该结论可以进一步推出:

结论

\(u, v\) 强连通,则 \(u, v\) 在树上路径上的所有点强连通。

结论

强连通分量在有向图 DFS 树上弱连通。

2.3 Tarjan 求 SCC

因为每个 SCC 在 \(T\) 上的最浅结点唯一,故考虑在最浅的结点处求出包含它的 SCC。称一个点是关键点,当且仅当它是某个 SCC 的最浅结点。

考虑判定关键点。如果 \(x\) 不是关键点,那么因为 SCC 在 DFS 树上弱连通,所以 \(x\) 的父亲 \(fa\)\(x\) 强连通。考虑 \(x\)\(fa\) 的路径,存在一条边跳出 \(T(x)\),可知 \(x\) 的子树内存在点 \(u\),使得从 \(u\) 出发存在返祖边 \(u\to v\) 满足 \(v\)\(x\) 的祖先(下图的红色部分),或存在横叉边 \(u\to v\) 满足 \(v\) 可达 \(x\) 的祖先且 \(v\notin T(x)\)(下图的绿色部分)。无论哪种情况均有 \(dfn(v) < dfn(x)\)

因此,设 \(g_x\) 表示 \(T(x)\) 内所有结点 \(u\) 的「返祖边 \(u\to v\)」和「\(v\) 可达 \(d\) 的横叉边 \(u\to v\)」的所有 \(v\) 的最小时间戳。\(x\) 是关键点当且仅当 \(g_x\geq dfn(x)\)

证明

考虑证明 \(x\) 不是关键点当且仅当 \(g_x < dfn(x)\)

充分性:若 \(g_x < dfn(x)\),那么存在 \(u\in T(x)\),使得从 \(u\) 出发存在返祖边或有用的横叉边 \(u\to v\) 满足 \(dfn(v) < dfn(x)\)。如果 \(u\to v\) 是返祖边,那么 \(v\) 显然是 \(x\) 的祖先;如果 \(u\to v\) 是横叉边,那么 \(v\) 可达 \(d\),又因为 \(dfn(d) < dfn(v) < dfn(x)\)\(d, x\) 均为 \(u\) 的祖先,所以 \(d\)\(x\) 的祖先。无论哪种情况,都说明包含 \(x\) 的 SCC 包含 \(x\) 的祖先,\(x\) 不是关键点。

必要性:若 \(x\) 不是关键点,那么 \(x\) 可达 \(fa(x)\)。考虑任意一条 \(x\rightsquigarrow fa(x)\) 的路径,找到第一次离开 \(T(x)\) 的边 \(u\to v\)。若 \(u\to v\) 是返祖边,那么 \(v\)\(x\) 的祖先;若 \(u\to v\) 是横叉边,那么 \(dfn(v) < dfn(x)\)\(v\) 可达 \(fa(x)\),则 \(v\) 可达 \(u\),则 \(v\) 可达 \(d\)。无论哪种情况,都说明 \(g_x < dfn(x)\)\(\square\)

说明:一般将 \(g_x\) 初始化为 \(dfn(x)\),判定条件为 \(g_x = dfn(x)\)

类似边双缩点,这次我们依然采用 “剥叶子” 的方式:维护栈 \(S\),表示已经访问过但还没有被删去(未形成确定 SCC)的结点。在回溯时判定关键点。若 \(u\) 为关键点,则将栈顶一直到 \(u\) 的所有结点弹出,表示它们形成一个 SCC。

最后考虑如何求 \(g\)

  • 对于树边 \(u\to v\),用 \(g_v\) 更新 \(g_u\)
  • 对于前向边 \(u\to v\),用 \(dfn(v)\) 更新 \(g_u\) 没有任何影响,因为 \(dfn(v) \geq g_u\)
  • 对于返祖边 \(u\to v\),用 \(dfn(v)\) 更新 \(g_u\)
  • 对于横叉边 \(u\to v\),若 \(v\) 不可达 \(d\),那么 \(v\)\(d\) 不强连通,所以从 \(v\) 不断回溯至 \(d\) 的过程中一定有关键点弹出 \(v\)。否则 \(v\) 可达 \(d\),那么 \(v\)\(d\) 强连通,因为 \(d\) 没有被弹出,所以 \(v\) 也没有被弹出。但是直接这样说明会涉及 \(g\) 的正确性和弹出 SCC 的正确性之间的循环论证。注意到横叉边更新 \(g\) 的正确性只依赖于已经弹出的每个点集是 SCC,所以归纳假设已经弹出的每个点集为 SCC,这样当前弹出点集的每个点的 \(g\) 是正确的,从而保证了当前弹出点集也是 SCC。

对于返祖边 \(u\to v\)\(v\) 显然没有被弹出。

综上,得到转移:对于树边 \(u\to v\),用 \(g_v\) 更新 \(g_u\)。对于非树边 \(u\to v\),若 \(v\) 在栈内,则用 \(dfn(v)\) 更新 \(g_u\)

SCC 缩点后,得到的新图不含环,否则环上所有结点对应的 SCC 可以形成一个更大的 SCC。这说明 SCC 缩点图是一张 DAG。

重要结论:对于两个 SCC \(S_1, S_2\),若 \(S_1\) 可达 \(S_2\),则 \(S_1\)\(S_2\) 后弹出栈。按弹出顺序写下所有 SCC,得到缩点 DAG 的反拓扑序。因此,按编号从大到小遍历 SCC,就是按拓扑序遍历缩点 DAG。

以下是 模板题 题解。

因为可以多次经过同一个点,所以一旦进入某个 SCC,就一定可以走遍其中所有点,获得所有点权值之和的贡献。但是一旦离开 SCC,就没法再回来了,否则离开后遍历的点和当前 SCC 强连通。

因此,将有向图 SCC 缩点后得到 DAG,每个 SCC 缩点后的权值等于其中所有点的权值之和。问题转化为 DAG 最长带权路径,拓扑排序 DP 即可。时间复杂度 \(\mathcal{O}(n + m)\)

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 1e4 + 5;
int n, m, cn, col[N];
int a[N], val[N], f[N];
int top, stc[N], vis[N], dn, dfn[N], low[N];
vector<int> e[N], g[N];
void tarjan(int id) {
  vis[id] = 1, dfn[id] = low[id] = ++dn, stc[++top] = id;
  for(int it : e[id]) {
    if(!dfn[it]) tarjan(it), low[id] = min(low[id], low[it]); // 树边
    else if(vis[it]) low[id] = min(low[id], dfn[it]); // 在栈内则更新
  }
  if(dfn[id] == low[id]) {
    col[id] = ++cn;
    while(stc[top] != id) col[stc[top]] = cn, vis[stc[top--]] = 0; // 弹出栈内结点到 id 形成强连通分量
    vis[id] = 0, top--;
  }
}
int main() {
  cin >> n >> m;
  for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
  for(int i = 1; i <= m; i++) {
    int u, v;
    scanf("%d%d", &u, &v);
    e[u].push_back(v);
  }
  for(int i = 1; i <= n; i++) if(!dfn[i]) tarjan(i);
  for(int i = 1; i <= n; i++) {
    val[col[i]] += a[i];
    for(int it : e[i]) {
      if(col[i] != col[it]) g[col[i]].push_back(col[it]);
    }
  }
  int ans = 0;
  for(int i = cn; i; i--) { // 按编号顺序从大到小遍历就是按拓扑序遍历
    f[i] += val[i];
    ans = max(ans, f[i]);
    for(int it : g[i]) f[it] = max(f[it], f[i]);
  }
  cout << ans << endl;
  return 0;
}

2.4 Kosaraju

Kosaraju 的思想:利用 SCC 内任意两点相互可达的性质,先一次 DFS 求出缩点图的拓扑序,再一次 DFS 求出所有 SCC。

考虑能否通过时间戳求出拓扑序。设 SCC 的编号为它包含的所有点的最大时间戳,通过编号不能推导出关于拓扑序的信息:对于两个 SCC \(S_1, S_2\)\(S_1\)\(S_2\) 的拓扑序大小和编号大小之间没有联系。无论 \(S_1\) 的编号小于还是大于 \(S_2\)\(S_1\) 均可能可达 \(S_2\)

时间戳不管用的本质原因在于,我们每访问到一个点就将其加入 DFS 序,这要求 DFS 的顺序就是合法拓扑序,但显然做不到:我们甚至不知道从哪里入手开始 DFS。

拓扑序是不现实的,只能寄希望于反拓扑序。如果我们将顺序改为离开每个点的次序,整个问题就豁然开朗了:离开 \(u\) 的时候,所有 \(u\) 可达的结点一定被遍历过了(这里可以看出为什么 SCC 需要被编号为最大离开时间,而不是最小或任意),所以若 \(S_1\) 可达 \(S_2\),那么 \(S_1\) 的编号大于 \(S_2\)。即若 \(S_1\) 的编号小于 \(S_2\),那么 \(S_1\) 不可达 \(S_2\)

接下来是第二次 DFS。

从离开时间最大的结点开始 DFS,会访问到它所在的 SCC。为保证其它 SCC 不被访问到,将所有边的方向反过来。这不改变任意两点之间的强连通性,所以反图的 SCC 划分方案和原图相同,但过程中不会访问其它 SCC,因为它们的编号小于当前 SCC,在原图上不可达当前 SCC,因而在反图上不可由该 SCC 到达。

将遍历到的结点打包成 SCC 删去(标记为已访问),不断找到最大的未被访问的结点重复上述过程,即可得到原图的 SCC 划分。时间复杂度 \(\mathcal{O}(n + m)\)

一般 SCC 缩点优先考虑 Tarjan,它只需要一次 DFS,而 Kosaraju 需要两次。Kosaraju 相比 Tarjan 的优势在于:整个过程只有 DFS 和标记。当图是稠密图时,可以 bitset 加速做到除预处理外 \(\mathcal{O}(\frac {n ^ 2} w)\)。而 Tarjan 是严格 \(\mathcal{O}(n + m)\) 的,更新 \(g\) 必须考虑到每条边。例如:多次询问保留编号在一段区间的边的 SCC 数量,在特定的数据范围下,莫队 + bitset 优化 Kosaraju 比 Tarjan 优秀。

2.5 例题

P3436 [POI2006] PRO-Professor Szu

对于一个 SCC,如果它内部有边且可达 \(n + 1\),则方案数为 \(+\infty\)

SCC 缩点后,统计出所有这样的 SCC。设 \(f_i\) 表示编号为 \(i\) 的 SCC 到 \(n + 1\) 所在 SCC 的方案数,建反图拓扑排序,若当前 SCC 内部有边且可达 \(n + 1\),即 \(f_i > 0\),那么令 \(f_i \gets 36501\)。对于每条反边 \(u\to v\),将 \(f_v\) 加上 \(f_u\) 后对 \(36501\)\(\min\)

时间复杂度 \(\mathcal{O}(n + m)\)代码

P7737 [NOI2021] 庆典

首先 SCC 缩点。

缩点后删去重边,利用题目性质保证每个点的入度不超过 \(1\):如果 \(x\to z\)\(y\to z\),设 \(x\to y\),那么删去 \(x\to z\),可达性不变。这说明图的可达性可以用一棵叶向树刻画。

称给定的 \(2(k + 1)\) 个点为关键点。称关键点 \(i\) 是好点当且仅当 \(s\rightsquigarrow i\)\(i\rightsquigarrow t\)。一个点可以经过当且仅当其祖先和后代均存在好点。

求好点的方法:设新图 \(G\),初始为空。对于关键点 \(i, j\),若 \(i\)\(j\) 的祖先或新加边 \(i\to j\),则在 \(G\) 上连边 \(i\to j\)\(s\)\(G\) 上可达的所有点就是 \(s\) 可达的关键点。对于 \(t\) 同理。复杂度 \(\mathcal{O}(k ^ 2)\)

法一:对于好点 \(i, j\)\(i\)\(j\) 的祖先,\(i\rightsquigarrow j\) 上所有城市均可以经过,树剖求链并。时间复杂度 \(\mathcal{O}(nk ^ 2 \log n\log\log n)\)

法二:用类似虚树的方法统计答案,保证栈内是一条好点构成的从上到下的链。注意,使用 \(d\) 替换栈顶时,若 \(d\) 不等于栈顶且栈的大小为 \(1\),说明 \(d\) 不是好点,直接清空栈。时间复杂度 \(\mathcal{O}(nk ^ 2)\)代码

直接建虚树,加边 DFS,然后在虚树上统计答案,时间复杂度 \(\mathcal{O}(nk\log k)\),但常数较大。

*ARC092D Two Faced Edges

对于连接 SCC \(S_1, S_2\) 之间的边,反向后使得 SCC 数量减少当且仅当 \(S_1\) 在 DAG 上能够不经过这条边到达 \(S_2\)\(\mathcal{O}(nm)\) 求出任意两点之间的路径数对 \(2\)\(\min\) 判断。

对于 SCC 内部的边,若反向 \(u\to v\)\(u\) 不可达 \(v\),那么 \(u, v\) 不强连通,SCC 数量改变。否则 \(u\) 可达 \(v\),可达性不弱于原图可达性,不影响点对之间的可达性。

因此,反向 \(u\to v\) 后强连通性不变的充要条件为:反向 \(u\to v\)\(u\) 可达 \(v\)。因为反向边 \(v\to u\) 显然无用,故等价于删去 \(u\to v\)\(u\) 可达 \(v\)

问题转化为对每个点的每条出边 \(u\to v_i\) 求出在 \(u\) 在不经过 \(u\to v_i\) 的情况下可达哪些点。考虑到 \(u\) 保留的出边是一段前缀 \(v_{1\sim i - 1}\) 和后缀 \(v_{i + 1\sim k}\) 的形式,且路径不会经过同一个点两次,所以 \(u\to v_i\) 合法当且仅当保留 \(u\to v_{1\sim i - 1}\)\(u\to v_{i + 1\sim k}\) 时,\(u\) 可达 \(v\)

正反扫一遍出边,每次从 \(v_i\) 开始 DFS 并记录每个点的可达性,并在扫当前出边 \(u\to v_i\) 之前,若 \(u\) 可达 \(v_i\)\(u\to v_i\) 合法。总时间复杂度为 \(\mathcal{O}(nm)\)

注意:我们考虑的是保留 \(u\) 的一段前缀或后缀的出边时每个点的可达情况,因此当 DFS 到 \(u\) 时需及时返回。

进一步地,发现对于连接 SCC 之间的边 \(u\to v\),我们也希望判断去掉该边后 \(u\) 能否到达 \(v\)。这和 SCC 内部的边的形式一致。只不过对于不同 SCC 之间的边,若 \(u\not\rightsquigarrow v\) 则反转 \(u\to v\) 不会改变 SCC 个数,反之则改变,这与 SCC 内部的边相反。这样就不需要显式建出 DAG 了。

对整张图进行 \(\mathcal{O}(nm)\) 的深搜会 TLE,我们需要更加高效的算法。Bitset 优化图的遍历是经典套路:每次找到 \(u\) 的所有出边中第一个没有被访问的点。具体地,设 vis 表示每个结点是否访问过,e[u] 表示 \(u\) 的所有出边,则 (~vis & e[u])._Find_first() 即为所求。时间复杂度 \(\mathcal{O}(\frac {n ^ 3}{w})\)代码

启示:序列去掉一个位置的信息可由前缀和后缀合并得到。

*CF1361E James and the Chase

很好的强连通题,需要对有向图 DFS 树有一定理解。

因为题目保证图强连通,所以从任意一个点出发 DFS 可达图上所有点。

判定一个点是否合法:以该点为根的任意 DFS 树只有树边和返祖边:横叉边和前向边将导致大于一条简单路径。

首先找到任意合法点:随机 \(100\) 个点判定是否合法。如果合法点的数量不少于 \(0.2n\),那么单组测试点正确率不低于 \((1 - 0.8 ^ {100}) ^ {1000}\approx 1 - 2\times 10 ^ {-7}\)。然后根据没有横插边和前向边的性质,求出其它每个点是否合法。

性质:点 \(x\) 到它子树内的点有且仅有一条简单路径,因为一个点向下走的唯一途径是树边,向上走的唯一途径是返祖边。除了一定存在的一直向下走的简单路径,如果还有别的简单路径,则这条路径一定走了一条返祖边。这条返祖边一定指向 \(x\) 的祖先,因为如果指向 \(x\) 子树内的结点,那么由于走返祖边之前必须经过该结点,所以路径经过了重复点,不合法。而从 \(x\) 的祖先回到目标点必须再次经过 \(x\),也不合法,所以这样的简单路径不存在。这说明,如果 \(x\)\(y\) 有大于一条简单路径,那么 \(y\) 不在 \(x\) 的子树内,且这样的简单路径一定有且仅有一步从 \(x\) 的子树内跳了出去。

称一条返祖边 \(u\to v\) 覆盖 \(u, v\) 的树上简单路径的除了 \(v\) 以外的所有点,一个点可以通过覆盖它的返祖边离开它的子树。

如果点 \(x\) 被大于一条边覆盖,那么它就有两种方法离开子树,然后向下走到它的父亲,即 \(x\)\(x\) 的父亲的简单路径数量大于 \(1\)\(x\) 不合法。

如果 \(x\) 只有一种方法离开子树,设对应返祖边为 \(u\to v\)。因为不存在同时覆盖 \(x\)\(v\) 的返祖边,也就是覆盖 \(v\) 的返祖边的较深一端一定为 \(x\) 的祖先,所以从 \(x\) 经过 \(u\) 走到 \(v\) 之后,如果 \(v\) 到其它点有大于一条简单路径,因为这样的路径一定会跳出 \(v\) 的子树,所以一定不会经过 \(x\) 以及它的子树,将 \(x\)\(v\) 的路径接上这样的路径不会使得路径经过重复点。相反,如果 \(v\) 到其它所有点有且仅有一条简单路径,那么容易通过反证法证明 \(x\) 同样如此。

又因为一个非根节点必须能够离开子树(否则不满足原图强连通的性质),所以每个点被至少一条返祖边覆盖。因此,点 \(x\) 合法的充要条件是:\(x\) 为根,或者有且仅有一条返祖边 \(u\to v\) 覆盖 \(x\)\(v\) 合法。

用树上差分维护每个点被多少条边覆盖,再用树形 DP 求出每个点最浅能够跳到哪个祖先,最后从上往下 DP 求出每个点是否合法,时间复杂度 \(\mathcal{O}(n + m)\)

总时间复杂度 \(\mathcal{O}(c\cdot (n + m))\),其中 \(c\) 是随机次数。代码

3. 欧拉回路

小学奥数之一笔画问题。

3.1 相关定义

  • 欧拉路径:经过连通图中所有边恰好一次的迹称为 欧拉路径
  • 欧拉回路:经过连通图中所有边恰好一次的回路称为 欧拉回路
  • 欧拉图:有欧拉回路的图称为 欧拉图
  • 半欧拉图:有欧拉路径但没有欧拉回路的图称为 半欧拉图

欧拉图能够以任意一点作为起点一笔画整张图然后回到该点,而半欧拉图只能从 \(S\) 开始,\(T\neq S\) 结束一笔画。

3.2 欧拉图的判定

3.2.1 有向图

考察有向欧拉图 \(G\) 的性质。

首先 \(G\) 显然弱连通。此外,回路上从每个点出发的边的数量和到达该点的边的数量相等。因此 \(G\) 的每个点的入度等于出度。

对于满足上述条件的图 \(G\),从任意一点 \(u\) 出发不断走当前没走过的边,最后回到 \(u\),得到任意回路 \(C\)。我们不会在其它点 \(v\neq u\) 停下来,因为此时 \(v\) 的已经走过的入边数量大于已经走过的出边数量,所以总存在没走过的出边。

\(C\) 的边从 \(G\) 中删去,并删去所有度数为 \(0\) 的点,得到若干弱连通子图 \(G_i\)\(G_i\) 仍然满足每个点的入度等于出度,且和 \(C\) 有交点(根据 \(G\) 的弱连通性易证)。假设 \(G_i\) 存在欧拉回路,那么将每个 \(G_i\) 的欧拉回路分别插入 \(C\),即得 \(G\) 的欧拉回路。

综上,可归纳证明有向欧拉图的判定准则:

有向图 \(G\) 是欧拉图,当且仅当 \(G\) 弱连通且 \(G\) 的每个点的入度等于出度。

读者可类似推导有向半欧拉图的判定准则,这里不加证明地给出结论:

有向图 \(G\) 是半欧拉图,当且仅当 \(G\) 弱连通,存在两个点的入度分别等于出度减 \(1\) 和出度加 \(1\),且其余每个点的入度等于出度。

入度等于出度减 \(1\) 的点为欧拉路径的起点,入度等于出度加 \(1\) 的点为欧拉路径的终点。

3.2.2 无向图

考察无向欧拉图 \(G\) 的性质。

首先 \(G\) 显然连通。此外, 对于非起始点,欧拉回路每次进入该点后总会离开;对于起始点,欧拉回路每次离开该点后总会回来。每次进入和离开一个点均对其度数产生 \(1\) 的贡献,所以每个点的度数均为偶数。

对于满足条件的图 \(G\),从任意一点 \(u\) 出发不断走当前没走过的边,最后回到 \(u\),得到任意回路 \(C\)。我们不会在其它点 \(v\neq u\) 停下来,因为此时 \(v\) 的已经走过的邻边关于 \(v\) 的总度数为奇数,所以总存在没走过的邻边。如果将自环算成两条邻边,也可以理解为 \(v\) 的已经走过的邻边数量为奇数。

\(C\) 的边从 \(G\) 中删去,并删去所有度数为 \(0\) 的点,得到若干连通子图 \(G_i\)\(G_i\) 仍满足每个点的度数为偶数,且和 \(C\) 有交点。假设 \(G_i\) 存在欧拉回路,那么将每个 \(G_i\) 的欧拉回路分别插入 \(C\),即得 \(G\) 的欧拉回路。

综上,可以归纳证明无向欧拉图的判定准则:

无向图 \(G\) 是欧拉图,当且仅当 \(G\) 连通且 \(G\) 的每个点的度数为偶数。

读者可类似推导无向半欧拉图的判定准则,这里不加证明地给出结论:

无向图 \(G\) 是半欧拉图,当且仅当 \(G\) 连通且恰存在两个点的度数为奇数。

两个奇度点分别为欧拉路径的起点和终点。

3.2.3 混合图

给定图 \(G\)\(G\) 有若干无向边和有向边,称为混合图。

忽略掉将所有有向边改为无向边后图不连通的情况,此时 \(G\) 显然不存在欧拉回路。

考虑钦定无向边的方向,将 \(G\) 转化为有向图。有向图存在欧拉回路的充要条件是弱连通且每个点的入度等于出度,第一个条件已经满足,要求满足第二个条件。

注意到每个点的入度加出度固定,就是它在 \(G\) 上的度数。所以,若原图存在奇度数点,无解。否则限制转化为要求每个点的出度为 \(\frac {d(i)} 2\)。若只考虑有向边时出度或入度已经大于 \(\frac {d(i)} 2\),无解。否则要求有 \(\frac {d(i)} 2 - d ^ +(i)\) 条将无向边定向后从 \(i\) 出发的有向边。

每条无向边要么给 \(d ^ +(u)\)\(1\),要么给 \(d ^ +(v)\)\(1\)。经典网络流模型:将所有无向边 \(e_i = (u_i, v_i)\) 抽象为左部点,所有点作为右部点。源点 \(S\) 向左部点连容量 \(1\) 的边。左部点 \(e_i\) 向右部点 \(u_i\)\(v_i\) 分别连容量 \(1\) 的边。右部点 \(i\) 向汇点连容量 \(\frac {d(i)} 2 - d ^ + (i)\) 的边。若最大流为无向边总数,则有解,且当前流给出了一种定向方案。否则无解。

对于半欧拉图判定,\(G\) 必须存在两个奇度点。枚举哪个奇度点作为起点,然后类似跑网络流即可。

3.3 Hierholzer

在研究欧拉图的判定条件时,我们通过构造一条合法的欧拉回路证明了判定条件的充要性。而构造的方法就可以用来求欧拉回路。

先考虑有向图,因为无向图需要处理重边的问题。

Hierholzer 的核心是不断往当前回路的某个点中插入环。具体地,从任意一点出发 DFS,找到任意一个回路 \(C\)。删去 \(C\) 上的所有边,然后依次加入 \(C\) 的每个结点 \(p_1, p_2, \cdots, p_{|C|}, p_1\)。加入 \(p_i\) 之前,我们先递归找到 \(p_i\) 所在连通子图的欧拉回路,将其插入 \(C\)。这是一个递归形式的算法。

使用链式前向星维护每个点的剩余出边(类似 Dinic 的当前弧优化),每条边只会被遍历一次。用双向链表维护回路的合并,时间复杂度 \(\mathcal{O}(n + m)\)

做法的时间复杂度优秀,但实现起来有些复杂。我们可以先理解复杂的方法做了些什么,再思考有哪些地方可以简化。

从整体上考察,我们实现了这样的步骤:从一个起点开始找到一个回路,然后以环上的每个点为起点找到一个回路,不断递归下去。为了依次从回路上的每个点开始找回路,需要显式地求出这个回路,再依次处理上面的所有结点。所以我们需要一个 DFS 函数找回路,另一个递归式函数求欧拉回路。

但显式地求出回路是不必要的:我们可以按任意顺序从当前回路上的点出发找回路。考虑在 DFS 找回路的回溯过程中直接对当前回路上的每个点找回路。换言之,将原本找回路的顺序倒过来,这样就没有必要显式地求出当前回路,而是在回溯的过程中,一边对当前点找回路,一边往回路中插入当前回路。

综上,我们得到求欧拉回路的最常用算法 —— Hierholzer 的具体步骤:遍历当前点 \(u\) 的所有出边 \(u\to v\)。若该边未走过,则向点 \(v\) DFS。遍历完所有出边后,将 \(u\) 加入回路。最终得到一条反着的欧拉回路(图有向,且倒序加入环),将其翻转即可。

若要求字典序最小,只需在一开始将每个点的所有出边从小到大排序,并从编号最小的结点开始 DFS。这样,在欧拉回路上从左往右看,每个点都取到了理论最小的编号。

对于无向图欧拉回路,和有向图一样做。注意判重边,使用求割边时的成对变换技巧,用 \(2k\)\(2k + 1\) 作编号存储一条边的两个方向。注意不能只判父亲,因为可能有重边。

对于有向图和无向图的欧拉路径,从出度等于入度加 \(1\) 的点或奇度数点开始 DFS,其它部分和欧拉回路没有区别。

在具体实现中,需要先判定再求解,否则求出的欧拉路径有误:存在路径上相邻的两点在图上不相邻。

模板题 代码。

#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e5 + 5;
int n, m, lar;
int top, stc[N], in[N], hd[N]; // hd 用于当前弧优化
vector<int> e[N];
void dfs(int id) {
  for(int &i = hd[id]; i < e[id].size(); ) dfs(e[id][i++]);
  stc[++top] = id;
}
int main() {
  cin >> n >> m;
  for(int i = 1; i <= m; i++) {
    int u, v;
    scanf("%d%d", &u, &v);
    e[u].push_back(v), in[v]++;
  }
  for(int i = 1; i <= n; i++) {
    sort(e[i].begin(), e[i].end());
    if(abs(int(e[i].size()) - in[i]) > 1) puts("No"), exit(0); // 注意 e[i].size() 要强制转成 int
    if(e[i].size() > in[i]) {
      if(lar) puts("No"), exit(0);
      else lar = i;
    }
  }
  dfs(lar ? lar : 1);
  if(top != m + 1) puts("No"); // 图不连通
  else {
    reverse(stc + 1, stc + top + 1);
    for(int i = 1; i <= top; i++) cout << stc[i] << " ";
  }
  return 0;
}

3.4 例题

P2731 [USACO3.3] 骑马修栅栏 Riding the Fences

无向图最小字典序欧拉路径板子题。

代码

P1127 词链

从每个字符串的第一个字符向最后一个字符连边,跑有向图欧拉回路。

注意,对邻接链表排序要按照每条边对应字符串的字典序排序,而非指向结点的编号大小。

时间复杂度 \(\mathcal{O}(S\log S)\),其中 \(S = \sum |s_i|\)代码

P3520 [POI2011] SMI-Garbage

\(f(i)\) 表示以 \(i\) 为一端的需要经过的边的数量。对于一条回路,所有点度数均为偶数,因此一次操作后 \(f(i)\) 的奇偶性不变。若存在 \(f(i)\) 为奇数,则无解。否则,注意到有解是需要经过的边形成的图的每个连通分量均存在欧拉回路的充要条件,对这张图跑一遍欧拉回路。因为要求不能经过相同的点,而路径上相同的点之间也一定是回路,所以借助栈把相同的点之间的回路弹出。

时间复杂度 \(\mathcal{O}(n + m)\)代码

*P3443 [POI2006] LIS-The Postman

题目保证无重边,所以每条路径片段的限制形如片段中相邻的两条边必须先后走。

所有限制关系形成若干条边先后走的链。将每条链缩成起点到终点的一条边,然后跑欧拉回路,最后输出这条边时需要重新展开成链。

若限制关系出现环或分叉则无解,并查集 + 链表维护。

时间复杂度是离散化的 \(\mathcal{O}(m\log m)\)代码

P3511 [POI2010] MOS-Bridges

若存在 \(d(i)\) 为偶数则无解。

二分答案转混合图欧拉回路判定,构建二分图后网络流即可。

时间复杂度 \(\mathcal{O}(m\sqrt m\log V)\)代码

CF1361C Johnny and Megan's Necklace

枚举权值 \(k\)。若答案不小于 \(k\),那么存在一组合法方案使得新添加的线连接的相邻两个珠子模 \(2 ^ k\) 的余数相同。将每个同余类抽象成一个点,原有的线相当于一条连接 \(a\bmod 2 ^ k\)\(b_i\bmod 2 ^ k\) 的边。在这张图上跑无向图欧拉回路即可。

时间复杂度 \(\mathcal{O}(n\log V)\)

*CF1458D Flip and Reverse

很棒的题目。

\(01\) 写成折线图,即 \(0\to -1\)\(1\to 1\) 做前缀和得到 \(v_i\)

考虑一次操作的本质:它相当于选取折线图上等高的两点,水平翻转。这不改变折线上每个线段连接的点的高度。

又因为如果 \(v_i\to v_{i + 1}\) 向上走,那么一定有向下回到高度 \(v_i\) 的边,可以感知到翻转的自由度很大。

因此,在 \(v_i\)\(v_{i + 1}\) 之间连无向边,求出 \(v_0\to v_n\) 的字典序最小的欧拉路径就是答案。

证明也很容易,根据上一段提到的性质,分成 “右侧有相同高度” 和 “右侧没有相同高度” 两种情况讨论即可。

时间复杂度 \(\mathcal{O}(n)\)

参考资料

第一章

第二章

第三章

posted @ 2025-07-14 16:39  牧云兔  阅读(1)  评论(0)    收藏  举报