最短路全家桶
最短路基础
顾名思义,最短路算法是求一个图中两点之间的最短的路径,其中路径的长度为所有边权之和。
最短路分为单源最短路和多源最短路。
单源最短路就是只有一个起点,只求由该起点到其他点的最短路径长度。下文用dis[x]表示起点到x的最短路。
多源最短路就是你要求任意两点之间的最短路径长度。下文用dis[u][v]表示u到v的最短路。
前言:
图中若有负环,则最短路的概念不成立(因为可以沿着负环一直走无数圈,路径最短为负无穷)。
所以一般我们讨论的图是无负环的。
此外,某些算法(如spfa)可以判断图中是否存在负环。
单源最短路算法
bfs
局限性:该算法只能处理边权为1的图。
做法就是维护一个普通的队列,遍历每一个没被标记的点。标记遍历过的点。
具体来讲,如果有边u->v,并且v没被标记,那么 dis[v] = dis[u] + 1;
每一个点第一次被遍历到的步数,一定是就是起点到该店的最短距离。
复杂度是O(n+m)的。
【拓展】01bfs
若该图中的边权为0或1,可以使用双端队列来实现。
具体来讲,若当前点u连向了v,并且边权为0,那么把v放到队首,如果边权为1,那么放到队尾。
原因:先遍历权值为0的边不会增加距离,并且只有先遍历边权为0的边才能保证第一次遍历到某个点时的路径长度刚好就是最短路。
Bellman-Ford
注:该算法可以解决有边权的最短路,但其时间复杂度很高,所以几乎用不到,但该算法可以帮助理解其他单源最短路算法。
前置知识:松弛
如果有一条边u到v,边权为w,但是目前dis[u] + w < dis[v],这时候显然不优,因为dis[v]可以是dis[u] + w,小于原来的dis[v]。
所以我们把dis[v]暂时更新成dis[u]+w,这个操作就是松弛。
代码:
if(dis[u] + w < dis[v]) dis[v] = dis[u] + w;
正文
我们对每一条边都进行松弛,称为一轮。重复n-1轮。
可以证明现在每个点的dis值已经是到起点s的最短路了。
证明比较复杂。
点击查看证明
``` 首先指出,图的任意一条最短路径既不能包含负权回路,也不会包含正权回路,因此它最多包含|v|-1条边。 其次,从源点s可达的所有顶点如果 存在最短路径,则这些最短路径构成一个以s为根的最短路径树。Bellman-Ford算法的迭代松弛操作,实际上就是按每个点实际的最短路径[虽然我们还不知道,但它一定存在]的层次,逐层生成这棵最短路径树的过程。 注意,每一次遍历,都可以从前一次遍历的基础上,找到此次遍历的部分点的单源最短路径。如:这是第i次遍历,那么,通过数学归纳法,若前面单源最短路径层次为1~(i-1)的点全部已经得到,而单源最短路径层次为i的点,必定可由单源最短路径层次为i-1的点集得到,从而在下一次遍历中充当前一次的点集,如此往复迭代,[v]-1次后,若无负权回路,则我们已经达到了所需的目的--得到每个点的单源最短路径。[注意:这棵树的每一次更新,可以将其中的某一个子树接到另一个点下] 反之,可证,若存在负权回路,第[v]次遍历一定存在更新,因为负权回路的环中,必定存在一个"断点",可用数学手段证明。 最后,我们在第[v]次更新中若没有新的松弛,则输出结果,若依然存在松弛,则输出'CAN'T'表示无解。同时,我们还可以通过"断点"找到负权回路。 ```代码:
memset(0x3f, dis, sizeof dis); dis[s] = 0;
for(int i = 1;i<=n-1;i++)
for(int j = 1;j<=m;j++)
if(dis[v[j]] > dis[u[j]] + w[j])
dis[v[j]] = dis[u[j]] + w[j];
复杂度:O(NM)
spfa
spfa实际上就是对Bellman-Ford的一个队列优化。
容易发现,只有被松弛过的点(dis被更新的点),才会在下一轮中进行松弛。
维护一个队列,维护一个vis数组来记录元素是否在队列中。
对于每一个队列中的元素x,松弛所有连向的y,如果y不在队列中,将y入队。
因为spfa不能保证第一次进队就是最优解,所以spfa的每个点是可以反复进队的。
[拓展] spfa判负环 (很重要!)
解法:
可以再建立一个0号结点,我们称它为“虚拟源点”。
把它向所有节点连一条边权为0的边,然后从0号点向其他点跑最短路,在一开始就可以将所有点入队列,通过所有结点来更新,这样再用上面两种方式都可以判定出负环。
直接上结论:如果一个点松弛次数超过n此,那么一定有负环。
原因:如果没有负环,spfa最多跑n层(从最短路树的角度理解,每一次都会多更新一层),每个点最多被松弛n次。
后话:spfa在随机数据下跑得很快,但是最坏复杂度高达O(MN),与Bellman-Ford复杂度相同,很容易被出题人卡掉。
spfa在费用流中也有用到,但这不是本文的重点。
dijstra(很重要!)
个人最爱用的算法
局限性:不能处理有负边权的图。(正常来说也遇不着)
我们想办法让每个点在第一次被访问到时就是最短距离。
我们只松弛当前队列中dis最小的点u所连的边,并且只把松弛了的点加入队列。
确定了dis[u]就是起点到u的最短路。
每次确定的dis值只会越来越大。(这点可以类比bfs)
证明:如果每次取出的u的dis[u]还不是最小值,也就是说dis[u]还会被其他节点松弛。(也就是说最小值不会被其他节点更新)
但是队列中的其他元素的dis值都已经比dis[u]大了,而且每条边边权都是正数。
所以不可能有节点的dis值加上边权后反而比dis[u]小,所以dis[u]不会被更新。
至于怎么求队列中的最小dis,考虑c++自带的优先队列,它可以保证队首的元素值最小。
点击查看代码
#define mp make_pair
#define pii pair<int,int>
const int N = 2e5+5;
long long dis[N]; bool vis[N]; //注意:这里的vis与spfa的vis不同,这里vis表示该点的dis值是否被确定过。
vector<pii> edge[N];
void dijkstra(){
priority_queue<pii, vector<pii>, greater<pii> > q;
for(int i=1;i<=n;++i) dis[i] = ((1<<31) - 1);
q.push(mp(0,s));dis[s] = 0;
while(!q.empty()){
int x = q.top().second;
q.pop();
if(vis[x]) continue;
vis[x] = 1;
for(auto y : edge[x]){
to = y.first,w = y.second;
if(dis[x] + w < dis[to]) {
dis[to] = w + dis[x];
q.push(mp(dis[to],to));
}
}
}
}
因为优先队列每次操作是带log的,所以最终复杂度为:O((n+m)logm)
【补充1】O(n^2)dij
再补充一个O(n^2)的dij,用于稠密图。
简单来说就是O(n)地去遍历数组来找dis最小的数,再松弛它所连的边。将以上步骤重复n-1次即可。(可证明重复n-1次后就是最短路)
【补充2】有向图最小环
先加入起点 S 和 S 连到的点,这与普通
好了,现在单源最短路算法讲完了,该讲讲它们究竟能玩出什么花样来。(初学者可以直接跳到多源最短路)
拓展1-同余最短路
解决问题:
给定
重点在图论建模:
- (点的意义)把
% (任取一个就行) 的每一个余数当成一个点 。 - (距离的意义)找到最小的可以用其他
表示的 ,使得 。对于点p,p+kx(k>0) 一定能被表示出,而p-kx(k>0)一定不能被表示出。 - (边的意义)
, 其中 。
统计答案:
可能需要小推一下式子。
拓展2-最短路图
type1 : 给定一张有向图,起点s,终点t
求s到t的所有最短路组成的DAG(没有负环的最短路图一定是DAG)
首先需要建一张正向图,一张反向图
dis1[] 表示正向图上点s到所有点的最短距离,dis2[]表示反向图上点t到所有点的最短距离
考虑正向图上的一条边(edge){u,v,w}
如何判断这条边需不需要加入最短路图呢?
很显然只需要满足
dis1[u]+w+dis2[v]==dis1[t] 就ok
定理:
i→j 的最短路径的任意一条子路径 u→v,都是最短路径。
type2 :只有源点s。判断一条边 u→v 是否在最短路图中,只需判断是否 dis[u]+val(u→v)==dis[v]。
证明显然。
例题
题解:
因为n^2能过,所以可以对于每一个S统计边的经过次数。
具体来说,对于每一个S建一张tpye2的最短路图,对于每一条从u到v的边,我们需要统计s->u的路径数cnt1,和v->T的路径数cnt2(T是任意一点)。
topo序的条件是没有环,刚好最短路图就没有环。
s->u就是这个题,直接正着按topo序dp计数就行。(补充:显然,最短路图中没有u->s的路经,所以以S为起点开始topo,cnt1[s]=1)
v->T则需要倒着走topo序。
统计贡献。对于在最短路图上的一条边u→v,贡献为cnt1[u]∗cnt2[v]。
拓展3-k短路A*做法:
A*:按照原来的权值val和预计还要花费的代价g(也就是所谓的估价函数)从大到小排序,依次搜索。
也就是节点按照 val[u] + g[u] 排序后搜索。
优缺点:能有效剪枝,但最坏复杂度一般不会改变。算法的速度取决于估价函数设计得好不好,也就是要让g尽量接近真实值。
使用条件:g小于等于实际所需的代价。
可以这样理解:如果估价函数设的太大,可能会导致把正解排序排到很后面。而就算估价小于实际,随着搜索的深入,实际的val会无限逼近真实值。
在求k短路中,实际代价val就是每条路径实际走过距离,把g设为目前节点到终点的最短路(可以预处理出)。还是用优先队列排序。
在之前的dij算法中,每个点最多只会访问一次,而在k短路中,每个点最多会被访问k次。
可以证明,第i次访问到节点i的路径长度 等于 到该节点第i短的路径。
感性理解:dij保证第一次访问时是最短路,那么去掉这一条路后,剩下的节点还是按照到该点的距离排序的,所以第二次到就是次短路,第k次是k短路。
最坏复杂度:
核心代码:
void kthdij(int s, int t) {
priority_queue<pii, vector<pii>, greater<pii> > q;
for(int i = 1; i <= n; ++i) vis[i] = 0;
q.push(mp(dis[s], s));
while(!q.empty()) {
int x = q.top().second; int d = q.top().first; q.pop();
if(x == t) {
ans[++ vis[t]] = d;// - dis[t];
if(vis[t] == k) return ;
}
else {if(vis[x] >= k) continue; ++ vis[x];}
for(auto way : edge[x]) if(vis[way.v] < k) {//if(len[way.v] > len[x] + way.w)
q.push(mp(d + way.w - dis[x] + dis[way.v], way.v));
}
}
return ;
}
多源最短路
Floyd
总体思路:
step1:找 点
step2:比较
(注意要先枚举中转站k)
我们可以将上述过程用dp来理解。
我们设
显然有下列转移方程:
把第一维 k 滚掉就是 dis 数组了。
void Floyd(){
for(int k=1;k<=n;++k)
for(int i=1;i<=n;++i)
for(int j=1;j<=n;++j)
dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
复杂度
【拓展1】Floyd求传递闭包:
要求出每一个点u能到达的所有点。设bool数组f[i][j]表示点i是否可以到点j。
只需把松弛操作改为:f[i][j] = f[i][j] or (f[i][k] and f[k][j])即可。
bitset优化:
将上式改写为
如果 f[i][k] 为真, f[i][j] = f[i][j] or f[k][j]
那么, f[i][j] = f[i][j]
献上代码:
for (int k = 1; k <= n; ++k)
for (int i = 1; i <= n; ++i)
if (f[i][k]) f[i] |= f[k];
例题
【拓展2】Floyd找环:
如果dis[j][i]之前被更新过,然后又找到了一个中转站k,那么存在一个i->...->k->...->j->...->i的环。
【拓展3】可以动态地去添加中转站k 例题。
该题中的点是逐个加入的,显然,在添加该点之前是不能把该点作为中转站,而添加之后就要考虑以该点为中转站的情况去更新其他点的最短路。
更新操作具体来说就是用新加进来的点再跑一边里面的两层循环。
Johnson多源带负权最短路
Floyd算法复杂度是
所以对于稀疏图来说,对每个点跑dij就已经比Floyd快了。
但是dij有一个缺陷:它不能处理有负权的图,于是Johnson算法应孕而生。(我认为是这样的)
Johnson算法流程:
- 我们设一个虚拟节点为
,从这个点向其他所有点连一条边权为 的边。 - 求出从
到其它点的最短路为 。 - 对于每条边边权设为
。 - 最后再对每个点跑dij,不过
到点 的距离为
因为先用spfa跑了最短路,所以
再考虑正确性:现有一条路径:a->b->c
计算得答案为 disa->b + h[a] - h[b] + disb->c + h[b] - h[c] == disa->b + disb->c + h[a] - h[c]
容易发现路径上的h值都抵消掉了,只剩下了头和尾,而头和尾的h值是确定的,所以原来的最短路和处理后的最短路是同一条(至少处理后值一样)。
【补充】求刚好走了 k 步后,任意两点之间的最短路
我们构建一个广义矩阵 A。这个矩阵形似链接列表,并且我们希望
现在需要通过新定义矩阵幂运算的意义来达成这一目的:
这个矩阵具有结合律,使用可以使用快速幂。复杂度
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 在鹅厂做java开发是什么体验
· 百万级群聊的设计实践
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
· 永远不要相信用户的输入:从 SQL 注入攻防看输入验证的重要性
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析