最短路全家桶

最短路基础

顾名思义,最短路算法是求一个图中两点之间的最短的路径,其中路径的长度为所有边权之和。

最短路分为单源最短路和多源最短路。

单源最短路就是只有一个起点,只求由该起点到其他点的最短路径长度。下文用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次后就是最短路)

其实 dij 的优先队列可以用各种东西替代,当值域小的时候可以使用枚举值域的方式,还有枚举点的做法,总之优先队列不过是求最小值的一个工具罢了。

【补充2】有向图最小环

先加入起点 S 和 S 连到的点,这与普通 \(dij\) 一致,然后\(dis[S]\) 赋值为正无穷,当第二次取出 S 时,就是一个从 S 转一圈在回到 S 的一个环。

好了,现在单源最短路算法讲完了,该讲讲它们究竟能玩出什么花样来。(初学者可以直接跳到多源最短路)

拓展1-同余最短路

解决问题:

\(\sum_{i=1}^n a_ix_i=b\)

给定 \(n, a_{1\dots n}, r\),求出有多少 \(b\in[1,r]\) 可以使等式存在非负整数解。

重点在图论建模:

  1. (点的意义)任取一个 \(a_i\) 作为 \(a_1\) ,把 \([0, a_1)\) 中每个数作为一个点,这相当于对于任意 \(b\),把 \(b \% a_1\) 的余数当成一个点 \(p\)
  2. (距离的意义)找到最小的可以用其他 \(a_i\) 表示的 \(b\),使得 \(b \% a_1 = p\)\(b + k\times a_1(k>0)\) 一定能被表示出,而 \(b - k\times a_1(k>0)\) 一定不能被表示出。
  3. (边的意义)\(p' = (p + a_i) \% a_1\), 其中 \(1<i<n\)

统计答案:

\[ans= \sum_i(\left \lfloor (r - dis_i) / a_1 \right \rfloor + 1) [r \ge dis_i] \]

可能需要小推一下式子。

拓展2-差分约束

给出一组包含 \(m\) 个不等式,有 \(n\) 个未知数的形如:

\[\begin{cases} x_{c_1}-x_{c'_1}\leq y_1 \\x_{c_2}-x_{c'_2} \leq y_2 \\ \cdots\\ x_{c_m} - x_{c'_m}\leq y_m\end{cases} \]

的不等式组,求任意一组满足这个不等式组的解。

容易发现,这个和最短路跑出来的 dis 数组满足的条件是一样的。

具体来讲,如果有一条 u 到 v 长度为 w 的边,在跑完最短路了以后就有 dis[v] - dis[u]\(\le w\)

把这 n 个未知数当成点,连边 \((x_{c'_1},x_{c_1}, y_1)\),跑最短路,即可得到一组满足条件的最小解,无解相当于负环。

因为实际建模时 y 经常是负数,所以一般用 spfa。

拓展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短路。

最坏复杂度:\(O(knlogn)\)

核心代码:

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:找 点\(i\) 和点\(j\) 路径的中转站k

step2:比较\(dis[i][j]\)\(dis[i][k]\) + \(dis[k][j]\),然后松弛

(注意要先枚举中转站k)

我们可以将上述过程用dp来理解。

我们设 \(F_{kij}\)表示只经过前k个点,点 i 到点 j 的最短路。

显然有下列转移方程:

\[F_{kij} = \max_{1 \le k \le n}\{F_{k-1ij}, F_{k-1ik} + F_{k-1kj}\} \]

把第一维 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]);
}

复杂度 $ O(n^3) $

【拓展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算法复杂度是 \(O(n^3)\),然而dij的复杂度只是 \(O(mlogm)\)

所以对于稀疏图来说,对每个点跑dij就已经比Floyd快了。

但是dij有一个缺陷:它不能处理有负权的图,于是Johnson算法应孕而生。(我认为是这样的)

Johnson算法流程

  1. 我们设一个虚拟节点为 \(0\) ,从这个点向其他所有点连一条边权为\(0\)的边。
  2. 求出从 \(0\) 到其它点的最短路为 \(h[i]\)
  3. 对于每条边边权设为 \(w+h[u]-h[v]\)
  4. 最后再对每个点跑dij,不过 \(x\) 到点 \(y\) 的距离为 \((dis_{i->j}-h_i+h_j)\)

因为先用spfa跑了最短路,所以 \(w+h[u] >= h[v]\) 一定成立。

再考虑正确性:现有一条路径: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。这个矩阵形似链接列表,并且我们希望 \(A^k_{ij}\) 刚好是走k步后点 i 到点 j 的最短路。

现在需要通过新定义矩阵幂运算的意义来达成这一目的:

\[A^{r+m}_{ij} = \min_{1\le k\le n}{A^r_{ik}+A^m_{k,j}} \]

这个矩阵具有结合律,使用可以使用快速幂。复杂度 \(O(n^3\log n)\)

posted @ 2024-12-07 21:38  花子の水晶植轮daisuki  阅读(63)  评论(0)    收藏  举报
https://blog-static.cnblogs.com/files/zouwangblog/mouse-click.js