图论 I
本篇笔记介绍了 OI 中基础的图论问题:最短路、最小生成树、双连通分量、欧拉回路。
定义与记号
涉及常见或可能用到的概念的定义。关于更多,见参考资料。
基本定义
- 图:一张图
由若干个点和连接这些点的边构成。点的集合称为 点集 ,边的集合称为 边集 ,记 。 - 阶:图
的点数 称为 阶,记作 。 - 无向图:若
没有方向,则 称为 无向图。无向图的边记作 , 之间无序。 - 有向图:若
有方向,则 称为 有向图。有向图的边记作 或 , 之间有序。无向边 可视为两条有向边 和 。 - 重边:端点和方向(有向图)相同的边称为 重边。
- 自环:连接相同点的边称为 自环。
相邻
- 相邻:在无向图中,称
相邻 当且仅当存在 。 - 邻域:在无向图中,点
的 邻域 为所有与之相邻的点的集合,记作 。 - 邻边:在无向图中,与
相连的边 称为 的 邻边。 - 出边 / 入边:在有向图中,从
出发的边 称为 的 出边,到达 的边 称为 的 入边。 - 度数:一个点的 度数 为与之关联的边的数量,记作
, 。点的自环对其度数产生 的贡献。 - 出度 / 入度:在有向图中,从
出发的边数称为 的 出度,记作 ;到达 的边数称为 的 入度,记作 。
路径
- 途径:连接一串相邻结点的序列称为 途径,用点序列
和边序列 描述,其中 。常写为 。 - 迹:不经过重复边的途径称为 迹。
- 回路:
的迹称为 回路。 - 路径:不经过重复点的迹称为 路径,也称 简单路径。不经过重复点比不经过重复边强,所以不经过重复点的途径也是路径。注意题目中的简单路径可能指迹。
- 环:除
外所有点互不相同的途径称为 环,也称 圈 或 简单环。
连通性
- 连通:对于无向图的两点
,若存在途径使得 且 ,则称 连通。 - 弱连通:对于有向图的两点
,若将有向边改为无向边后 连通,则称 弱连通。 - 连通图:任意两点连通的无向图称为 连通图。
- 弱连通图:任意两点弱连通的有向图称为 弱连通图。
- 可达:对于有向图的两点
,若存在途径使得 且 ,则称 可达 ,记作 。 - 关于点双连通 / 边双连通 / 强连通,见对应章节。
特殊图
- 简单图:不含重边和自环的图称为 简单图。
- 基图:将有向图的有向边替换为无向边得到的图称为该有向图的 基图。
- 有向无环图:不含环的有向图称为 有向无环图,简称 DAG(Directed Acyclic Graph)。
- 完全图:任意不同的两点之间恰有一条边的无向简单图称为 完全图。
阶完全图记作 。 - 树:不含环的无向连通图称为 树,树上度为
的点称为 叶子。树是简单图,满足 。若干棵(包括一棵)树组成的连通块称为 森林。相关知识点见 “树论”。 - 稀疏图 / 稠密图:
远小于 的图称为 稀疏图, 接近 的图称为 稠密图。用于讨论时间复杂度为 和 的算法。
子图
- 子图:满足
且 的图 称为 的 子图,记作 。要求 所有边的两端均在 中。 - 导出子图:选择若干个点以及两端都在该点集的所有边构成的子图称为该图的 导出子图。导出子图的形态仅由选择的点集
决定,记作 。 - 生成子图:
的子图称为 生成子图。 - 极大子图(分量):在子图满足某性质的前提下,子图
称为 极大 的,当且仅当不存在同样满足该性质的子图 且 。 称为满足该性质的 分量。例如,极大的连通的子图称为原图的连通分量,也就是我们熟知的连通块。
约定
- 记
表示点集大小 , 表示边集大小 。
基本知识
读者需要掌握的知识:宽度优先搜索(BFS),深度优先搜索(DFS)。
拓扑排序及其应用
对 DAG 进行拓扑排序,得到结点序列
每次取入度为
拓扑排序没有结束时,必然存在入度为
通过 BFS 或 DFS 实现拓扑排序:
- BFS:用队列维护所有入度为
的点。取出队首 ,删去从 出发的所有边 。如果删边导致某个点 从入度大于 变成入度等于 ,那么将 入队。初始将所有入度为 的点入队。 - DFS:对于当前点
,若 在删去 后从入度大于 变成入度等于 ,那么向 DFS。初始从每个入度为 的点开始搜索。
时间复杂度为
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 100 + 5;
int n, deg[N];
vector<int> e[N];
int main() {
cin >> n;
for(int i = 1; i <= n; i++) {
int x;
cin >> x;
while(x) e[i].push_back(x), deg[x]++, cin >> x;
}
queue<int> q;
for(int i = 1; i <= n; i++) if(!deg[i]) q.push(i);
while(!q.empty()) {
int t = q.front();
q.pop();
cout << t << " ";
for(int it : e[t]) if(!--deg[it]) q.push(it);
}
return 0;
}
拓扑序常用于解决建图题或图论类型的构造题。
拓扑序不唯一,例如图
拓扑排序衍生出的各种问题:
- 拓扑序 DP,即在拓扑序上通过 DP 的方式统计答案。
- DAG 最长路。设
表示以 结尾的最长路径长度,按拓扑序转移: 。例题:P1137,P3573(第一章例题),P3387(4.3 小节的模板题)。 - DAG 路径计数。设
表示以 结尾的路径条数,按拓扑序转移: 。例题:P3183,P4017。 - 点对可达性统计。设
表示 是否可达 ,按拓扑序转移: ,用 bitset 优化,时间复杂度 。例题:P7877(“建图相关” 例题),HDU7171(HDU 多校 2022 比赛记录 Round 3 1010)。
- DAG 最长路。设
- 最小 / 最大字典序拓扑序:在 BFS 求拓扑序时,将队列换成优先队列。可归纳证明每一步都取到了字典序最小值。例题:P3243,CF1765H(CF 合集 1751-1775)。
- 树拓扑序计数,拓扑序容斥:见 “计数 DP”。
- 结合有向图强连通分量缩点,缩点后拓扑排序,解决一般有向图上的问题。
无向图 DFS 树
给定无向连通图
给每个点标号为它被访问到的次序,称为 时间戳,简称 dfn。按 DFS 的访问顺序得到的结点序列称为 DFS 序,时间戳为
上图是一个可能的 DFS 树以及对应的时间戳。
无向图 DFS 树的性质:
- 祖先后代性:任意非树边两端具有祖先后代关系。
- 子树独立性:结点的每个儿子的子树之间没有边(和上一条性质等价)。
- 时间戳区间性:子树时间戳为一段区间。
- 时间戳单调性:结点的时间戳小于其子树内结点的时间戳。
正确性留给读者思考。
关于有向图 DFS 树,见第四章。
1. 最短路及其应用
最短路是图论的一类经典问题。最优化 DP 在有环的图上转移就是最短路。
1.1 相关定义
在研究最短路问题(Shortest Path Problem,SPP)时,研究对象是路径而非途径,因为总可以钦定两点之间的最短路不经过重复点,否则起点和终点之间存在负环,最短路不存在。
- 带权图:每条边带有权值的图称为 带权图(赋权图)。所有边的权值非负的图称为 非负权图;所有边的权值为正的图称为 正权图。
- 边权:边的权值称为 边权,记作
或 。带权边记作 。若边不带权,默认边权为 。 - 路径长度:路径上每条边的权值之和称为 路径长度。
- 负环:长度为负数的环称为 负环。
- 最短路:在一张图上,称
到 的 最短路 为最短的连接 到 的路径。若不存在这样的路径(不连通或不可达),或最小值不存在(存在可经过的负环),则最短路不存在。 - 记
表示最短路起点, 表示最短路终点。
1.2 单源最短路径问题
问题描述:给定 源点
设
接下来介绍该问题的几种常见解法。
1.2.1 Bellman-Ford
Bellman-Ford 是一种暴力求解单源最短路径的方法。
称一轮 松弛 表示对每条边
证明
在
的最短路 中,对于每个点 , 一定是 的最短路。这说明一个点的最短路由另一个点的最短路扩展而来。 因为最短路至多有
条边,而第 轮松弛会得到边数为 的最短路,故只需松弛 轮。
该算法可以判断一张图上是否存在负环:若第
算法的时间复杂度为
1.2.2 Dijkstra
Dijkstra 算法适用于 非负权图。网上的一些博客认为 Dijkstra 使用了贪心思想,笔者表示怀疑。
称 扩展 结点
在
如何判断一个点的
证明
归纳假设已经扩展过的结点
在扩展时取到了最短路。 为未扩展的 最小的结点。
的最短路一定由 ( )的最短路扩展而来,不可能出现 的情况,其中
均未扩展。否则由于边权非负, ,即当前 ,与 的最小性矛盾。 初始令源点的
为 ,假设成立,算法正确。
简单地说,一个点的最短路由长度非严格更小的最短路更新得到,
取出
注意,一个结点可能有多个入边并多次进入优先队列,但当它第一次被取出时,对应的
算法的时间复杂度为
扩展:当边权只有
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 1e5 + 5;
int n, m, s, dis[N];
vector<pii> e[N];
int main() {
cin >> n >> m >> s;
for(int i = 1; i <= m; i++) {
int u, v, w;
scanf("%d%d%d", &u, &v, &w);
e[u].push_back(make_pair(v, w));
}
memset(dis, 0x3f, sizeof(dis)), dis[s] = 0; // 初始化
priority_queue<pii, vector<pii>, greater<pii>> q; // 优先取权值较小的 pair
q.push(make_pair(0, s)); // 注意第一关键字要放到前面
while(!q.empty()) {
auto t = q.top();
q.pop();
int id = t.second; // 不要搞反了,编号是 second
if(t.first != dis[id]) continue; // 若权值不等于最短路,说明已扩展,跳过
for(auto _ : e[id]) {
int it = _.first, d = t.first + _.second;
if(d < dis[it]) q.push(make_pair(dis[it] = d, it));
}
}
for(int i = 1; i <= n; i++) cout << dis[i] << " ";
return 0;
}
1.2.3 SPFA 与负环
关于 SPFA,___。
SPFA(Shortest Path Faster Algorithm)是队列优化的 Bellman-Ford。
松弛点
时间复杂度相比 BF 并没有改进,仍为
注意:若使用 SPFA 求解点对点的最短路径,如费用流 EK,当队头为目标结点时不能结束算法。因为一个点进入队列并不代表其
SPFA 判负环:若一个点 进入队列 超过
一般判入队次数慢于最短路长度,推荐使用后者。
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e3 + 5;
int n, m, dis[N], len[N], vis[N];
vector<pair<int, int>> e[N];
void solve() {
cin >> n >> m;
for(int i = 1; i <= n; i++) e[i].clear();
for(int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[u].push_back(make_pair(v, w));
if(w >= 0) e[v].push_back({u, w});
}
queue<int> q;
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
q.push(1), len[1] = dis[1] = 0;
while(!q.empty()) {
int t = q.front();
q.pop(), vis[t] = 0;
for(auto it : e[t]) {
int d = dis[t] + it.second, to = it.first;
if(d < dis[to]) {
dis[to] = d, len[to] = len[t] + 1; // 更新时修改最短路长度
if(len[to] == n) return puts("YES"), void();
if(!vis[to]) vis[to] = 1, q.push(to);
}
}
}
puts("NO");
}
int main() {
int T;
cin >> T;
while(T--) solve();
return 0;
}
1.2.4 三角形不等式
在单源最短路径问题中,对于每条边
三角形不等式是每个点的单源最短路径长度满足的性质。相反,给出若干条三角形不等式的限制,求能否找到一组满足条件的解,就是经典的 差分约束 问题。
1.3 差分约束问题
问题描述:给定若干形如
1.3.1 算法介绍
只要
通过单源最短路径求出的
从
因为
时间复杂度
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e3 + 5;
int n, m, vis[N], dis[N], len[N];
vector<pair<int, int>> e[N];
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[v].push_back(make_pair(u, w));
}
queue<int> q;
for(int i = 1; i <= n; i++) q.push(i), vis[i] = 1;
while(!q.empty()) {
int t = q.front();
q.pop(), vis[t] = 0;
for(auto it : e[t]) {
int to = it.first, d = dis[t] + it.second;
if(d < dis[to]) {
dis[to] = d, len[to] = len[t] + 1;
if(len[to] == n) puts("NO"), exit(0);
if(!vis[to]) q.push(to), vis[to] = 1;
}
}
}
for(int i = 1; i <= n; i++) cout << dis[i] << " ";
return 0;
}
例题:P4926,P5590,P3530。
1.3.2 解的字典序极值
一般而言差分约束系统的解没有 “字典序极值” 这个概念,因为我们只对变量之间的差值进行约束,而变量本身的值随着某个变量取值的固定而固定。
字典序的极值建立于变量有界的基础上。不妨设希望求出当限制
求解差分约束系统时我们使用了三角形不等式,将其转化为最短路问题。这给予它很好的性质:通过 SPFA 求得的解恰为字典序最大解。
对于一条
考虑
对于字典序最小解,限制
1.4 全源最短路径问题
问题描述:求任意两点之间的最短路
1.4.1 Johnson
前置知识:Bellman-Ford,Dijkstra。
Johnson 算法用于解决 带有负权边 的全源最短路径问题。
Johnson 算法的巧妙之处在于为每个点赋予 势能
考虑路径
新长度为
则
对于固定的
由于负权,我们无法使用 Dijkstra 求解最短路。多次 Bellman-Ford 或 SPFA 的时间复杂度不优秀。尝试应用上述分析,合理地为每个点赋权值,从而消去图上的所有负权边。
为使
操作结束后,更新每条边的权值
最后不要忘了将
算法的时间复杂度为
模板题 代码。
#include <bits/stdc++.h>
using namespace std;
#define pii pair<int, int>
const int N = 3e3 + 5;
int n, m, h[N], dis[N];
vector<pii> e[N];
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
e[u].push_back(make_pair(v, w));
}
for(int i = 1; i <= n; i++) {
bool found = 0;
for(int j = 1; j <= n; j++)
for(auto it : e[j])
if(h[j] + it.second < h[it.first])
found = 1, h[it.first] = h[j] + it.second;
if(i == n && found) puts("-1"), exit(0);
}
for(int i = 1; i <= n; i++) {
long long ans = 0;
for(int j = 1; j <= n; j++) dis[j] = i == j ? 0 : 1e9;
priority_queue<pii, vector <pii>, greater <pii>> q;
q.push({0, i});
while(!q.empty()) {
auto t = q.top(); q.pop();
int id = t.second;
if(t.first != dis[id]) continue;
for(auto _ : e[id]) {
int it = _.first, d = t.first + h[id] - h[it] + _.second;
if(d < dis[it]) q.push({dis[it] = d, it});
}
}
for(int j = 1; j <= n; j++) ans += 1ll * j * (dis[j] + (dis[j] < 1e9 ? h[j] - h[i] : 0));
cout << ans << endl;
}
return 0;
}
1.4.2 Floyd 与传递闭包
Floyd 也可以解决带负权边的图的全源最短路径问题。
设
省去第一维导致的重复更新不影响答案,因为最短路不经过重复点。简化后的算法描述为:初始化所有
很明显,按任意顺序枚举中转点,甚至枚举相同的中转点,只要每个点至少一次作为中转点,依然能得到正确的答案。
算法的时间复杂度为
此外,Floyd 可以求传递闭包。有向图
bitset
优化求传递闭包的复杂度至 。- 对于稀疏有向图,缩点后拓扑排序可做到
求传递闭包。
1.5 扩展问题
默认图(弱)连通且无负环。
1.5.1 最短路树
从
在单源最短路径过程中,记录每个点最后一次被更新最短路的对应前驱
这样,得到一棵有根叶向树,称为 从
一张图的最短路树不唯一,如下图所示(无向图)。
最短路树以外的所有边不影响
类似地,将图上所有边翻转方向后求得的以
最短路树在求解单源最短路径问题的变形时发挥了很大的作用,如接下来介绍的删边最短路。
扩展:在 不含零环的图上 将
例题:P6880。
*1.5.2 删边最短路
问题描述:给定一张 无向正权图,对图上的每条边,求删去该边后
首先求出
若
若
证明
不妨设
在 上的顺序为 。 设
和 分别表示 上 的子树和 的子树以外的部分。设 和 分别表示 上 的子树和 的子树以外的部分。 引理
。
如上图,绿色部分的
和蓝色部分的 无交。 证明
假设存在
。因为 ,所以 的最短路形如 (在 上)。因为 ,所以 的最短路形如 (在 上)。 根据最短路的性质,有
以及 。注意这里将最短路树上的路径进行了翻转,这用到了 原图为无向图 的性质( )。对于有向图,不等式右侧不一定存在。 将左侧路径拆开,右侧路径翻转,得
以及 。 两式相加,得
,与正权图矛盾。
简单地说,根据
可以推出 和 ,得 。如上图。 考虑删去
后任意一条 的最短路 。因为 , ,所以存在 使得 , 。不妨设 是路径上最后一条这样的边。 将
上 的部分替换成 一定不劣,因为后者是 的最短路。又因为 ,所以这部分一定不会包含 。 将
上 的部分替换成 一定不劣,因为后者是 的最短路。又因为 ,根据引理, ,即 ,所以这部分同样不会包含 。 这样,对于删去
后的任意最短路,总存在调整它的方案使得恰好只经过 一条非树边。而枚举每条非树边计算最短路长度并更新的过程考虑到了所有恰好只经过一条非树边的路径,相对应的也就考虑到了所有最短路。算法正确性得证。
尽管如此,对一条边求答案仍需要
枚举
线段树维护区间取 multiset
离线扫描线,时间复杂度
扩展:一开始我们要求
扩展:对于存在零权边的非负权图,导出矛盾的
例题:P1186,P2685,P3573,CF1163F。有向图删边最短路 P3238 是假题,不可做。
1.5.3 平面图最小割
前置知识:割的定义。
- 平面图:能画在平面上,满足除顶点处以外无边相交的图称为 平面图。
- 网格图:形成一个网格的平面图称为 网格图。形式化地,对于点数为
的网格图,存在一种为每个点赋互不相同的标号 ( , )的方案,满足两点之间有边当且仅当它们的 相同且 相差 ,或 相同且 相差 。
一般平面图最小割问题都在网格图上,所以我们先介绍网格图最小割,满足 边权非负 且 割的源点和汇点在边界 上。称一个点在边界上,当且仅当它的
如上图,左边是一张平面图,右边是一张网格图,边界上所有点用黄色标注。
考虑求网格图左上角
引理
总存在权值最小的割满足
的所有点连通, 的所有点连通。 证明
假设
的点不连通,那么存在不包含 的连通块 。将 并入 ,原来的非割边不会变成割边,而一些割边变成非割边,所以割的权值不会变大。
考虑
考虑从左上角和右下角向外引出两条权值无穷大的射线,将网格图外的平面分成左下和右上两部分。现在我们要建出网格图的 对偶图:将所有面抽象成点,对于通过一条边相邻的两个面,在它们对应的点之间连权值为这条边的权值的边(这不是对偶图的严谨定义)。
一个满足
实边表示对偶图的边,虚边表示原图的边。注意左侧和下侧,上侧和右侧分别是同一个点。从右上到左下的最短路即为左上到右下的最小割。上图给出了一条可能的最短路以及它对应的最小割。
扩展:不需要源汇在左上角和右下角。只要源汇在边界上,就可以通过转对偶图的方式求最小割。
扩展:不需要是网格图。平面图也可以转对偶图求最小割,整个过程没有用到网格图的性质,只是便于理解。
例题:P4001,P7916。
*1.5.4 短路
前置知识:可持久化线段树或其它可持久化结构。
问题描述:给定一张 有向非负权 图,求
一个解决问题的有效思路是:找一种好的方式刻画研究对象,这里 “好的” 指便于考虑和解题。
考虑一条
进一步地,发现一个 有序 的非树边序列
这很优秀,因为直觉告诉我们
还有一个问题:在研究
注意到
这类 “不断扩展结构,取出权值前
证明
假设第
次取出队列的结构 不是权值第 小的结构,且 是所有这样的操作中编号最小的。考虑第 小的结构 ,其前驱 一定被考虑过: 的排名小于 ,而根据假设,前 次操作均取出了正确的结构,所以 被正确取出了。所以 一定在优先队列中且 小于 。这和当前取出 矛盾了。
只要每个点的后继数量不大,那么时间复杂度为
回归原问题,一个设置前驱的方案为:令
解决方法也是经典的:前驱不一定要减少元素个数,也可以减小最后一个元素的权值。对此,加入限制:设
这样,一个序列至多有两个后继:
- 如果
不是从 出发的 最大的边,那么将其删去,改成后继。 - 加入从
在 上任意祖先出发的 最小的边。
容易证明这是我们钦定的前驱关系对应的后继关系。用可持久化权值线段树维护最短路树上从一个点的所有祖先出发的所有边,查后继和权值最小的边均可线段树二分。
综上,时间和空间复杂度是关于
注意
扩展:存在负权边会使得一开始求最短路的复杂度变成
扩展:对于
例题:P2483。
1.5.5 同余最短路
同余最短路算法可以求出在给定范围内有多少重量可由若干物品做完全背包得到,即一段区间内有多少数可由给定正整数进行 系数非负 的线性组合得到。
问题描述:给定
其核心在于观察到,如果一个数
一种理解方式:考虑每个数能否被表出,设为
为减小常数,一般选择使得同余类个数最少的
上述过程非常像一个最短路:对于每个点
求出
用
时间复杂度为 Dijkstra 的
存在
例题:ARC084B,P4156(见 “字符串进阶 I”)。
1.6 例题
P1462 通往奥格瑞玛的道路
最小化最大值,考虑二分答案并使用 Dijkstra 检查
时间复杂度
P4568 [JLOI2011] 飞行路线
注意到
时间复杂度
P6880 [JOI 2020 Final] 奥运公交
不翻转的情况是平凡的。考虑翻转,将贡献拆为
对于
- 若边
不在最短路树上,因为最短路要么经过 ,要么不经过。对于前者,即 的最短路,加上 ,再加上 的最短路。对于后者,最短路即原最短路。两种情况都可以预处理后 计算。 - 如果边
在最短路树上,可以重新跑最短路,因为总共只有 条最短路树上的边。稠密图,用 Dijkstra 跑最短路。
对于
时间复杂度
P4001 [ICPC-Beijing 2006] 狼抓兔子
网格图最小割。建出网格图的对偶图,每条边与其对偶边权值相等。从右上到左下的最短路即为所求。
代码。
CF1765I Infinite Chess
直接 BFS,复杂度高达
注意到行数很少,这样斜向攻击只会影响到半径为
离散化后模拟得到每个格子是否被攻击,跑最短路即可。时间复杂度
P4926 [1007] 倍杀测量者
化乘法为加法不难想到取对数。
考虑对没有女装的限制建出差分约束图,若出现负环则一定有人女装。对于固定分数的人
答案显然满足可二分性。
时间复杂度
*P5304 [GXOI/GZOI2019] 旅行者
随机分组也可以,做
考虑任意最优路径
代码。
*P5590 赛车游戏
好题。
转换思路,与其设置边权使路径长度相等,不如设置路径长度去拟合边权的限制。
设
时间复杂度
*ABC232G Modulo Shortest Path
将所有点按照
考虑朴素 Dijkstra 的过程,发现操作本质上是:区间
时间复杂度
本题的官方解法非常精妙:取模操作可以想象成在环上走。考虑建立虚点
P3573 [POI2014] RAJ-Rally
本题可以借助删边最短路的思想,枚举除了最长链以外的所有点,用线段树维护它对最长链上一段区间的贡献。因为保证有向图无环,所以可行。需要添加虚点保证弱连通。
另外一种思路:删去点 multiset
维护。
时间复杂度
*P3530 [POI2012] FES-Festival
一道加深对差分约束理解的好题。
首先建出差分约束图。跑一遍负环判无解。
但如何满足不同的
首先强连通分量缩点(见第四章强连通分量),不同 SCC 之间独立,因为可以将两个 SCC 之间的距离任意拉远。
考虑 SCC 内部的贡献。不妨设
考虑最短路的最大长度
因为图是稠密图,所以使用 Floyd 求解任意两点之间的最短路。答案即每个 SCC 的答案之和。
时间复杂度
*P7916 [CSP-S 2021] 交通规划
显然,
关键性质是任意两点的匹配不会相交,即不会出现
因此,首先求出所有
时间复杂度
*ARC084B Small Multiple
好题。
所有正整数都可以从
注意不可以将
时间复杂度
*AGC056C 01 Balanced
将
对于限制
对于相邻的两个位置
我们惊讶地发现,在保证字典序最大的前提下,不可能出现
这题的思想很高妙,虽然解法是差分约束但是形式新颖,需要猜性质(不要被固有思维限制)才能做出本题。
边权只有
若为了避免出现
*P7516 [省选联考 2021 A/B 卷] 图函数
设一条边的边权为它的编号。
题面描述花里胡哨,我们先推些性质。
- 将
的值摊到每个点对 上。 - 点对
最多只会贡献一次,因为计算 时,枚举 时 已经被删去了,所以只可能在计算 且枚举 时产生贡献。 - 模拟图函数的计算过程,点对
产生贡献当且仅当存在 的路径 和 的路径 使得路径上所有点的编号不小于 。 - 设
表示所有这样的路径 和 中,边权最小值的最大值。注意是 和 的边权最小值而不是 或 。若 则 。则 会对所有 产生贡献。
至此,存在
这个做法严重卡常,需要加一些优化:若
考虑对每个
类似 ARC092F,考虑 bitset 优化。
肯定不能先枚举
此外,维护图的邻接矩阵
用 _Find_first
和 _Find_next
找 bitset 中第一个
*CF1163F Indecisive Taxi Fee
修改一条边的权值相当于删去这条边的最短路和强制经过这条边的最短路的较小值。前者是删边最短路,后者也是容易的:
- 对于
, 。 - 对于
, 。
时间复杂度
*P7515 [省选联考 2021 A 卷] 矩阵游戏
神题。
既然如此,本题的突破口一定在
考虑
考虑
观察到
但是对于
这样一个约束有三个元素,无法处理。
但是根据之前的经验,将
这样,得到如下思路:设
题目卡常,注意常数。SPFA 用最短路长度判无解会跑得快一些。
时间复杂度
思考:
P2483 【模板】k 短路 / [SDOI2010] 魔法猪学院
代码。
2. 无向图最小生成树
本章讨论的图为 无向连通图。
关于有向图最小生成树(最小树形图),见 “图论 II”。
2.1 相关定义
- 生成树:对于连通图,形态是一棵树的生成子图称为 生成树。
通俗地说,生成树就是连通了图上所有点的树。非连通图不存在生成树。
- 生成森林:由每个连通分量的生成树组成的子图称为 生成森林。
- 非树边:对于某棵生成树,原图的不在生成树上的边称为 非树边。
生成树算法的核心思想:往生成树加入一条边形成环,考察环的性质。
给定一张带权连通图,求其边权和最小的生成树,称为 最小生成树(Minimum Spanning Tree,MST)。对于非连通图,对每个连通分量求最小生成树即得最小生成森林。
注意:连通图有最小生成森林,等于它的最小生成树。非连通图没有最小生成树。对任意图都可以求最小生成森林,但只有连通图才能求最小生成树。
2.2 最小生成树问题
介绍三种求最小生成树的算法。
2.2.1 Kruskal
考虑最终求得的生成树
考虑一条非树边
这说明
这启发我们将边按照边权从小到大排序,依次枚举每一条边,若当前边两端不连通则往生成树中加入该边。用并查集维护连通性,时间复杂度
若存在非树边
不满足上述性质,则考虑被替换的边 其中 。因为考虑到 时 不连通,所以考虑到 时 不连通,与 矛盾。 借助该工具,可以归纳证明加入的每一条边均在原图的最小生成树中。
并查集合并时新建虚点维护合并关系,得到 Kruskal 重构树。它刻画了在仅考虑权值不超过某个阈值的边时整张图的连通情况。见 “树论”。
2.2.2 Prim
维护当前点集
另一种理解方式:维护每个点的权值
“取出权值最小的点” 的过程类似 Dijkstra 算法借助优先队列实现。算法的时间复杂度为
Prim 算法的正确性证明和 Kruskal 差不多。
2.2.3 Boruvka
对于每个点
对于每个点,求出边权最小的邻边。将这些边删去后加入最小生成树。除去至多
注意在选择边权最小的边时,不能选择两端已经在同一连通块的边:
- 对每个点,选择一端为该点,另一端和它不在同一连通块的权值最小的边。
- 对每个连通块,选择一端在该连通块,另一端不在该连通块的权值最小的边。
两种选边方式均正确,视情况使用更方便的一种。对于前者,总边数为
算法的时间复杂度为
Boruvka 在解决一类 MST 问题上非常有用:给定
*2.3 拟阵和生成树
注:不保证本小节内容的严谨性。可跳过 2.3.1 和 2.3.2 直接阅读 2.3.3 小节的结论。
线性代数是研究图论问题的常见方法。当生成树和线性代数相结合,又会碰撞出怎样的火花呢?本小节简要介绍了拟阵,这个从线性代数中抽象出的概念,在生成树上的应用。
2.3.1 拟阵的定义与性质
拟阵的定义:记
- 遗传性:独立集的所有子集也是独立集。若
, 则 。一般认为 。 - 交换性:对于两个大小不同的独立集,存在仅属于较大的独立集的元素,使得较小的独立集加入该元素后仍为独立集。对于
,若 ,那么存在元素 满足 。
拟阵是对线性代数中 “线性无关” 的关系的抽象:对于向量集合
拟阵和生成树的关系:令
- 遗传性:一个边集无环,则该边集的子集显然无环。
- 交换性:设两个独立集
,那么 形成的连通块数量大于 。因此,总存在 形成的连通块,使得它在 上不连通。因此存在 的一条边在加入 时不会产生环。
由此构造的拟阵
给出拟阵相关的若干重要定义:
- 基:对于独立集
,若加入任何 的元素都会变成非独立集,则称 是拟阵的一个 基,也称 极大独立集。 - 环:对于非独立集
,若删去其任何元素都会变成独立集,则称 是拟阵的一个 环,也称 极小非独立集。
定理 1
基的大小相同。
证明
若存在两个大小不同的基
,根据交换性,存在 使得 ,与基的定义矛盾。
定理 2(基交换定理)
设两个不同的基
,对于任意 ,存在 满足 为 的基。 证明
因为
且 ,根据交换性,存在 使得 。
任意大小相同的不同独立集
图拟阵的每个基对应了
- 秩:基的大小称为拟阵的 秩,对于任意
的子集 ,定义 秩函数 表示 的极大独立集(基)大小,即 。
可知
秩函数
- 有界性:
。 - 单调性:
。
2.3.2 拟阵上的最优化问题
基交换定理很有用,它告诉我们拟阵的两个基可以通过替换元素相互得到。
通过上一小节的铺垫,我们可以解决拟阵上的最优化问题:给出定义在
如果
不妨设
为方便讨论,以下设元素权值均为 正整数。
拟阵的优秀性质启发我们尝试直接贪心:将
证明
设
表示考虑 之后 的形态, 表示最优解包含的下标不大于 的元素。设贪心结果 和最优解 选择的元素下标分别为 和 。 若存在
,那么 只包含下标不大于 的元素( 只包含下标不大于 的元素)。进行 的单步替换,存在 使得 。则 且 。而 ,根据遗传性, ,这与 “能加入就加入” 的贪心算法矛盾。 故
,即 ,所以 , 一定是最优解。
扩展:如果要求权值最小,只需将元素按权值不降排序再贪心。
扩展:如果要求权值最大的 基,不要删去任何元素,直接做就行了。因为任意时刻
证明
设
。 假设考虑到
时, 不是 的基且 最小。因为 不会变小且 至多增加 ,故只可能是 且 。 设
为 的基,则 。根据交换性,存在 可以加入 。而 无法加入 ,所以 。因此 , 不是 的基,与 最小的假设矛盾。
从以上两条证明中可以总结出一个小套路:证明拟阵的性质,一般方法是反证 + 交换性和基交换定理。这有助于加深对拟阵的感性认知。
因为一张图的所有生成树也是拟阵,所以拟阵上的最优化问题的解法可以直接解决最小生成树问题。直接套用贪心过程,就得到了 Kruskal 算法。
2.3.3 最小生成树的性质
拟阵是研究最小生成树的强力工具。拟阵的贪心过程可以求得最小权基,但最小权基并不一定唯一。若要刻画所有最小权基的形态,研究它们共同具有的性质,就需要对贪心过程进行更深层次的探索。
一些问题需要将最小生成树的边权不超过某个值的边单独拎出来。请读者思考:在任意最小生成树中,所有权值不大于
如果你觉得这个问题比较困难,可以先思考特殊情况:所有元素的权值互不相同。
结论
若所有元素的权值互不相同,则最小权基唯一。
证明
假设存在两个最小权基
。按权值从小到大排序后,设 和 选择的元素下标分别为 和 。 因为
,所以存在 使得 。找到最大的 ,不妨设 ,那么 包含的元素下标不大于 。进行 的单步替换,存在 使得 。因为元素权值互不相同,所以 ,故 ,与 是最小权基矛盾。
借助相同的思想,可以证明如下性质:
结论
对于权值
,设权值不大于 的元素集合为 ,则对于任意最小权基 , 包含的权值不大于 的元素个数为 。 证明
设两个最小权基
。对它们不断应用最小权唯一时的替换方法(可能是 的单步替换,也可能是 的单步替换,取决于 和 的大小关系),则 均不增加。又因为 是最小权基,所以 不减少。所以每次替换时删去和加入的元素权值相同。这说明任意两个最小权基在相互替换直到相等的过程中,权值等于某个值的元素数量不变,所以任意最小权基的权值等于某个值的元素个数相等。 再根据贪心过程得到的最小权基包含的权值不大于
的元素个数为 ,得证。 推论
对于每个权值
,任意最小权基含有的权值不大于 和等于 的元素个数为定值。 推论
对于每个权值
,任意最小生成树含有的权值不大于 和等于 的边的个数为定值。
在图拟阵中,集合
可以感受到,在最小生成树中,每个权值的边相对独立。求最小生成树的过程可以看做:从小到大枚举所有权值。将已经加入的边的两端缩成一个点,求当前权值的边的任意一棵生成森林,并将这些边加入最小生成树。所有最小生成树都可以通过该过程生成,且每层 “当前权值的边” 的生成森林的多样性导致了最小生成树的多样性。
重要事实:将最小生成树
实际上,将权值大于
此外,点对
2.4 扩展问题
除了以下提到的扩展问题,还有使用 LCT 维护生成树的经典手段:在生成树上加入一条边,先求出这条边两端的路径上的权值最大的边,将最大边从生成树中删去,再加入当前边。
2.4.1 次小生成树
注:可以不看证明只看结论。
问题描述:求次小生成树的权值。次小生成树即权值第二小的生成树。
感性理解,如果次小生成树和最小生成树相差很多边,可以用基交换定理调整成只相差一条边。而只相差一条边是好做的:枚举每条非树边,求出其两端对应路径上权值最大的边,得到替换后生成树的最小权值。所有替换方案的最小权值对应的生成树就是次小生成树。
倍增求路径最大边权,时间复杂度
结论
对于任意最小生成树
,若存在次小生成树 ,则存在次小生成树 使得 和 只相差一条边。 证明
设
和 包含的边的编号为 和 ,其中边按权值不降排序。存在最大的 使得 。 如果
,进行 的单步替换,得到权值更小的生成树,矛盾。 否则
,进行 的单步替换,得到权值不大于 ,且与 相差边数减少 的生成树。 因此,如果
与 相差大于一条边,总可以调整使得权值不增,且恰与 相差一条边。
这个结论对严格次小生成树仍然适用。严格次小生成树即权值严格大于
结论
对于任意最小生成树
,若存在严格次小生成树 ,则存在严格次小生成树 使得 和 只相差一条边。 证明
次小生成树的证明不再适用,因为调整过程中可能出现
。 欲证结论,只需证明对任意非最小生成树
,可以从 不断调整,每次调整不增加权值,得到与 相差一条边的生成树。结论的证明较复杂,但这是笔者能想到的最简单的证明。如果有更简洁的证明欢迎交流。 引理(强基交换定理)
设两个不同的基
,对于任意 ,存在 ,使得 和 均为 的基。 证明较复杂,略去。
设
和 包含的权值为 的边集分别为 和 。不断应用次小生成树的证明,直到某一步 使得 。在这一步之前停下。因为最小生成树的每个权值的边数固定,所以:
- 存在
满足 , 。 - 对于任意
,有 。 - 对于
,有 (否则不会选中 )。 考虑
:对 用强基交换定理,存在 使得 。因为 ( ),所以 。因为 是最小生成树,所以 。因此 ,即 。根据强基交换定理,令 变成 。不断进行该操作直到 为空,此时 等于 加上一个元素。 从
到 降序枚举 :归纳假设对于所有 ,恰存在一个 满足 等于 加上一个元素,设为 ,且对于其它 有 。类似 的情况,对 使用强基交换定理直到 为空。过程中可能删去了 多出来的元素,此时 等于 加上一个元素,新的 变成当前的 ;也可能没有删掉,此时 。归纳假设仍然成立。 经过上述调整,有:
- 存在
使得 , ,且 为 加上一个元素(即 )。 - 对于
,有 。 - 对于
和 ,有 。 此时
不存在权值大于 的元素。 从
到 降序枚举 :归纳假设 不存在权值大于 的元素,且对于 ,有 。对 进行 的单步替换。假设选择 。因为 不存在权值大于 的元素,所以 。而如果 ,则根据遗传性,替换后 的所有权值不大于 的元素形成独立集,且元素个数为 的权值不大于 的元素的个数加 。这和 “ 的所有权值不大于 的元素为 (拟阵的边集)的所有权值不大于 的元素的基” 矛盾。因此 。不断进行操作直到 为空,此时 ( )或 为 加上一个元素( )。归纳假设仍然成立。 最终我们得到:
存在
使得 为 加上一个元素, 为 加上一个元素。 对于任意
, 。 这说明
和 之间只相差一条边,且 。而显然地,调整的每一步 不增。
如果从 “最小生成树的权值不大于
可以类似推导出任意拟阵存在次小权基和严格次小权基(若存在)与最小权基只有一个元素不同。实际上我们已经证明了这一点:上述两条结论的证明用到的所有关于最小生成树的结论都是由拟阵本身得出的,故适用于所有拟阵。将证明中的 “最小生成树” 改成 “独立集” 即可。
例题:P4180。
2.4.2 最小生成树计数
问题描述:求一张图的最小生成树的数量。
前置知识:生成树计数(矩阵树定理)。
根据 2.3.3 小节提到的性质,从小到大枚举边权
求生成森林个数有些麻烦。2.3.3 小节最后提到,可以先求出任意最小生成树,将生成树上权值不等于
注:最小生成树计数和最小生成森林计数的不同点在于,对于非连通图,前者的答案为
复杂度分析:设最小生成树有
例题:P4208。
*2.4.3 小生成树
和
问题描述:求无向带权简单图前
首先求出最小生成树。
已知次小生成树可由最小生成树加入一条非树边并删去一条树边得到。枚举非树边
对最小生成树求出权值增加量最小的非树边,则有两种选择:强制选择这条边,和强制不选择这条边。对于前者,给出了一棵新的生成树;对于后者,没有给出新的生成树,但边的状态改变了。
如果强制不选择这条边,那么接下来就是决策权值增加量次小的边是否强制选择,依次类推。于是最小生成树给出了若干棵新的生成树,其中第
不断从优先队列取出权值最小的生成树
为什么这样做是对的:钦定权值增加量最小的非树边是否选择,本质是将当前生成树可扩展到的生成树集合分裂为了两半,一半含这条非树边,另一半不含这条非树边。每棵可扩展到的生成树恰属于其中一半。对于含非树边的,替换后的生成树一定是这些生成树的权值最小值,因为替换后的生成树是相对于当前生成树而言的次小生成树,即不存在可扩展到的生成树的权值小于替换后的生成树(否则和替换后的生成树为次小生成树矛盾了)。对于不含非树边的,对应集合的最小值依然是当前生成树,但一条非树边的状态从可加入变成了不可加入。
如果将每棵生成树抽象为一个点,用一棵有根树描述整个扩展过程(根节点是最小生成树),那么每个非叶子结点有两个儿子,一个是实点,从当前点向下走到实点表示用一条非树边替换了一条树边得到新的生成树;另一个则是虚点,从当前点向下走到虚点表示当前生成树的一条非树边从可加入变成了不可加入。由于实点一定是当前生成树能扩展到的最小生成树,所以整个过程是不重不漏的。
支持对一个 “点” 求权值增加量最小的非树边:直接求树链最值需要倍增,但我们只需求
优先队列中不需要存储生成树的具体形态,只需记录每棵生成树由哪棵生成树如何替换一条边扩展得到,以及权值。等到真正取出这棵生成树之后再对其 DFS 预处理相关信息(以支持求权值增加量最小的非树边)。
此外,为了避免从优先队列中不断取出虚点导致求了
时间复杂度
*2.4.4 最小度限制生成树
问题描述:给定
该问题有
先求出不含点
证明
设
为非树边,根据最小生成树的基本性质,存在连接 的树边构成的路径 ,且路径上每条边的权值不大于 。 假设
在最小 度生成树上,断开 , 不连通。因此 上存在一条边使得其两侧不连通,加入该树边即可。
设
最小生成树可以贪心,那么最小
考虑删去树边
结论
对于初始最小生成树
,考虑使得 最小的边 。对于任意 ,存在最小 度生成树 删去了 。 证明
设
分别为删去 后两个连通块 的最小权点,其中有一个是 的最小权点,设为 ,则 在任何连通块内都是最小权点,所以 一定被选中。 根据
的最小性, 一定是 的连接 的路径 ( )上的最大权边。 假设
并从 中删去点 ,设包含 的连通块为 。
若
被选中:因为 均被选中,所以它们在 上不连通。设 和 的交集为路径 。因为 ,所以 非空,设为 ( )。
若
被选中的点在 左侧(含有 一侧的子树),则 (因为 也被选中),即 。加入边 ,删去 ,考虑权值变化:
- 因为
是 的最大权边,所以 。 - 因为
被选中的点不变,其它连通块没有缩小,所以被选中的点的权值之和不增。 这说明调整后权值不增。
若
被选中的点在 右侧(含有 一侧的子树),则 ,即 。加入边 ,删去 ,类似可证权值不增。 若
未被选中:因为 是 右侧的最小权点,所以 所在连通块一定包含 左侧的点,因此 。设任意一条与 所在连通块 相连的被删去的边为 (因为 ,所以 存在),它连接的另一个连通块 的最小权点为 。在原树 中删去 后,另一个连通块 包含 ,所以 的最小点权不大于 ,因此 。 加入边
,对权值的影响为 。然后删去 。因为 的最小权点在 左边,所以 分裂出的两个连通块 ,一个的最小权点为 最小权点,另一个包含 ,最小点权不大于 ,对权值的影响不大于 。而 ,因此权值不增。 综上,总可以调整使得
权值不增且 。
因此,在求最小
设
结论
对于
和任意 ,存在 为 删去一条边,且 关于 在 上下凸。 证明
当
或 时显然成立。 若
,根据上述结论,设使得删去后新的权值最小的边为 (不一定是权值最小的边),可钦定 一定删去 。 设删去
后得到的两棵树为 和 ,归纳假设 和 满足结论。 因为
和 的贡献独立,所以 等于 的最小值,其中 , ,且 。这是下凸函数的 卷积,根据经典理论(闵可夫斯基和), 也是下凸的。而 和 的每个差分值都对应 “删去一条边产生的贡献”,所以 的每个差分值都对应 “从 或 中删去一条边产生的贡献”。 另一种理解方式:结论实际上说明了在
的过程中,每次删去一条使得新的权值最小的边,且权值变化量(每条边的代价)随着删边而不降(下凸即二阶导非负, )。现在 分裂成 和 ,因为 和 独立,所以删去使得新的权值最小的边相当于初始令 ,每次选择 和 较大的那个差分值,令其为 ,然后将 和被选中的 加上 。 此时只需证明
。 可以证明在子图上删去一条边的代价不小于在原图上删去这条边的代价:设在原图删去
之后,不含 的连通块为 ,新增点权为 的最小点权 。在子图删去 之后,一定有一侧连通块 包含于 。新增点权为两侧连通块最小点权的较大值,不小于 的最小点权 。而 ,所以 。 结合
的最小性,得证。
上述结论告诉我们,随着
至此,存在
若删去
设删去
注意到对于
设
- 如果
,则 路径上的最大权边为 ,得 ,推出 。 - 如果
,则 ( 是 的最大边权),根据 的最小性得 。新的 有可能变为 ,但无论何种情况点权均不大于 。
因此
在 Kruskal 的过程中维护连通块的最小点权。用
最后处理遗留的细节:之前钦定了
算法在
2.5 例题
P1967 [NOIP2013 提高组] 货车运输
求出最大生成树,那么使得
设
P4180 [BJWC2010] 严格次小生成树
因为总可以调整使得严格次小生成树和最小生成树之间只差一条边,所以先求出任意最小生成树,然后枚举非树边,加入该边之后替换掉环上比该非树边权值小的权值最大的边,为原树一条路径上权值最大或严格第二大的边。
倍增即可,时间复杂度
P4208 [JSOI2008] 最小生成树计数
最小生成树计数的板子题。因为相同权值的边不超过
模数不为质数,需要使用辗转相除式的行列式求值。
时间复杂度
*CF888G Xor-MST
根据异或不难想到对
对于 01 Trie 上某个状态
考虑计算答案。若
因为一个结点最多被枚举
*CF1305G Kuroni and Antihype
这题就很厉害了。
先假设存在一个邀请方案,分析它的一般性质。
将邀请的操作视为有根树森林,每个点由它的父亲邀请而来,则总贡献为每个点的权值乘以它的儿子数量。对于除了根以外的节点,它的儿子个数为它的度数
将总贡献加上每个点的权值,这是一个定值,则我们希望最大化每个点的权值乘以度数。度数启发我们将每个点的贡献摊到与它相邻的边上,每条边的贡献即它的两端的权值之和。对原图求最大生成树(注意包含点
考虑 Kurskal,从小到大枚举边权
考虑 Boruvka,我们需要找到一个连通块向外的最小边权。考虑高维前缀和,对每个 mask 维护权值是它的子集的最大点权以及对应点编号,但这样在查询一个点权值补集的信息时,点权最大的点可能和该点在同一连通块。因此,对每个 mask,我们还要维护权值是它子集且不与权值最大点在同一连通块的次大点。写起来细节较多,但时间复杂度更优,为
3. 无向图连通性:双连通分量
无向图连通性中的双连通分量和有向图可达性中的强连通分量是图论的重要部分,分两章节介绍。
本章研究双连通分量相关知识点,研究对象为无向连通图。默认图为 无向连通图。
3.1 相关定义
无向图连通性,主要研究割点和割边。
- 割点:在无向图中,删去后使得连通分量数增加的点称为 割点。
- 割边:在无向图中,删去后使得连通分量数增加的边称为 割边,也称 桥。
孤立点和孤立边的两个端点都不是割点,但孤立边是割边。非连通图的割边为其每个连通分量的割边的并。
为什么割点和割边这么重要?对于无向连通图上的非割点,删去它,图仍然连通,但删去割点后图就不连通了。因此割点相较于非割点对连通性有更大的影响。割边同理。
- 点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
- 边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
- 点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双。
- 边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双。
将某种类型的连通分量根据等价性或独立性缩成一个点的操作称为 缩点,原来连接两个不同连通分量的边在缩点后的图上连接对应连通分量缩点后形成的两个点。根据连通分量的类型不同,缩点可分为无向图上的点双连通分量缩点(圆方树,见 “图论 II”),边双连通分量缩点(本章),以及有向图上的强连通分量缩点(下一章)。
边双和点双缩点后均得到一棵树,而强连通分量缩点后得到一张有向无环图。
- 点双连通:若
处于同一个点双连通分量,则称 点双连通。一个点和它自身点双连通。由一条边直接相连的两点也是点双连通的。 - 边双连通:若
处于同一个边双连通分量,则称 边双连通。一个点和它自身边双连通,但由一条边直接相连的两点不一定边双连通。
点双连通和边双连通是无向图连通性相关最基本的两条性质。
注:点双连通和边双连通有若干等价定义,本文选取的定义并非最常见的定义。其它定义将会作为连通性性质在下文介绍。
3.2 双连通的基本性质
研究双连通的性质时,最重要的是把定义中的基本元素 —— 割点和割边的性质理清楚。然后从整体入手,考察对应分量在原图上的分布形态,再深入单个分量,考察分量内两点之间的性质。以求对该连通性以及连通分量有直观的印象,思考时有清晰的图像作为辅助,做题思路更流畅。
边双连通比点双连通简单,所以先介绍边双连通。
3.2.1 边双连通
考虑割边两侧的两个点
结论
两点之间任意一条迹上的所有割边,就是两点之间的所有必经边。
可以看出割边就是必经边的代名词。必经边需要两个点才有定义,而割边直接由原图定义,且边双缩点后可根据树边(割边)直接求出两点之间的必经边。
断开一条割边,整张图会裂成两个连通块。断开所有割边,整张图会裂成割边条数
添加任意一条边
不同边双没有公共点,即每个点恰属于一个边双,所以:
边双连通的传递性
若
和 边双连通, 和 边双连通,则 和 边双连通。
边双内部不含割边,所以:
结论
边双连通当且仅当 之间没有必经边。
考虑边双中的一条边
结论
对于边双内任意一条边
,存在经过 的回路。 结论
对于边双内任意一点
,存在经过 的回路。
这个结论可以继续加强,见 3.2.3 小节 Menger 定理。
3.2.2 点双连通
删去割点后不连通的两个点之间任意一条路径必然经过该割点,称这样的点为必经点:从
错误结论
两点之间任意一条路径上的所有割点,就是两点之间的所有必经点。
错误原因是对于割边,经过割边一定会从删去割边的一个连通块走到另一个连通块,但经过割点不一定从删去割点的一个连通块走到另一个连通块。如图
, 是割点,也出现在 的路径上,但不是 的必经点。 关于两点之间割点的刻画,见 “图论 II” 广义圆方树。
与边双不同的是,两个点双之间可能有交:考虑 “
进一步地,若两点双有交,交点一定阻碍了它们继续扩大:如果删去该点之后两点双仍连通,同样地,两点双可以合并为更大的点双。
结论:若两点双有交,那么交点一定是割点。
区分:称两点属于同一个边双,即两点边双连通,就是判断它们所在边双是否相同。称两点属于同一个点双,即两点点双连通,是检查是否存在点双同时包含这两个点。因为两点双至多有一个交点,所以若两点点双连通,那么包含这两点的点双唯一。
说明:点双包含原图割点,但在只关心该点双时,原图割点变成了非割点。删去割点后,原图分裂为若干连通分量,但该点双仍连通。
现在我们知道点双交点是割点,那么割点一定是点双交点吗?删去一个割点,将整张图分成若干连通块。存在割点的两个邻居
结论
一个点是割点当且仅当它属于超过一个点双。
结论
由一条边直接相连的两点点双连通。
结合上述性质,得:
推论
一条边恰属于一个点双。
可知割点是连接点双的桥梁,正如割边是连接边双的桥梁。用一个点代表一个点双,并将点双代表点向它包含的割点连边,得到 块割树(Block-Cut Tree,“点双连通块 - 割点” 树,和圆方树有细微差别)。
考虑点双中的一个点
结论
对于
的点双内任意一点 ,存在经过 的简单环。
关于更多点双连通的性质和点双缩点,见 3.2.3 小节 Menger 定理和 “图论 II” 广义圆方树。
3.2.3 Menger 定理及其推论
前置知识:最大流最小割定理。
Menger 定理是研究图连通性时相当重要的定理。从 Menger 定理出发,能得到大量双连通问题的关键结论。
让我们先回顾一下最大流最小割定理:在一张网络上,
的最大流等于 表示存在 条 的边不相交的迹,且 不能更大。 之间的最小割等于 表示存在 条边使得断开这些边后 不连通,且 不能更小。
聪明的读者已经想到,如果将定理应用在边双连通图上,对于任意
结论
对于边双内任意两点
,存在经过 的回路。
对于点双,目标是割掉(删去)若干个点使得
上 的最大流等于 表示存在 条 的点不相交(不在除了 以外的点相交)的路径,且 不能更大。 上 之间的最小割等于 表示存在 个点使得删去这些点后 不连通,且 不能更小。
特别地,当
结论
对于点双内任意两点
,存在经过 的简单环。 结论
对于
的点双内任意两点,存在经过 的长度不小于 的简单环。 证明
若
不直接相邻,使用上述结论。若 直接相邻,因为删去 后整张图仍连通(否则 至少有一个是割点),所以将 接在图上 之间的路径即可。
通过上述铺垫,可以很自然地推出 Menger 定理:
边形式
对于无向图
上任意不同的两点 ,使得 不连通所需删去的边的数量的最小值,等于 之间边不相交的迹的数量的最大值。 点形式
对于无向图
上任意不同且不相邻的两点 ,使得 不连通所需删去的点的数量的最小值,等于 之间点不相交(不在除了 以外的点相交)的路径数量的最大值。
根据 Menger 定理,对于无向图
- 局部边连通度:使得
不连通所需删去的边的数量的最小值为 的 局部边连通度,记作 。它等于 之间边不相交的迹数量的最大值。 - 局部点连通度:对于
,使得 不连通所需删去的点的数量的最小值为 的 局部点连通度,记作 。它等于 之间点不相交的路径数量的最大值。 -边连通:若 ,则称 之间是 k-边连通 的。 -点连通:若 ,则称 之间是 k-点连通 的。
在此基础上,可以定量描述整张图的连通性。
-
全局边连通度:使得存在两个点不连通所需删去的边的数量的最小值为
的 全局边连通度,记作 。它等于任意两点局部边连通度的最小值,即 。特殊定义 。 -
全局点连通度:使得存在两个点不连通所需删去的点的数量的最小值为
的 全局点连通度,记作 。它等于任意不相邻两点局部点连通度的最小值,即 。因 不存在不相邻的两点,特殊定义 。 -
-边连通图:对于 ,若 ,则称 是 k-边连通图,其性质称为 k-边连通性。它可以等价表述为:删去任意不超过 条边,整张图仍然连通。 -
-点连通图:对于 ,若 ,则称 是 k-点连通图,也称 k-连通图,其性质称为 k-点连通性。它可以等价表述为: 且删去任意不超过 个点,整张图仍然连通。
将 Menger 定理推广至任意点对,得到以下结论:
边形式推广
一张图是 k-边连通图当且仅当每对点之间有
条边不相交的迹。 点形式推广
一张图是 k-连通图当且仅当
且每对点之间有 条点不相交的路径。
3.2.4 双连通总结
现在我们知道为什么不存在割边(点)的图叫作边(点)双连通图了:不存在割边说明
根据 Menger 定理,一张图不存在割边等价于对任意两点
目前为止我们都是在单独研究边双和点双,接下来探究它们的联系。下文讨论的点双忽略了 “一边连两点” 等平凡情况,默认
如果一张点数大于
- 根据基本性质,一般 “不经过重复边” 的问题借助边双解决,而 “不经过重复点” 的问题借助点双解决。
- 每个点恰属于一个边双,每条边可能恰属于一个边双(非割边),也可能不属于任何边双(割边);每条边恰属于一个点双,每个点可能属于一个点双(非割点),也可能属于多个点双(割点)。
3.2.5 点双连通的更多性质
以下假设图无自环。
在点双基本性质的基础上,可以推出一些点双的常用性质。
基本性质:对于
性质
对于
的点双中任意一点 与一边 ,存在经过 的简单环。 证明
将
拆成 和 不影响点双连通性。根据基本性质,存在经过 的简单环。因 仅与 相连,故 在环上,将这两条边替换为 得经过 的简单环。 不影响点双连通性的证明:
- 删去
或 ,由 与 或 连通且删去 或 后原图连通可知 不是割点。这一步要求 ,用到了无自环的条件。 - 删去
以外的点,将 和 视为 ,原图连通。 - 删去
,相当于删去 ,若原图不连通则 为割边,当 时 或 为割点,矛盾。
实际上,钦定经过一条边和一个点是几乎等价的(除非钦定经过
性质
对于
的点双中任意不同两点 与一边 ,存在 的简单路径。 证明
由性质
,存在经过 的简单环 ,若 则结论成立。否则令 为任意 的有向路径,考虑 上第一个属于 的交点 ,存在使得 的 ,否则 为割点:删去 后 无法到达 的剩余结点。令路径 为 上 的部分,接上 通过 上有 的一侧到 的路径,则 即为所求。
性质
对于
的点双中任意不同三点 ,存在 的简单路径。 证明
考虑某条以
为端点的边 。由性质 ,存在 ,因此存在 。
3.3 Tarjan 求割点
前置知识:DFS 树,DFS 序。
注意区分:DFS 序表示对一张图 DFS 得到的结点序列,而时间戳 dfn 表示每个结点在 DFS 序中的位置。
记
不妨认为
笔者希望提出一种新的理解 Tarjan 算法的方式。网上大部分博客讲解 Tarjan 算法时 low
数组凭空出现,抽象的定义让很多初学者摸不着头脑,从提出问题到解决问题的逻辑链的不完整性让我们无法感受到究竟是怎样的灵感启发了这一算法的诞生。
3.3.1 非根结点的割点判定
设
若
反之,若删去
现在要刻画 “不经过
注意到,如果
进一步地,因为
因此,如果
-
对于
,如果存在 满足 ,那么删去 后 的每个点和 均连通: 内所有点通过树边连通,且 和 某点直接相连。 -
反之,如果
内所有点的 值均不小于 ,那么删去 后 的每个点和 均不连通。因为如果连通,那么总得有一个点能一步连通。
这样,我们得到了非根结点的割点判定法则:
是割点当且仅当存在树边 ,使得 子树 不存在 点 使得 。 这等价于存在
的儿子 ,满足 。
设 low
的真正含义),根据树形 DP,有
对于后半部分,忽略
特别地,若使用树边更新,则
说明:将
应用:研究删去
- 一个小结论:由 Tarjan 的过程可知只保留最浅的返祖边不改变图的点双连通性。在双极定向时有用(见图论 II)。
3.3.2 根的割点判定、代码
设
若
综上,使用 Tarjan 算法求无向图
再次强调,以下代码仅在求解割点时正确。求解割边需要额外的特判。
模板题 代码。
#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。
3.4 割边的求法
3.4.1 Tarjan
Tarjan 求割边的思路和求割点的思路类似。
探究一条边是割边的充要条件。
首先,树边使得整张图连通,所以割掉非树边不影响连通性。因此
不妨设
考虑如何判定这种情况。根据求解割点的经验,不难得出
Tarjan 求割边有个细节,就是判断非树边。对于当前边
解决方法是记录边的编号。对于 vector,在 push_back
时将当前边的编号一并压入。对于链式前向星,使用成对变换技巧:初始化 cnt = 1
,每条边及其反边在链式前向星中存储的编号分别为
算法的时间复杂度为
3.4.2 树上差分法
求出
- 如果存在,那么删去树边后
分裂出的两部分通过 连接,树边不是割边。 - 否则,删去后
分裂出的两部分不连通,树边是割边。
对于非树边
算法的时间复杂度为
3.5 边双连通分量缩点
3.2.1 小节有结论:将
接下来介绍 Tarjan 算法实现的边双连通分量缩点,类似方法可应用在点双缩点上。关于点双缩点,见 “图论 II” 广义圆方树。
可以先 Tarjan 找出所有割边,再 DFS 找出所有边双连通分量,但太麻烦。我们希望一遍 Tarjan 就可以求出所有割边和边双连通分量。为此,需要对算法进行一些改进。
考虑
考虑将上述操作与 Tarjan 算法相结合:回溯时,若判定
最后栈内剩下一些点,它们单独形成一个边双,且包含根结点。不要忘记将它们弹出。
正确性说明(直观理解,非严格证明):设原图割掉
上述思想在点双缩点和强连通分量缩点时也会用到。
算法的时间复杂度
模板题 代码。
#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。
3.6 例题
P3469 [POI2008] BLO-Blockade
一道 Tarjan 求割点的练手题。
设删去与结点
因为
时间复杂度
双倍经验:SP15577 Blockade。
P2860 [USACO06JAN] Redundant Paths G
题目相当于添加最少的边使得整张图变成一个边双。
考虑边双缩点得到缩点树
我们希望用最少的路径覆盖
证明这是答案下界非常容易,因为每条链至多覆盖两个叶子到它唯一相邻的点的边。当只有两个点的时候特殊讨论。接下来给出一个达到该下界的构造方法。
称两个叶子匹配表示在最终方案中,存在一条连接它们的链。
首先,当叶子个数为奇数时,考虑任意一个叶子
先将所有叶子任意两两匹配,再调整。设在当前方案中,存在一条边
因此,当前方案必然是
当前方案覆盖了
因此,对于当前方案,若某条边没有覆盖,通过上述调整一定能不改变链的条数使得原来被覆盖的边仍被覆盖,且该边也被覆盖。答案上界得证。
时间复杂度
CF51F Caterpillar
题目要求不能存在环,所以首先将所有边双缩成一个点。缩点后整张图会变成一棵森林,先处理每一棵树,再用连通块数量
不妨认为原图连通。
考虑确定主链后如何用最少的操作使得一棵树变成毛毛虫。
对于除了主链以外的结点,考虑它是否作为最终挂在主链旁边的叶子。将主链看成根,具有祖先后代关系的点对不能同时被选中作为叶子,因为此时后代和主链之间隔了一个祖先,说明它到主链的距离
问题转化为选择最多的点使得它们之间没有祖先后代关系。我们的决策是选择所有叶子,因为若一个非叶子被选中,我们一定可以取消选择它,并选它的所有儿子。
因此,最终保留的结点为主链上的结点和我们选中作为叶子的结点。这说明对于路径
当
设缩点树的叶子数量为
时间复杂度
4. 有向图可达性:强连通分量
研究有向图可达性时,强连通分量是最基本的结构。
本章研究强连通分量。默认图为 有向弱连通图。
4.1 相关定义
- 强连通:对于有向图的两点
,若它们相互可达,则称 强连通,这种性质称为 强连通性。
显然,强连通是等价关系,强连通性具有传递性。
-
强连通图:满足任意两点强连通的有向图称为 强连通图。它等价于图上任意点可达其它所有点。
-
强连通分量:有向图的极大强连通子图称为 强连通分量(Strongly Connected Component,SCC)。
强连通分量在求解与有向图可达性相关的题目时很有用,因为在只关心可达性时,同一强连通分量内的所有点等价。
有了使用 Tarjan 算法求割点,割边和边双缩点的经验,我们可以自然地将这些方法进行调整后用于强连通分量缩点。不过在此之前,我们需要研究有向图 DFS 树的性质,毕竟整个过程离不开 DFS。
4.2 有向图 DFS 树
不同于无向图,对于弱连通图
考察有向图 DFS 树
除了有向树边以外,
- 从祖先指向后代的非树边,称为 前向边。
- 从后代指向祖先的非树边,称为 返祖边。
- 两端无祖先后代关系的非树边,称为 横叉边。
前向边和返祖边由无向图 DFS 树的非树边的两种定向得到,而横叉边则是无向图 DFS 树所不具有的,因为无向图 DFS 树的非树边具有祖先后代性。
为什么有向图 DFS 树会出现横叉边?因为一个后访问到的结点可以向之前访问的结点连边。例如
接下来讨论有向边
尽管子树的独立性变弱了,但我们依然可以发现一些性质:
- 对于前向边
, 本身就能通过树边到达 ,删去后没有任何影响。它甚至不影响可达性。 - 对于返祖边
,它使 上从 到 的路径上的所有点之间强连通。多个返祖边结合可以形成更大更复杂的强连通结构。 - 对于横叉边
, 。否则 先被访问,那么无论是 的儿子访问到了 ,还是 本身直接访问 ,离开 之前 一定被访问过了,即 。那么 为前向边,矛盾。
综合上述三条性质,可知返祖边和横叉边均减小时间戳,只有树边增大时间戳。前向边不影响强连通性,所以忽略掉所有前向边。
考虑横叉边
若
反之,若
结论
若
,则 可达 当且仅当 可达 。
- 一个更深刻的结论和理解见图论 II 的 4.3 小节引理 2(一般图支配树)。
因此,对于横叉边
该结论可以进一步推出:
结论
若
强连通,则 在树上路径上的所有点强连通。 结论
强连通分量在有向图 DFS 树上弱连通。
4.3 Tarjan 求 SCC
因为每个 SCC 在
考虑判定关键点。如果
因此,设
证明
考虑证明
不是关键点当且仅当 。 充分性:若
,那么存在 ,使得从 出发存在返祖边或有用的横叉边 满足 。如果 是返祖边,那么 显然是 的祖先;如果 是横叉边,那么 可达 ,又因为 且 均为 的祖先,所以 是 的祖先。无论哪种情况,都说明包含 的 SCC 包含 的祖先, 不是关键点。 必要性:若
不是关键点,那么 可达 。考虑任意一条 的路径,找到第一次离开 的边 。若 是返祖边,那么 是 的祖先;若 是横叉边,那么 且 可达 ,则 可达 ,则 可达 。无论哪种情况,都说明 。
说明:一般将
类似边双缩点,这次我们依然采用 “剥叶子” 的方式:维护栈
最后考虑如何求
- 对于树边
,用 更新 。 - 对于前向边
,用 更新 没有任何影响,因为 。 - 对于返祖边
,用 更新 。 - 对于横叉边
,若 不可达 ,那么 和 不强连通,所以从 不断回溯至 的过程中一定有关键点弹出 。否则 可达 ,那么 和 强连通,因为 没有被弹出,所以 也没有被弹出。但是直接这样说明会涉及 的正确性和弹出 SCC 的正确性之间的循环论证。注意到横叉边更新 的正确性只依赖于已经弹出的每个点集是 SCC,所以归纳假设已经弹出的每个点集为 SCC,这样当前弹出点集的每个点的 是正确的,从而保证了当前弹出点集也是 SCC。
对于返祖边
综上,得到转移:对于树边
SCC 缩点后,得到的新图不含环,否则环上所有结点对应的 SCC 可以形成一个更大的 SCC。这说明 SCC 缩点图是一张 DAG。
重要结论:对于两个 SCC
以下是 模板题 题解。
因为可以多次经过同一个点,所以一旦进入某个 SCC,就一定可以走遍其中所有点,获得所有点权值之和的贡献。但是一旦离开 SCC,就没法再回来了,否则离开后遍历的点和当前 SCC 强连通。
因此,将有向图 SCC 缩点后得到 DAG,每个 SCC 缩点后的权值等于其中所有点的权值之和。问题转化为 DAG 最长带权路径,拓扑排序 DP 即可。时间复杂度
#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;
}
4.4 Kosaraju
Kosaraju 的思想:利用 SCC 内任意两点相互可达的性质,先一次 DFS 求出缩点图的拓扑序,再一次 DFS 求出所有 SCC。
考虑能否通过时间戳求出拓扑序。设 SCC 的编号为它包含的所有点的最大时间戳,通过编号不能推导出关于拓扑序的信息:对于两个 SCC
时间戳不管用的本质原因在于,我们每访问到一个点就将其加入 DFS 序,这要求 DFS 的顺序就是合法拓扑序,但显然做不到:我们甚至不知道从哪里入手开始 DFS。
拓扑序是不现实的,只能寄希望于反拓扑序。如果我们将顺序改为离开每个点的次序,整个问题就豁然开朗了:离开
接下来是第二次 DFS。
从离开时间最大的结点开始 DFS,会访问到它所在的 SCC。为保证其它 SCC 不被访问到,将所有边的方向反过来。这不改变任意两点之间的强连通性,所以反图的 SCC 划分方案和原图相同,但过程中不会访问其它 SCC,因为它们的编号小于当前 SCC,在原图上不可达当前 SCC,因而在反图上不可由该 SCC 到达。
将遍历到的结点打包成 SCC 删去(标记为已访问),不断找到最大的未被访问的结点重复上述过程,即可得到原图的 SCC 划分。时间复杂度
一般 SCC 缩点优先考虑 Tarjan,它只需要一次 DFS,而 Kosaraju 需要两次。Kosaraju 相比 Tarjan 的优势在于:整个过程只有 DFS 和标记。当图是稠密图时,可以 bitset 加速做到除预处理外
4.5 例题
P3436 [POI2006] PRO-Professor Szu
对于一个 SCC,如果它内部有边且可达
SCC 缩点后,统计出所有这样的 SCC。设
时间复杂度
P7737 [NOI2021] 庆典
首先 SCC 缩点。
缩点后删去重边,利用题目性质保证每个点的入度不超过
称给定的
求好点的方法:设新图
法一:对于好点
法二:用类似虚树的方法统计答案,保证栈内是一条好点构成的从上到下的链。注意,使用
直接建虚树,加边 DFS,然后在虚树上统计答案,时间复杂度
*ARC092D Two Faced Edges
对于连接 SCC
对于 SCC 内部的边,若反向
因此,反向
问题转化为对每个点的每条出边
正反扫一遍出边,每次从
注意:我们考虑的是保留
进一步地,发现对于连接 SCC 之间的边
对整张图进行 vis
表示每个结点是否访问过,e[u]
表示 (~vis & e[u])._Find_first()
即为所求。时间复杂度
启示:序列去掉一个位置的信息可由前缀和后缀合并得到。
*CF1361E James and the Chase
很好的强连通题,需要对有向图 DFS 树有一定理解。
因为题目保证图强连通,所以从任意一个点出发 DFS 可达图上所有点。
判定一个点是否合法:以该点为根的任意 DFS 树只有树边和返祖边:横叉边和前向边将导致大于一条简单路径。
首先找到任意合法点:随机
性质:点
称一条返祖边
如果点
如果
又因为一个非根节点必须能够离开子树(否则不满足原图强连通的性质),所以每个点被至少一条返祖边覆盖。因此,点
用树上差分维护每个点被多少条边覆盖,再用树形 DP 求出每个点最浅能够跳到哪个祖先,最后从上往下 DP 求出每个点是否合法,时间复杂度
总时间复杂度
5. 欧拉回路
小学奥数之一笔画问题。
5.1 相关定义
- 欧拉路径:经过连通图中所有边恰好一次的迹称为 欧拉路径。
- 欧拉回路:经过连通图中所有边恰好一次的回路称为 欧拉回路。
- 欧拉图:有欧拉回路的图称为 欧拉图。
- 半欧拉图:有欧拉路径但没有欧拉回路的图称为 半欧拉图。
欧拉图能够以任意一点作为起点一笔画整张图然后回到该点,而半欧拉图只能从
5.2 欧拉图的判定
5.2.1 有向图
考察有向欧拉图
首先
对于满足上述条件的图
将
综上,可归纳证明有向欧拉图的判定准则:
有向图
是欧拉图,当且仅当 弱连通且 的每个点的入度等于出度。
读者可类似推导有向半欧拉图的判定准则,这里不加证明地给出结论:
有向图
是半欧拉图,当且仅当 弱连通,存在两个点的入度分别等于出度减 和出度加 ,且其余每个点的入度等于出度。
入度等于出度减
5.2.2 无向图
考察无向欧拉图
首先
对于满足条件的图
将
综上,可以归纳证明无向欧拉图的判定准则:
无向图
是欧拉图,当且仅当 连通且 的每个点的度数为偶数。
读者可类似推导无向半欧拉图的判定准则,这里不加证明地给出结论:
无向图
是半欧拉图,当且仅当 连通且恰存在两个点的度数为奇数。
两个奇度点分别为欧拉路径的起点和终点。
5.2.3 混合图
给定图
忽略掉将所有有向边改为无向边后图不连通的情况,此时
考虑钦定无向边的方向,将
注意到每个点的入度加出度固定,就是它在
每条无向边要么给
对于半欧拉图判定,
5.3 Hierholzer
在研究欧拉图的判定条件时,我们通过构造一条合法的欧拉回路证明了判定条件的充要性。而构造的方法就可以用来求欧拉回路。
先考虑有向图,因为无向图需要处理重边的问题。
Hierholzer 的核心是不断往当前回路的某个点中插入环。具体地,从任意一点出发 DFS,找到任意一个回路
使用链式前向星维护每个点的剩余出边(类似 Dinic 的当前弧优化),每条边只会被遍历一次。用双向链表维护回路的合并,时间复杂度
做法的时间复杂度优秀,但实现起来有些复杂。我们可以先理解复杂的方法做了些什么,再思考有哪些地方可以简化。
从整体上考察,我们实现了这样的步骤:从一个起点开始找到一个回路,然后以环上的每个点为起点找到一个回路,不断递归下去。为了依次从回路上的每个点开始找回路,需要显式地求出这个回路,再依次处理上面的所有结点。所以我们需要一个 DFS 函数找回路,另一个递归式函数求欧拉回路。
但显式地求出回路是不必要的:我们可以按任意顺序从当前回路上的点出发找回路。考虑在 DFS 找回路的回溯过程中直接对当前回路上的每个点找回路。换言之,将原本找回路的顺序倒过来,这样就没有必要显式地求出当前回路,而是在回溯的过程中,一边对当前点找回路,一边往回路中插入当前回路。
综上,我们得到求欧拉回路的最常用算法 —— Hierholzer 的具体步骤:遍历当前点
若要求字典序最小,只需在一开始将每个点的所有出边从小到大排序,并从编号最小的结点开始 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;
}
5.4 例题
P2731 [USACO3.3] 骑马修栅栏 Riding the Fences
无向图最小字典序欧拉路径板子题。
代码。
P1127 词链
从每个字符串的第一个字符向最后一个字符连边,跑有向图欧拉回路。
注意,对邻接链表排序要按照每条边对应字符串的字典序排序,而非指向结点的编号大小。
时间复杂度
P3520 [POI2011] SMI-Garbage
设
时间复杂度
*P3443 [POI2006] LIS-The Postman
题目保证无重边,所以每条路径片段的限制形如片段中相邻的两条边必须先后走。
所有限制关系形成若干条边先后走的链。将每条链缩成起点到终点的一条边,然后跑欧拉回路,最后输出这条边时需要重新展开成链。
若限制关系出现环或分叉则无解,并查集 + 链表维护。
时间复杂度是离散化的
P3511 [POI2010] MOS-Bridges
若存在
二分答案转混合图欧拉回路判定,构建二分图后网络流即可。
时间复杂度
CF1361C Johnny and Megan's Necklace
枚举权值
时间复杂度
*CF1458D Flip and Reverse
很棒的题目。
将
考虑一次操作的本质:它相当于选取折线图上等高的两点,水平翻转。这不改变折线上每个线段连接的点的高度。
又因为如果
因此,在
证明也很容易,根据上一段提到的性质,分成 “右侧有相同高度” 和 “右侧没有相同高度” 两种情况讨论即可。
时间复杂度
CHANGE LOG
- 2021.12.5:修改例题代码与部分表述,增加基础定义。
- 2022.4.22:重构文章。
- 2022.5.21:进行一些增补,添加 Floyd 算法和 SCC 缩点。
- 2022.5.25:添加 Hierholzer 算法。
- 2022.8.15:修正强连通分量部分的事实性错误。
- 2022.11.1:重构文章。
- 2023.4.10:重写最短路和差分约束,添加例题代码。
- 2023.4.29:重写无向图连通性部分,添加说明。
- 2023.6.21:重写有向图可达性部分,添加 Kosaraju 算法。
- 2023.7.3:添加用拟阵理解生成树的部分。
- 2023.7.4:添加生成树相关的扩展问题。
- 2023.7.6:添加同余最短路。
- 2023.7.31:添加边双连通分量缩点的代码,添加点双性质进阶,修改小错误。
- 2023.8.5:补充说明。
- 2023.8.25:修改 typo,添加例题 ABC232G。
- 2023.8.28:添加例题 CF1361E。
- 2023.9.11:添加基图定义。
- 2024.8.9:修订文章,添加一些图片。
- 2024.8.13:修正事实性错误(3.2.5 小节添加无自环的要求)。
参考资料
定义
第一章
- Dijkstra 算法描述及正确性证明 —— ciwei。
- Floyd 算法正确性 —— dypdypdyp123。
- 【图论】传递闭包的概念 —— zkq_1986。
- 删边最短路问题 —— dengyaotriangle。
- 堆的可持久化 —— 俞鼎力。
- 可持久化线段树求 k 短路 —— ix35。
第二章
- 最小生成树 —— OI Wiki。
- 怎么理解拟阵(matroid) —— SleepyBag。
- 拟阵及应用 —— 好地方 bug。
- 2018 集训队论文《浅谈拟阵的一些拓展及其应用》—— 杨乾澜。
第三章
- Menger's theorem —— Wikipedia。
- k-vertex-connected graph —— Wikipedia。
- k-edge-connected graph —— Wikipedia。
- 双连通 / 圆方树 胡扯笔记 —— ycx060617。
第四章
- Tarjan 算法求 SCC 学习笔记 —— ycx060617。
第五章
- 欧拉回路与欧拉通路存在性的充要条件及其证明。
- 欧拉图 —— OI Wiki。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!