高级图论
本篇博客已经弃用。最新修订版见 图论 II。
初级图论。
CHANGE LOG
- 2022.5.26:修改文章。
- 2022.6.8:添加 SAT 的定义。
- 2022.6.10:添加 DAG 的支配树。
- 2022.9.30:添加 DAG 链剖分。
1. 同余最短路
说难也不算难,但是挺有意思的一个知识点。应用不广泛。
前置知识:SPFA / Dijkstra 求最短路。
1.1 算法简介
同余最短路用于求解在某个范围内有多少重量可以由若干物品做完全背包凑出,或者说,有多少数值可由给定的一些数进行 系数非负 的线性组合得到。
我们尝试具体描述这样的问题。给出
同余最短路的核心思想在于观察到:如果一个数
这个思想非常巧妙,感觉不是凡人能够想到的思路。一种可能的理解方式是,考虑每个数能否被表出的状态
得到模
注意到上述过程非常像一个最短路:对于每个点
初始值
求出
用
时间复杂度为 dijkstra 的
1.2 例题
*I. P2371 [国家集训队] 墨墨的等式
同余最短路模板题。
设
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;
int n, a[N], in[N];
long long f[N], l, r, ans;
int main() {
cin >> n >> l >> r;
for(int i = 1; i <= n; i++) cin >> a[i];
memset(f, 0x3f, sizeof(f));
sort(a + 1, a + n + 1);
queue<int> q;
q.push(0), f[0] = 0;
while(!q.empty()) {
int t = q.front();
q.pop(), in[t] = 0;
for(int i = 2; i <= n; i++) {
int it = (t + a[i]) % a[1];
long long d = f[t] + a[i];
if(d < f[it]) {
f[it] = d;
if(!in[it]) in[it] = 1, q.push(it);
}
}
}
for(int i = 0; i < a[1]; i++) {
if(r >= f[i]) ans += max(0ll, (r - f[i]) / a[1] + 1);
if(l > f[i]) ans -= max(0ll, (l - 1 - f[i]) / a[1] + 1);
}
cout << ans << endl;
return cerr << "Time: " << clock() << endl, 0;
}
II. P3403 跳楼机
同余最短路的板子题。
*III. AT3621 [ARC084B] Small Multiple
一道神仙题。
注意到所有数都可以从
注意,不可以以
时间复杂度
IV. P2662 牛场围栏
同余最短路板题。
SPFA 的时间复杂度为
V. 模拟赛题 梦回 2021
给定
个值域 的 随机数,求最大的不能被这些数表示出的数。 , 。
和 IV. 一样,无限背包问题考虑同余最短路。因为数据随机,所以最小值的期望为
直接跑 SPFA 的期望复杂度为
[P4156 WC2016]论战捆竹竿 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
2. 2-SAT
2-SAT 具有很强的逻辑性,因为 SAT 本身由逻辑学术语定义。
2.1 SAT 的定义
该部分与 OI 没有太大关系,不感兴趣的读者可以跳过。
为方便说明,首先给出相关术语。
- 合取:用符号
表示,是自然语言联结词 “且” 的抽象。命题 表示 的合取,称为合取式,读作 且 ,其为真当且仅当 均为真。简单地说,合取就是逻辑与&&
,可以类比计算机科学中的按位与&
,相信这个概念大家并不陌生。 - 析取:用符号
表示,是自然语言联结词 “或” 的抽象。命题 表示 的析取,称为析取式,读作 或 ,其为真当且仅当 至少有一个为真。同样的,合取是逻辑或||
,类比按位或|
。 - 否定:用符号
表示, 表示命题 的否定,其真假性与 相反。否定是逻辑非!
,类比按位取反~
。
上述三条概念均为基本命题联结词,大概可以看作给常见的 “与或非” 三种运算起了高大上的名字。将命题变元(可真可假的布尔变量)用合取
布尔逻辑式可满足,指存在一个对所有命题变元的真假赋值,使得该布尔逻辑式为真。
布尔可满足性问题(Boolean Satisfiability Problem)简称 SAT,它定义为检查一个布尔逻辑式是否可满足,是第一个被证明的 NPC 问题。
- 命题变元或其否定称为文字。
- 若干个文字的析取称为简单析取式,形如
,其中 表示命题 或其否定 。 - 若干简单析取式的合取称为合取范式(Conjunctive Normal Form,CNF),形如
,其中 表示一个简单析取式。
考虑 SAT 的简单版本:命题公式为合取范式,且组成它的每个简单析取式至多含有
当
2.2 算法介绍
首先我们将抽象的 2-SAT 描述得更具体一些。将简单析取式看成条件,我们希望每个条件均被满足,而每个简单析取式的形态是固定的:
注意到每个条件仅和至多两个文字(变量)有关,这启发我们思考图论解法。
尝试将限制写成 “若,则” 的形式,因为可达性具有传递性,而蕴含性同样具有传递性(若
: 必须为真。可以用若 则 来限制 为真。 : 必须为假。同理,若 则 。 : 和 不能同时为假,即若 则 ,若 则 。 :若 则 ,若 则 。 :若 则 ,若 则 。
注意第三,四,五中简单析取式所产生的连边具有 对称性。因为若一个命题成立,则其逆否命题仍然成立:若
这样,若一个命题变元
因此,对有向图进行强连通分量缩点,若
除此以外,是否一定有解呢?
首先确定解的形态。对于与命题变元
接下来尝试构造一组解。注意到若
先猜后证,考虑证明这样做的正确性。反证法,假设存在
因此此时一定有解。
综上,若只需求出任意一组解,那么对所点后的图拓扑排序,然后对于每个命题变元相关的两个文字,使得拓扑序更大的那个为真。注意到我们在 tarjan 时已经得到了缩点后 DAG 的反向拓扑序:若 col[v] <= col[u]
,因此只需选择 col
较小的文字赋为真。时间复杂度
如果要求字典序最小解,可以按位贪心,每次尽量选字典序小的点,遍历所有其能够到达的点并检查是否使当前决策出现矛盾,贪心的局部决策不影响全局合法性留给读者自证。时间复杂度
2.3 例题
P4782 【模板】2-SAT 问题
#include <bits/stdc++.h>
using namespace std;
constexpr int N = 2e6 + 5; // 两倍空间
int cnt, hd[N], nxt[N], to[N];
void add(int u, int v) {nxt[++cnt] = hd[u], hd[u] = cnt, to[cnt] = v;}
int n, m, dn, dfn[N], low[N], top, stc[N], vis[N], cn, col[N];
void tarjan(int id) {
dfn[id] = low[id] = ++dn, vis[id] = 1, stc[++top] = id;
for(int i = hd[id]; i; i = nxt[i]) {
int it = to[i];
if(!dfn[it]) tarjan(it), low[id] = min(low[id], low[it]);
else if(vis[it]) low[id] = min(low[id], dfn[it]);
}
if(low[id] == dfn[id]) {
col[id] = ++cn;
while(stc[top] != id) col[stc[top]] = cn, vis[stc[top--]] = 0;
vis[id] = 0, top--;
}
}
int main() {
cin >> n >> m;
for(int i = 1; i <= m; i++) {
int u, a, v, b;
scanf("%d%d%d%d", &u, &a, &v, &b);
add(u + (!a) * n, v + b * n); // 当 u 等于 !a 时,v 必须等于 b
add(v + (!b) * n, u + a * n); // 同理
}
for(int i = 1; i <= n << 1; i++) if(!dfn[i]) tarjan(i);
for(int i = 1; i <= n; i++) if(col[i] == col[i + n]) puts("IMPOSSIBLE"), exit(0);
puts("POSSIBLE");
for(int i = 1; i <= n; i++) putchar('0' + (col[i + n] < col[i])), putchar(' '); // 选 col 更小的
return 0;
}
P3825 [NOI2017] 游戏
若没有 x
就是裸的 2-SAT。
注意到 x
的状态:a
或 c
,这保证了任何一种合法解都被考虑到。
时间复杂度
*P6965 [NEERC2016] Binary Code
一个字符串至多含有一个问号,所以状态至多有两种,考虑 2-SAT,设
容易发现,若字符串
刻画前缀关系的结构是字典树。对于若
我们还要处理
综上,点数和边数关于
3. 广义圆方树
前置知识:tarjan 求割点和边双的缩点的正确性证明(懒得再证一遍正确性了),详见 初级图论。
广义圆方树是刻画图上点的必经性的强力工具。它可以描述原图任意两点之间的所有割点,即
点双和边双的缩点方法类似,都是借助 tarjan 算法求出所有连通分量的具体形态。但是它们缩点后得到的结构形态却截然不同。这是因为一个节点最多只会出现在一个边双当中(相对应的,一条边最多属于一个点双),所以边双缩点时可以将一个边双内部所有点看成一个点。但是一个点可能出现在若干点双当中,而且为了刻画必经点,我们希望在缩点时保留这些割点(边双缩点时也保留了割边对吧)。
3.1 点双相关性质
咕咕咕,可以看例题 V. 题解,有时间再补充在这里。
3.2 算法介绍
广义圆方树是定义在 一般无向图 上的一种树型结构。它是点双缩点后的产物,可以有效解决必经性相关的题目。
点双的缩点本质上可以看成 “缩边”,就是把原图上所有对刻画必经性无用的边全部丢掉。
考察一个点双。如果我们希望将它缩成一棵树,并且树上任意两点之间的简单路径恰包含且仅包含它们之间的必经点,那么任意两点之间都必须直接相连,因为单独考察一个点双,其内部不存在必经点。但这和 “缩成一棵树” 矛盾。
为此,我们尝试建出点双的 “代表点” 并向点双内部所有点连边。容易发现这样一种菊花满足条件,因为任意两点通过中心点间接地直接相连。
这样就可以自然地引出圆方树的定义了:将原图的点视为圆点。对于原图中的每一个点双,新建 代表该点双 的方点连向点双内部所有圆点。
每个点双缩成一张菊花图,多个菊花图通过原图中的割点相连,因为 点双的分隔点是割点。类比边双缩点时,每个边双缩成一个点,多个点通过原图中的割边相连。
广义圆方树的建法只需在 tarjan 算法的基础上稍作修改即可。节点 low[v] >= dfn[u]
(回忆割点的判定法则),说明
正确性(弹出点集形成的连通分量的唯一性和极大性)可以类似边双缩点证,这里不再赘述。以下是一些注意点。
- 当原图不连通时,每个连通块处理完之后栈内会剩下一个点,即进入该连通块的点。
- 每个点双均会新建一个方点,所以需要开两倍空间存储圆方树。当图是一张菊花时,点双数量为
。
广义圆方树提供了原图上所有割点的信息,是解决必经性问题的得力助手,也是很有用的一类树形结构。代码见例题 I.
3.3 性质
我们先给出一些与点双连通性相关的引理:
引理 1:除了
本身, 在原图上的必经点为割点。
证明:考虑一个不等于
引理 2:
是 的必经点当且仅当删去 后 不连通。
证明:若删去
引理 3:若
与 均点双连通,但 不点双连通,则 是 的必经点。
证明:考虑
上述引理容易感性理解,读者应当认为它们的成立非常自然。
接下来是一些由浅到深,层层递进的圆方树相关性质,最重要且常见的性质在最后给出。
性质 1:圆点
的度数等于包含它的点双个数。
证明:根据圆方树的构建方式,显然。
性质 2:圆方树上圆方点相间。
证明:任何两个在同一点双内的点由该点双对应的方点间接相连。
性质 3:原图上直接相连的
包含于同一点双。
证明:
性质 4:圆点
是叶子当且仅当它在原图上是非割点。
证明:
若
若
性质 5:在广义圆方树上删去圆点
后剩余节点的连通性与在原图上删去 相等。
证明:因为删去
若
若
性质 6:
简单路径上的所有圆点恰好是原图 之间的所有必经点。
证明:若圆点
总结一下,这些性质无非就是在描述一个核心结论:若在圆方树上
3.4 例题
I. P5058 [ZJOI2004] 嗅探器
对原图建出广义圆方树,那么
如果不存在这样的圆点则无解,此时
时间复杂度
#include <bits/stdc++.h>
using namespace std;
const int N = 4e5 + 5;
int n, a, b, node;
int dn, dfn[N], low[N], top, stc[N];
vector<int> e[N], g[N];
void tarjan(int id) {
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]);
if(low[it] == dfn[id]) {
g[++node].push_back(id), g[id].push_back(node);
for(int x = 0; x != it; )
g[node].push_back(x = stc[top--]), g[x].push_back(node);
}
}
else low[id] = min(low[id], dfn[it]);
}
}
int fa[N], dep[N];
void dfs(int id, int ff) {
fa[id] = ff, dep[id] = dep[ff] + 1;
for(int it : g[id]) if(it != ff) dfs(it, id);
}
int main() {
cin >> n, node = n;
scanf("%d%d", &a, &b);
while(a && b) {
e[a].push_back(b), e[b].push_back(a);
scanf("%d%d", &a, &b);
}
tarjan(1), dfs(1, 0);
scanf("%d%d", &a, &b);
int ans = N;
if(dep[a] < dep[b]) swap(a, b);
while(dep[a] > dep[b]) if((a = fa[a]) != b) ans = min(ans, a);
while(a != b) ans = min(ans, min(a = fa[a], b = fa[b]));
if(ans > n) puts("No solution");
else cout << ans << "\n";
return 0;
}
II. P4630 [APIO2018] Duathlon 铁人两项
广义圆方树基础练习题。
首先进行一步转化,对所有点对
由于本题简单路径定义为不经过重复点的路径,且题目考察连通性相关,不难想到建出广义圆方树。因此相当于求
注意,路径上每个除了
如上图,从最左边的
设每个点的权值为
转换贡献方式,考察圆方树上每个节点对答案的贡献。容易通过一遍 dfs 求出子树大小的同时求解该问题。
注意原图可能不连通。
时间复杂度线性。代码。
III. P4606 [SDOI2018] 战略游戏
扣掉一个节点后
对每个点是否为圆点做树上前缀和,虚树上一条边的贡献(不含两端)容易计算。再加上所有不在
代码。
IV. P4334 [COI2007] Policija
建出广义圆方树。
对于类型 2 的询问,直接判
对于类型 1 的询问,首先判
时间复杂度线性对数,瓶颈在于求 LCA。代码。
*V. P3225 [HNOI2012] 矿场搭建
不那么套路的连通性相关题目。
题目希望我们给出一种选择关键点的方式,满足删去任何一个点后形成的每个连通块内都存在至少一个关键点。
我们发现,由于删去非割点后整张图仍连通,所以删去非割点后连通块存在关键点蕴含于删去割点后连通块存在关键点。
唯一的特例是整张图点双连通。此时从图上任意选择两点作为关键点均合法,方案数为
进行特判后,每个点双至少有一个原图上的割点。
为了解题,我们需要深入剖析广义圆方树的结构。剔除广义圆方树上所有叶子,即原图的非割点,我们得到了广义圆方树的由原图割点和点双方点构成的骨架,称为主干树。
定义一个点双的度数等于它对应的方点在主干树上的度数,等价于该点双包含的原图割点数量。
考虑一个大小为
进一步地,我们发现删去主干树上任何一个割点,形成每个连通块必然包含至少一个主干树的叶子。因此方案合法。
综上,令主干树的叶子对应点双大小分别为
时间复杂度线性。代码。
VI. CF487E Tourists
一道圆方树经典题。
问题在于修改点权时可能影响到很多方点权值,无法承受。考虑使用 只维护儿子信息 的技巧,将方点权值改为所有儿子的权值最小值,用 multiset
维护。修改圆点权值时修改其父节点的 multiset
并更新其父节点权值。
查询同样求出
时间复杂度
*VII. P8456 「SWTR-8」地地铁铁
题解。
[P8331 ZJOI2022] 简单题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
CF1763F Edge Queries
这题是不是有点裸?题目给的性质乱七八糟,完全没用。
建出圆方树,则
对于
视
4. 支配树
由于 NOI 不可能考一般有向图的支配树(考了也写不动啊),所以这玩意以后再学。
4.1 定义
对有向图
容易证明支配关系具有
- 传递性:若
支配 , 支配 ,则 支配 。 - 自反性:
支配 。 - 反对称性:若
支配 , 支配 ,则 。
这保证了支配是偏序关系,但偏序关系只能建出 DAG,不够优秀。
支配关系有一条特殊性质:若
不妨称这种性质为半完全性。其重要之处在于,任何具有半完全性的系统均可以建树:考虑支配
因此,令
在
支配树用于描述有向图在给定起点时节点的必经性,类似广义圆方树完整地描述了无向图的必经性。
4.2 DAG 支配树
有向无环图具有特殊性质,使得我们能够方便地求出其支配树。
对 DAG 拓扑排序,设当前节点为
当
当
通过上述推理,我们容易发现,若
因为拓扑序在
4.3 例题
I. P2597 [ZJOI2012] 灾难
一道经典的 DAG 支配树问题,代码。
5. DAG 链剖分
前置知识:树链剖分。
DAG 链剖分在国内信息学算法竞赛界第一次被系统阐述是在 2022 年戴江齐学长的集训队论文中。它看上去是比较新的科技,但实际上很早就有 DAG 链剖分相关的题目了(GRE Words Once More)。
5.1 算法介绍
DAG 链剖分的核心思想和树链剖分类似,都是通过 设置重儿子 的方法将图剖分成若干条链,使得图上任意路径经过的链的条数在可接受的范围内。
若原图存在若干无入度的节点,则新建源点向它们连边。因此,可以规范 DAG 的形态仅有节点
从树剖出发,考虑定义重儿子为
因此,设
DAG 链剖分常与后缀自动机结合,因为字符串
5.2 例题
*I. UOJ752 Border 的第五种求法
感觉思想挺简单的,但是是未曾设想的道路。
对
是 的前缀。 在 link 树上是 的祖先。
因为
- 如何拆重链:容易求出当前节点
到重链末尾形成的字符串在原串上的任意对应下标 。不妨设当前要匹配 ,则当前重链匹配长度即 ,容易 SA 或者 SAM 求出。
二维数点,离线后在树上扫一遍,用 BIT 维护单点修改区间查询。时间复杂度
*II. CF1098F Ж-function
提供一个比较好想的思路。
一句话题解:DAG 链剖分 + P4211 + 处理算错的贡献。
翻转
考虑上式,发现形式很像经典老题 P4211 [LNOI2014] LCA。问题在于
然后我们处理算错的贡献。因为
注意第一部分因为离线,空间复杂度必须
6.1 Dilworth 定理
CF1738G Anti-Increasing Addicts
果然还是 Anton 出的题最智慧。
初步感知问题:
-
当没有限制一些位置必须保留时,直接删去
的正方形即可满足条件。 -
题目要求不能出现保留位置不出现长度为
的链,根据 dilworth 定理,必须用不超过 个反链覆盖所有保留位置。 -
条反链至多覆盖 个位置,因为第一条反链至多覆盖 个位置,第二条反链至多覆盖 个位置。想象每次剥去正方形的一行一列。因此,反链覆盖位置是极大的,限制已经最严格。
设
根据
为了让最大化覆盖位置,不妨设第
为此,我们规定反链 能向上走就向上走。也就是说,当且仅当上方已经被覆盖或再往上走就无法覆盖某个
如上图。
只需证明第
参考资料
第二章:
- 算法学习笔记(71):2-SAT - Pecco。
- 【计算理论】计算复杂性(NP 完全问题 - 布尔可满足性问题 | 布尔可满足性问题是 NP 完全问题证明思路)- 韩曙亮。
- 合取 - 百度百科。
- 2-SAT 适定性 (Satisfiability) 问题知识点详解 - zeng_jun_yv。
第三章:
第四章:
第五章:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!