图论总结
应 Zikme 要求,不放代码。好像更抽象了
茅坑顿开。
0. 前言
下文中:
- \(u \to v\) 表示从 \(u\) 到 \(v\) 的单向边,\(w_{u,v}\) 为 \(u \to v\) 的边权。
- \(P(u,v)\) 表示 \(u\) 到 \(v\) 的最短路径。
- \(s\) 为源点,\(t\) 为终点。
- \(d_u\) 为 \(s\) 到 \(u\) 的最短路长度。(算法执行中)
- \(D_u\) 为 \(s\) 到 \(u\) 的最终最短路长度。
越简单的算法,证明越难想。——Zikme
1. dijkstra
1. 流程简述
算法核心是将图分为两个点集 \(S,\ T\)。其中 \(S\) 为已确定最短路点集,\(T\) 为未确定最短路点集。
算法流程:
重复以下操作,直至 \(T=\emptyset\):
-
从 \(T\) 中选取最短路长度最小的点 \(v\)。令 \(u\) 为 \(S\) 中存在 \(u \to v\) 的点中最短路长度最小的点。
-
进行松弛操作:\(d_v=\min\{d_v,d_u+w_{u,v}\}\)。
-
将 \(v\) 从 \(T\) 移至 \(S\),\(D_v=d_v\)。
2. 算法证明
显然,最短路有一个性质:对于一个最短路径,其前缀仍然是最短路。
故一定存在上述 \(u\) 点。
所以,只要证明加入 \(T\) 集合时 \(v\) 点的最短路已被确定(即 \(v\) 加入 \(S\) 时 \(D_v=d_v\)),dijkstra 算法得证。
考虑用反证法证明。
注:该证明只适用于边权非负的情况。这便是为什么 dijkstra 不能跑负权图。
设 \(u\) 为移动至 \(S\) 集合时 \(D_u<d_u\) 的点。
故当前最短路径与实际最短路径至少存在一个点不相同。
令 \(v_1,v_2\) 为当前最短路径与实际最短路径第一个不相同的点。
故 \(s\) 到 \(u\) 的当前最短路径为 \(p_1:s \to \cdots \to v_1 \to \cdots \to u\),\(s\) 到 \(u\) 的实际最短路径为 \(p_2:s \to \cdots \to v_2 \to \cdots \to u\)。
可将其简化为 \(p_1:s \to v_1 \to u,\ p_2:s\to v_2\to u\)。
显然,当 \(v_2\) 还未被更新到的时候,\(p_1\) 才能成为最短路径。
所以有 \(v_2\in T\)。
又因为 \(v_2\in p_2\) 且在 \(u\) 前,所以一定有 \(d_{v_2}<d_u\)。(注:只有在边权非负的情况下才成立。)
与 “\(d_u\) 为所有 \(p\in T\) 中 \(d_p\) 最小” 矛盾。故不存在 \(v_2\) 使得 \(p_1\) 不为最短路径。
dijkstra 算法得证。
3. 时间复杂度
暴力时间复杂度:
- 查找 \(T\) 集合中 \(d_u\) 的最小值,时间复杂度 \(O(n)\)。
- 松弛一条边,时间复杂度 \(O(1)\)。
对于图中 \(n\) 个点进行上述操作,总时间复杂度 \(O(n\times(n+1))=O(n^2)\)。
优化:
由于要查找 \(T\) 集合中 \(d_u\) 的最小值,可以使用支持修改的最值数据结构。
可选的有:
-
堆:每松弛一条边 \(u\to v\), 将 \(v\) 插入堆或修改 \(v\) 的权值,共 \(m\) 次;同时需取 \(n\) 次堆顶。时间复杂度 \(O((n+m)\log n)=O(m\log n)\)。
-
优先队列:由于优先队列不支持直接修改,故队列中有 \(m\) 个元素。总时间复杂度 \(O(m\log m)\)。
但是当数据为稠密图 (\(m\approx n^2\)) 时,上述优化时间复杂度均退化为 \(O(n^2\log n)\) 级别。还不如暴力
2. Bellman-Ford / SPFA 证明
1. 流程简述
其本质为松弛操作:\(d_u=\min\{d_u,d_v+w_{u,v}\}\)。
对图上的每一条边尝试松弛,共松弛 \(m\) 次。
意义显然。
其核心为:对于 \(P(u,v)\), 经过 \(n\) 次松弛即可保证正确性。
2. 算法证明
证明如下:
一次松弛至少可以将最短路的长度加 \(1\)。
由于最短路径不可能重复经过一条边(无负权环),最坏情况(最短路为链状)需要将 \(n\) 个点全部遍历。故 \(|P(u,v)|<n\)。
所以至多经过 \(n\) 次松弛操作即可保证正确性。得证。
综上所述,总时间复杂度为 \(O(n\times m)\)。
3. 算法优化
显然,大多数的松弛操作都是无效的。
而只有上一次被松弛过的节点(可能在最短路径中)所连接的边才可能会有有效的松弛操作。
考虑用队列维护。反正最坏情况下复杂度也为 \(O(n\times m)\)。
复杂度证明:
看不懂。不写了。反正非负权用 nm (指时间复杂度 \(O(n\times m)\)) 的 spfa。
至于怎么让这玩意死透,@changwenxuan 已经进行了详尽的研究。
干点正事。
3. Floyd 证明
1. 流程简述
设 \(g_{u,v}\) 为 \(u\to v\) 的最短路。
有转移方程 \(g_{u,v}=\min\{g_{u,v},g_{u,k}+g_{k,v}\}\)。
2. 算法证明
其原始方程为 \(g_{k,u,v}=\min\{g_{k-1,u,v},g_{k-1,u,k}+g_{k-1,k,v}\}\),表示只经过前 \(k\) 个点,\(u\to v\) 的最短路。
时间复杂度 \(O(n^3)\),空间复杂度 \(O(n^2)\)。
试证明第一维可以直接省略。
显然有 \(f_{k-1,u,v}=f_{k,u,v}\)。
如果没有,图上就有负环。最短路什么的也无所谓了。故可以将第一维直接省略。
4. Common Tricks
1. 最短路树
由于最短路的前缀也一定是最短路,所以对于单源最短路,其构成一棵有根叶向树。
主要用处是统计方案数和 dp。
2. 虚点
对于某些具有相同性质的点,可以另建一个点并将其与具有相同性质的点连边。
相当于将具有相同性质的点缩为一个点集,大幅减少边数及点数。
3. 分层图
运用 dp 思想,将不同决策导致状态不同的点装进不同的图内。
虽然一般来说这样的决策只能有一个。不然边数爆炸。
在大多数情况下等价于直接在图上进行多维 dp。
虽然我觉得建图要好看一些。
例题大赏
题目不按任何顺序排序。
[SDOI2010] 大陆争霸
第一眼看上去好像是最短路加了个强制拓扑。
也就是说当结界还没被破坏的时候,已经到达的机器人只能干等着。
在 dijkstra 中,机器人所在的点可以更新最短路。但拓扑图上该点的入度不为 \(0\),即结界产生器没有被全部破坏时,不能入队。
当炸掉一个结界产生器的时候,可以将其在拓扑图上连接的所有点的入度减一。
而当拓扑图上的一个点入度为 \(0\),即结界产生器全部被破坏的时候,便可以入队。
不用考虑当前是否有机器人在该点等待。即使没有,由于题目保证有解,所以若该点在 \(1\to n\) 的最短路上一定可达。之后也会有更优决策来替代它。
爆改 dijkstra。需要对算法流程有一定的理解。
CF1648E
在一带权无向图上,每次只能连续经过两条边,且代价为两条边边权之和的平方。求单源最短路。
数据范围 \(1\leq n\leq10^5,\ 1\leq m \leq 2\times10^5,\ 1\leq w\leq50\)。
暴力建图的话总边数为 \(m^2\),会爆炸。
考虑一个路径 \(u\to x\to v\),\(w_1=w_{u,x},\ w_2=w_{x,v}\)。
由于一次移动至少要经过两条边,所以上述代价和等价于将边权全部移到 \(x\to v\) 上。
这样的话 \(u\to x\) 就没那么重要了。在 \(w_2\) 一定的情况下共有 \(w=50\) 种可能的权值。
可以考虑以 \(w_1\) 的值构造分层图。
令 \((w,u)\) 为入边边权为 \(w\) 且入点为 \(u\) 的点集。但是由于其并不影响最终结果,可以将其作为一个点。
则对于点对 \((u,x,v)\),可以将 \(u\) 向 \((w_1,u)\) 连一条边权为 \(0\) 的边;将 \((w_1,u)\) 向 \(v\) 连一条边权为 \((w_1+w_2)^2\) 的边。
显然 \(u\to v\) 的代价等价于原图。
显然,对于一个点 \(u\)(假定其为路径的起始点),分层后其最大出度为 \(w=50\)。
总边数从 \(m^2\) 骤降到了 \(mw\)。
总时间复杂度为 \(O(m\log m)\) 左右。
分层图的各种妙妙玩法。
没思路时可以考虑转移边权或点权。
例如缩点,点权转边权等。
[HAOI2010] 道路
不管怎么说,双倍经验。
题意很简洁了。
对于每个源点 \(s\),先跑一遍 dijkstra。显然,若满足 \(dis_v=dis_u+w_{u,v}\),则 \(e(u,v)\) 一定在最短路上。
显然在 \(w_{u,v}>0\) 时,不存在 \(u,v\) 使得 \(dis_u=dis_v+w_{u,v} \wedge dis_v=dis_u+w_{u,v}\)。
因此,若将最短路径上的点从原图中取出,加入集合 \(V\),其构成的图一定为 DAG \(G(V,E)\)。
可以考虑将其进行拓扑排序。
其实是可以在 dijkstra 中排序的。dijkstra 的遍历顺序就是拓扑序。但是如果你硬要再写一遍也没人拦你
然后捏?
图上 dp。
设 \(G(V,E)\) 的源点(集)为 \(s'\),汇点(集)为 \(t'\)。
对于任意 \(E_i=e(u,v)\),设 \(out_i\) 为 \(s'\to u\) 的方案数,\(in_i\) 为 \(v\to t'\) 的方案数。
根据乘法原理,得 \(ans_i=in_i\times out_i\)。
\(out_i\) 好求。转移方程为 \(out_u=out_u+out_v[dis_u+w_{u,v}=dis_v]\ \ e(u,v)\in E\)。
事实上 \(in_i\) 和 \(out_i\) 的求法是一样的。
将 \(v\to t'\) 这部分建个反图 \(G'(V',E')\) 就可以达到同样的效果。
转移方程为 \(in_u=in_u+in_v[dis_v+w_{u,v}=dis_u]\ \ e(u,v)\in E'\)。
在 DAG 上算方案数有两种基本策略:
- 按照拓扑序 dp。
- 对于每个点 / 边分类讨论后,运用组合数学求方案数。
[NOIP2017] 逛公园
看到 \(k=50\),考虑按路径长度为一维度 dp。
设 \(f_{u,k}\) 为 从 \(1\) 到 \(u\) 路径长度为 \(d_u+k\) 的总方案数。
需要以倒拓扑序的顺序进行转移。
对于图上的每条边 \(e(u,v)\),有转移方程:
\(f_{u,k}=\sum\limits_{e(u,v)}f_{v,k-(d_u + w - d_v)} \ (d_u+w-d_v<k)\)。
其中 \(d_u+w_{u,v}-d_v\) 为较 \(d_v\) 经过 \(e(u,v)\) 多走的路程。
考虑按路径长度进行记忆化搜索求解。
具体实现可以建反图后搜索。
注:在搜索过程中如果遇见 \(0\) 环,则存在无穷种路径,输出 \(-1\)。可以通过维护每个点在当前路径长度被搜索到的次数判环。
若题目对某一条件存在限制,可以考虑将条件列入 dp 方程中求解。
[GZOI / GXOI2019] 旅行者
如果已知分组方案,对两组分别建立虚拟原点后跑最短路即可。
主要问题是如何分组。
随机化算法 shuffle 战神
设答案源点为 \(u\),汇点为 \(v\)。
将 \(K\) 分为大小相同的两组 \(K_1,K_2\),\(u\in K_1\wedge v\in K_2\) 的概率为 \(\frac 1 4\)。
故尝试 \(T\) 次全错的概率为 \(1-(\frac 3 4)^T\)。
尝试 \(20\) 次左右就行了。正确率为 \(99.68\%\)。
upd: NKOJ过不了。
二进制分组
按照 \(K_i\) 二进制上的每一位分组。
正确性证明:
显然源点与汇点是不同的点。故其至少有一个二进制位不同。将其按每一位的二进制分组,至少有一次源汇点会被分在不同组。
upd: NKOJ也过不了。
upd2: 没判 -1 导致的。
时间复杂度 \(O(m\log^2n)\)。
果国的奇妙旅行
下文中令 \(d_u\) 为点 \(u\) 的度数。
如题,求最优策略下的期望购票数。
期望:若有连续随机变量 \(x\),其期望 \(\mu_x=\sum\limits_{x\in D}x\times p_x\)。
其中 \(D\) 为定义域,\(p_x\) 为 \(x\) 出现的概率。
由于结果确定,通常期望 dp 采用倒推的方式。
设 \(f_u\) 为 \(u\) 到 \(n\) 的期望购票数。
将决策分开考虑。对于每个在点 \(u\) 抽到了去点 \(v\) 的票,有两种决策:移至 \(v\) 点或留在原地。
这样的状态设计可以让实际决策形成一个 DAG。
显然,若有 \(f_v\geq f_u\),最优决策为留在原地,期望为 \(\frac {f_u}{d_u}+1\)。
反之则可以移至点 \(v\),期望为 \(\frac {f_v} {d_u}+1\)。
故有转移方程
写起来挺好看的。但是在 \(f_u\) 的递推式中出现了自己。那就不好玩了。
不能用正常顺序求解。
注意到一个性质:\(\forall e(u,v):\min f_u\) 一定是由 \(\min f_v\) 转移过来的。
感觉和 dijkstra 有点像。考虑在 dijkstra 的过程中维护 \(f_u\)。
可以考虑把 \(f_u\) 替换成其他的可维护变量。
然后发现好像 \(f_u>f_v\) 的数量是可以维护的。顺便把 \(\sum\limits_{f_u>f_v}f_v\) 的值维护了。
每次选出当前已知的最小 \(f_v\)。对于每条边 \(e(u,v)\), 令 \(l_u\) 为满足 \(f_u>f_v\) 的数量,\(s_u\) 为满足 \(f_u>f_v\) 的 \(f_v\) 总和。
将上述方程化简得:
解得:
注:可以将 \(f_u\) 初始化为 \(\infin\)。~~如果后面有解的话,这反正也不是最优解。和 \(dis_u=\infin\) 一个意思。
优雅。这样就将 \(f_u\) 这个无法维护的量转为了 \(d_u\) 与 \(l_u\),\(s_u\) 三个可以维护的量。
图上期望 dp 的思路为:
- 确定最优策略
- 倒序 dp
- 若方程存在自环,可以将方程拆开后根据可维护变量进行转移。
[NKOJ8736] 指针分析
我也不知道这玩意为什么要放到最短路专题里。
将字母压位后 模 拟 即 可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】