图论——连通性
图论——连通性
基本知识
路径相关
-
途径:连接一串结点的序列称为 途径,用点序列
和边序列 描述,其中 ,通常写为 。 -
迹:不经过重复边的途径称为 迹。
-
回路:
的迹称为 回路。 -
路径:不经过重复点的迹称为 路径,也称 简单路径。不经过重复点比不经过重复边强,所以不经过重复点的途径也是路径。注意题目中的简单路径可能指迹。
-
环:除
外所有点互不相同的途径称为 环,也称 圈 或 简单环。
连通性相关
-
连通:对于无向图的两点
,若存在途径使得 且 ,则称 连通。 -
弱连通:对于有向图的两点
,若将有向边改为无向边后 连通,则称 弱连通。 -
连通图:任意两点连通的无向图称为 连通图。
-
弱连通图:任意两点弱连通的有向图称为 弱连通图。
-
可达:对于有向图的两点
,若存在途径使得 且 ,则称 可达 ,记作 。
无向图的连通性
本节研究双连通分量相关知识点,研究对象为无向连通图。若无特殊说明,默认图为 无向连通图。
相关定义
无向图连通性,主要在研究割点和割边。
- 割点:在无向图中,删去后使得连通分量数增加的结点称为 割点。
- 割边:在无向图中,删去后使得连通分量数增加的边称为 割边(桥)。
孤立点和孤立边的两个端点都不是割点,但孤立边是割边。非连通图的割边为其每个连通分量的割边的并。
为什么割点和割边这么重要?对于无向连通图上的非割点,删去它,图仍然连通,但删去割点后图就不连通了。因此割点相较于非割点对连通性有更大的影响。割边同理。
- 点双连通图:不存在割点的无向连通图称为 点双连通图。根据割点的定义,孤立点和孤立边均为点双连通图。
- 边双连通图:不存在割边的无向连通图称为 边双连通图。根据割边的定义,孤立点是边双连通图,但孤立边不是。
点双连通图和边双连通图有很好的性质,帮助我们解题。
- 点双连通分量:一张图的极大点双连通子图称为 点双连通分量(V-BCC),简称 点双。
- 边双连通分量:一张图的极大边双连通子图称为 边双连通分量(E-BCC),简称 边双。
将某种类型的连通分量根据等价性或独立性缩成一个点的操作称为 缩点,原来连接两个不同连通分量的边在缩点后的图上连接对应连通分量缩点后形成的两个点。
边双和点双缩点后均得到一棵树,而强连通分量缩点后得到一张有向无环图。
-
点双连通:若
处于同一个点双连通分量,则称 点双连通。一个点和它自身点双连通。由一条边直接相连的两点也是点双连通的。 -
边双连通:若
处于同一个边双连通分量,则称 边双连通。一个点和它自身边双连通,但由一条边直接相连的两点不一定边双连通。
点双连通和边双连通是无向图连通性相关最基本的两条性质。
注:点双连通和边双连通有若干等价定义(或很相似),本文选取的定义并非最常见的定义。其它定义将会作为连通性性质在下文介绍。
双连通的基本性质
研究双连通的性质时,最重要的是把定义中的基本元素 —— 割点和割边的性质理清楚。然后从整体入手,考察对应分量在原图上的分布形态,再深入单个分量,考察分量内两点之间的性质。以求对该连通性以及连通分量有直观的印象,思考时有清晰的图像作为辅助,做题思路更流畅。
边双连通比点双连通简单,所以先介绍边双连通。
边双连通
考虑割边两侧的两个点
结论:两点之间任意一条迹上的所有割边,就是两点之间的所有必经边。
可以看出割边就是必经边的代名词。必经边需要两个点才有定义,而割边直接由原图定义,且边双缩点后可根据树边(割边)直接求出两点之间的必经边。
断开一条割边,整张图会裂成两个连通块。断开所有割边,整张图会裂成割边条数
不同边双没有公共点,即每个点恰属于一个边双,所以:
边双连通的传递性:若
和 边双连通, 和 边双连通,则 和 边双连通。
边双内部不含割边,所以:
结论:
边双连通当且仅当 之间没有必经边。
考虑边双中的一条边
结论:对于边双内任意一条边
,存在经过 的回路。 结论:对于边双内任意一点
,存在经过 的回路。
点双连通
删去割点后不连通的两个点之间任意一条路径必然经过该割点,称这样的点为必经点:从
结论:两点之间任意一条路径上的所有割点,就是两点之间的所有必经点。
与边双不同的是,两个点双之间可能有交:考虑一个 “
结论:若两点双有交,那么交点一定是割点。
-
区分:称两点属于同一个边双,即两点边双连通,就是判断它们所在边双是否相同。称两点属于同一个点双,即两点点双连通,是检查是否存在点双同时包含这两个点。因为两点双交点数大于
,所以若两点点双连通,那么包含这两点的点双唯一。 -
说明:点双包含原图割点,但在只关心该点双时,原图割点变成了非割点。删去割点后,原图分裂为若干连通分量,但该点双仍连通。
现在我们知道点双交点是割点,那么割点一定是点双交点吗?删去一个割点,将整张图分成若干连通块。存在割点的两个邻居
结论:一个点是割点当且仅当它属于超过一个点双。
结论:由一条边直接相连的两点点双连通。
结合上述性质,得:
推论:一条边恰属于一个点双。
可知割点是连接点双的桥梁,正如割边是连接边双的桥梁。用一个点代表一个点双,并将点双代表点向它包含的割点连边,得到 块割树。
考虑点双中的一个点
结论:对于
的点双内任意一点 ,存在经过 的简单环。
Tarjan 求割点
前置知识:DFS 树,DFS 序。
- 注意区分:DFS 序表示对一张图 DFS 得到的结点序列,而时间戳 DFN 表示每个结点在 DFS 序中的位置。
记
不妨认为
提出一种新的理解 Tarjan 算法的方式。Tarjan 算法时 low
数组凭空出现,抽象的定义让很多初学者摸不着头脑,从提出问题到解决问题的逻辑链的不完整性让我们无法感受到究竟是怎样的灵感促使了这一算法的诞生。
非根的割点判定
设
若
交换条件和结论,若存在
如何表示 “不经过
注意到,如果
进一步地,因为
因此,如果
-
对于
,如果存在 满足 ,那么删去 后 的每个点和 均连通: 内所有点连通,且 和 连通。 -
反之,如果
内所有点的 值均不小于 ,那么删去 后 的每个点和 均不连通。因为如果连通,那么总得有个点能一步连通。
这样,我们得到了非根结点的割点判定法则:
是割点当且仅当存在树边 ,使得 子树 不存在 点 使得 。 这等价于存在
的儿子 ,满足 。
设
对于右半部分,忽略
说明:将
应用:研究删去
根的割点判定与代码
设
若
综上,使用 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;
} 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;
}
割边的求法
Tarjan
求割边的思路和求割点的思路类似。
探究一条边是割边的充要条件。
首先,树边使得整张图连通,所以割掉非树边不影响连通性。因此
不妨设
考虑如何判定这种情况。根据求解割点的经验,不难得出
Tarjan
求割边有个细节,就是判断非树边。对于当前边
解决方法是记录边的编号。对于 vector
,在 push_back
时将当前边的编号一并压入。对于链式前向星,使用成对变换技巧:初始化 cnt = 1
,每条边及其反边在链式前向星中存储的编号分别为
算法的时间复杂度为
边双连通分量缩点
将
接下来介绍 Tarjan 算法实现的边双连通分量缩点,类似方法可应用在点双缩点上。关于点双缩点,见 “图论进阶” 圆方树。
先 Tarjan
找出所有割边,再 DFS 找出所有边双连通分量是可行的,但太麻烦了。我们希望一遍 Tarjan
就可以做到求出所有割边和边双连通分量。为此,需要对 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;
}
例题
[CEOI2011] Traffic
“保证边在交点以外的任何地方不相交”,从这句话得知图是一个平面图,又因为图为一个平面图,所以我们很容易地发现一个性质。
对于所有西岸和东岸先按照
降序排列,然后依次考虑西岸的每一个点,如果这个西岸的点不能到达某个东岸的点,那么他后面的所有点都不能到达这个东岸的点。换句话说也就是,每个西岸的点能到达东岸的点是一段 值连续的区间,且如果一个点不能到达区间内的一个其它点,那么所有的东岸点都不能到达,因为图是平面图。
所以我们考虑一个这样的做法,求出所有西岸点能到达的所有东岸点,再对这些点以及中间的点建出反图,先按东岸点
所以最终答案即为这个西岸点能到达的
[POI2008] BLO-Blockade
分情况讨论,即这个点是不是原图的割点,所以我们要求出割点。
- 如果不是,那么答案即为
。 - 如果是,那么答案为去掉这个割点的每个连通块的大小乘去除这个连通块的的其它所有连通块的大小之和。
很容易发现,答案具有对称性,所以我们只用在求割点的过程中求出一半答案最后再
这种发现性质并应用性质的思想值得学习。
[HNOI2012] 矿场搭建
这道题很有意思,很明显这道题和点双连通分量有关,可以分
- 如果一个点双连通分量里面没有割点那么只需设置
个救援出口,因为只设置一个的话,可能刚好这个点发生事故。令这个点双大小为 ,所以方案数 。 - 如果一个点双连通分量里面有且只有一个割点,那么要只需设置
个救援出口,因为如果割点发生事故的话,点双里的其它点可以去这个救援出口,否则可以通过割点到达其它连通块。令这个点双大小为 ,所以方案数为 ,既不能在割点设置救援出口。 - 如果一个点双连通分量里面有多个割点,那么要无需设置救援出口,因为如果一个割点发生事故的话,点双里的其它点还可以通过其他割点到其他连通块的救援出口。方案数为
。
先求出所有点双,最后将所有连通块的方案数相乘即为答案。
实时交通查询系统
很明显是关于必经点的问题,所以先建出圆方树,再考虑怎么求出答案,因为圆点和方点交错出现,我们可以分类讨论两个圆点在圆方树上的路径的除开本身起点和终点的两个圆点的其它圆点个数。(注: 圆方树的性质为圆点只向方点连边,方点只向圆点连边,换句话说也就是圆点和方点交错出现。)
- 如果两个圆点在圆方树上的
为方点,令两个圆点和 在圆方树上的深度分别为 , 与 。则 到 路径上的圆点个数为 , 到 的圆点个数为 ,两者加起来为 。 - 如果两个圆点在圆方树上的
为圆点,令两个圆点和 在圆方树上的深度分别为 , 与 。则 到 路径上的圆点个数为 , 到 的圆点个数为 ,因为两者直接加起来会把 算两次,所以最后要 ,两者加起来为 。
综上所述,圆点个数为
有向图的可达性
研究有向图可达性时,强连通分量是最基本的结构。
本文研究强连通分量。若无特殊说明,默认图为 有向弱连通图。
相关定义
- 强连通:对于有向图的两点
,若它们相互可达,则称 强连通,这种性质称为 强连通性。
显然,强连通是等价关系,强连通性具有传递性。
-
强连通图:满足任意两点强连通的有向图称为 强连通图。它等价于图上任意点可达其它所有点。
-
强连通分量:有向图的极大强连通子图称为 强连通分量(Strong Connected Component,SCC)。
强连通分量在求解与有向图可达性相关的题目时很有用,因为在只关心可达性时,同一强连通分量内的所有点等价。
有了使用 Tarjan 算法求割点,割边和边双缩点的经验,我们可以自然地将这些方法进行调整后用于求解强连通分量的缩点问题。不过在此之前,我们需要研究有向图 DFS 树的性质,毕竟整个过程一定离不开 DFS。
有向图 DFS 树
不同于无向图,对于弱连通图
我们考察有向图 DFS 树
除了有向的树边以外,
-
从祖先指向后代的非树边,称为 前向边。
-
从后代指向祖先的非树边,称为 返祖边。
-
两端无祖先后代关系的非树边,称为 横叉边。
前向边和返祖边由无向图 DFS 树的非树边的两种定向得到,而横叉边则是无向图 DFS 树所不具有的了,因为无向图 DFS 树的非树边具有祖先后代性。
为什么有向图 DFS 树会出现横叉边?因为一个后访问到的结点可以向之前访问的结点连边。例如:
接下来讨论有向边
尽管子树的独立性变弱了,但我们依然可以发现一些性质:
-
对于前向边
, 本身就能通过树边到达 ,删去后没有任何影响。它甚至不影响可达性。 -
对于返祖边
,它使 上从 到 的路径上的所有点之间强连通。多个返祖边结合可以形成更大更复杂的强连通结构。 -
对于横叉边
, 。否则 先被访问,那么无论是 的儿子访问到了 ,还是 本身直接访问 离开 之前 一定被访问过了,即 。那么 为前向边,矛盾。感性理解一下就是,只会有右边的子树连向左边的子树,否则如果左边的子树连向右边的子树就不符合 的遍历顺序,又因为左边子树的 值一定比右边的子树大,所以有 。
综合上述三条性质,可知返祖边和横叉边均减小时间戳,只有树边增大时间戳。前向边不影响强连通性,所以接下来我们忽略掉所有前向边。
考虑横叉边
考虑到若
反之,若
结论: 若
,则 可达 当且仅当 可达 。
可知对于横叉边
该结论可以进一步推出:
结论: 若
强连通,则 在树上路径上的所有点强连通。
结论: 强连通分量在有向图 DFS 树上弱连通。
Tarjan 求 SCC
因为每个 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;
}
例题
间谍网络
考虑这个有向图中的一个强连通分量,我们只用掌握了其中一个点,那么其它点都会被掌握。所以我们先用 Tarjan
缩点,再重新建一个图,入度为
[APIO2009] 抢掠计划
先分析题目性质,发现如果我们能到达一个强连通分量里的点那么我们一定能到达里面的所有点。所以我们可以先用 Tarjan
缩点,再进行拓扑排序 +
设
则初始化应为
转移方程应为(采用刷表法):
其中
最终答案为
注:此题有一个坑点,进行 Tarjan
缩点时只需以 Tarjan
,因为可能从起点出发可能到不了这些点,从而导致更新答案错误。
[ZJOI2007] 最大半连通子图
因为一个强连通分量里的点绝对是这个图的半连通子图,所以我们先对原图进行缩点,然后再进行拓扑排序 +
设
则转移方程为(采用刷表法):
其中
初始化为在新图中入度为
则最后答案即为
注: 此题有一个巨大的坑点,就是直接缩点后进行拓扑 +
[SNOI2017] 炸弹
很容易就想到如果一个炸弹能炸到其它炸弹我们就将这个1炸弹与它能炸到的炸弹连一条有向边。
但是一个炸弹
建图完毕后,这个图可能有环存在,我们先进行缩点,把原图缩成一个有向无环图,然后再进行
- 对这个
建反图,再进行拓扑 + 。 - 我们发现出度为
为初始状态很符合记忆化搜索的过程,所以直接进行 然后再更新答案即可。
注: 这道题拓扑排序还是要去重边,原因同上一道例题,因为统计个数就会算重答案,而
[HAOI2010] 软件安装
我们发现直接建图后对于一个强连通分量里的所有点要么全选,要么全不选,所以可以先把这些强连通分量缩点。
又因为原图除入度为
那我们就可以从上一题的经验中知道,这道题也可以进行记忆化搜索。
直接树上
[SDOI2010] 所驼门王的宝藏
考虑如果我们到了某一行的某一个格子,且这个格子的类型为“横天门”,那么我们就可以到达这一行的所有格子,且特别地我们与这一行的其它“横天门”形成了一个环。所以我们可以考虑对每一行新建一个虚点,不管什么样的格子都向这个虚点连一条有向边,如果这个格子为“横天门”,那么这个虚点再向这个点单独连边。“纵寰门”同理。
然后我们再对这个图进行缩点,拓扑排序 +
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现