简单最短路径算法

前言

图的最短路径算法主要包括:

  • 有向无权图的单源最短路径
    • 宽度优先搜索算法(bfs)
  • 有向非负权图的单源最短路径
    • 迪杰斯特拉算法(Dijkstra)
  • 有向有权图的单源最短路径
    • 贝尔曼福特算法(Bellman-Ford)
    • 最短路径快速算法(SPFA)
  • 有向有权图的多源最短路径
    • 弗洛伊德算法(Floyd)
    • 负环
    • 约翰逊算法(Johnson)
  • 有向非负权图的单源k短路径:
    • 迪杰斯特拉算法(Dijkstra)
  • 有向非负权图的单源汇k短路径:
    • 启发式搜索算法(A*)
      事实上k短路径可以用可持久化可并堆来做,但是我不会。
  • 有向无环图的单源最短路径
    • 拓扑排序+动态规划

关于简单路径的问题:
事实上图上任意一条长度不为 − ∞ -\infty 的最短路径一定是简单路径,并且任意一条长度不为 + ∞ +\infty +的最长路径也一定是简单路径。显然。

我们把所有边的边权取反,这样原来的最长路径就成为了最短路径,原来的最短路就成为了现在的最长路。

约定

  • 对于无权图 G G G,我们认为它相当于边权全为 1 1 1的带权图。
  • 负环指的是环上所有边的边权和为负数的环,正环同理。
  • 本文所说的最短路径,并不要求路径上没有重复节点,即路径上可能存在环。
    事实上最短路径上的环一定是负环或零环,零环可以舍去,因此如果图中不存在负环,则我们认为最短路径一定是简单路径
  • 我们用 d i s u , v dis_{u,v} disu,v表示图中从 u u u v v v的最短路,记作 u u u v v v的距离
  • 单源最短路径指的是,起点为固定的一个点 S S S,终点为图中任意点的所有最短路径。
    即单源最短路径算法要求出: d i s S , u ∈ V dis_{S,u\in V} disS,uV

宽度优先搜索算法(bfs)

bfs算法求出有向无权图上的单源最短路径。

bfs算法的过程是这样的:

  • 初始, S S S与自身的距离为 0 0 0
  • 标记所有 S S S的临接点,它们与 S S S的距离为 1 1 1
  • 标记所有 f u = 1 f_u=1 fu=1的点的临接点中未被标记的点,它们与 S S S的距离为 2 2 2
  • 标记所有 f u = 2 f_u=2 fu=2的点的临接点中未被标记的点,它们与 S S S的距离为 3 3 3
  • 标记所有 f u = 3 f_u=3 fu=3的点的临接点中未被标记的点,它们与 S S S的距离为 4 4 4
  • 以此类推,直到所有点都被标记。

很容易使用归纳的方法证明bfs算法的正确性:
假设目前标记了所有 d i s S , u ≤ k dis_{S,u}\leq k disS,uk的点,并正确计算了其距离,显然所有 d i s S , u = k + 1 dis_{S,u}=k+1 disS,u=k+1的点都会在下一轮当中被扩展。

可以使用队列来维护这个过程,具体来说是:

  • 首先把 S S S加入队列,标记 S S S
  • 设目前队列的队头为 u u u,把队头的所有未被标记的临接点 v v v的距离更新, f v = f u + 1 f_v=f_u+1 fv=fu+1,然后标记这些点,把它们入队
  • 弹出队头
  • 重复这个入队出队的过程直到队列为空

时间复杂度 O ( n + m ) O(n+m) O(n+m),其中 n n n为图的点数, m m m为边数。

迪杰斯特拉算法(Dijkstra)

松弛

在一般的求解单源最短路的过程中,我们需要维护 d u d_u du表示当前已知的从 S S S u u u的最短路径长度,或称 d u d_u du为从 S S S u u u的最短路径长度的上界,即最短路径估计。初始 d S = 0 , d u ∉ S = + ∞ d_S=0,d_{u\not\in S}=+\infty dS=0,duS=+

对于图中一条从 u u u v v v的有向边,其边权为 w w w,我们知道在最终的最短路上一定满足 d i s u + w ≥ d i s v dis_u+w\geq dis_v disu+wdisv,因此如果当前的 d u + w < d v d_u+w<d_v du+w<dv,那我们可以从 u u u走这一条边来作为当前到达 v v v的最短路,说明 v v v的最短路径上界为 d u + w d_u+w du+w而不是 d v d_v dv,因此执行 d v ← d u + w d_v\leftarrow d_u+w dvdu+w

因此我们选出一条边 ( u , v ) (u,v) (u,v),尝试用 d u d_u du来更新 d v d_v dv的过程叫做松弛,如果成功更新了 d v d_v dv,那称为松弛成功,否则称为松弛失败。

松弛的过程:

	if(d[u]+w<d[v])d[v]=d[u]+w;

迪杰斯特拉算法

迪杰斯特拉算法是一种求解非负权图的单源最短路径的算法。

迪杰斯特拉算法的过程是这样的:

  • 初始所有点都未被标记
    • 选出目前 d d d最小的未被标记的点,设为 u u u
    • 标记 u u u
    • u u u的所有出边进行松弛操作
  • 直到所有点都被标记,此时 d u = d i s S , u d_u=dis_{S,u} du=disS,u

证明

引理1

归纳可以证明由于边权非负,如果把被标记的点排成一个序列,则它们的 d d d一定非严格递增。

引理2

u u u是从 S S S v v v的其中一条最短路上 v v v的前驱的充要条件是: d i s S , u + w = d i s S , v dis_{S,u}+w=dis_{S,v} disS,u+w=disS,v
w w w表示最短路上从 u u u v v v的边的边权

引理2显然。

引理3

假设 u u u是从 S S S v v v的其中一条最短路上 v v v的前驱,若对边 ( u , v ) (u,v) (u,v)进行松弛时, d u = d i s S , u d_u=dis_{S,u} du=disS,u,则松弛后 d v = d i s S , v d_v=dis_{S,v} dv=disS,v
若松弛成功,则 d v = d u + w = d i s S , u + w = d i s S , v d_v=d_u+w=dis_{S,u}+w=dis_{S,v} dv=du+w=disS,u+w=disS,v
若松弛失败,则 d v d_v dv本来就等于 d i s S , v dis_{S,v} disS,v

引理4

d i s u , x + d i s x , v ≥ d i s u , v dis_{u,x}+dis_{x,v}\geq dis_{u,v} disu,x+disx,vdisu,v

若小于,则先沿着 u , x u,x u,x的最短路走到 x x x,再沿着 x , v x,v x,v的最短路走到 v v v,会得到比 d i s u , v dis_{u,v} disu,v更短的最短路,与 d i s u , v dis_{u,v} disu,v的定义矛盾

引理5

若从 u u u v v v的某条最短路经过了 x x x,则 d i s u , x + d i s x , v = d i s u , v dis_{u,x}+dis_{x,v}=dis_{u,v} disu,x+disx,v=disu,v

根据引理4,我们知道 d i s u , x + d i s x , v ≥ d i s u , v dis_{u,x}+dis_{x,v}\geq dis_{u,v} disu,x+disx,vdisu,v,而显然 d i s u , x + d i s x , v > d i s u , v dis_{u,x}+dis_{x,v}>dis_{u,v} disu,x+disx,v>disu,v的话,不可能存在一条经过 x x x的最短路。

迪杰斯特拉算法的正确性

归纳假设之前的所有被标记的点被标记时,都满足其最短路径估计( d d d)等于其最短路径长度( d i s dis dis):
设下一次被选出的点是 v v v,则我们可以证明选出它时, d v = d i s S , v d_v=dis_{S,v} dv=disS,v
找到其中一条由 S S S v v v的最短路上 v v v的前驱 u u u

  • w > 0 w>0 w>0
    u u u一定被标记,说明 d u = d i s S , u d_u=dis_{S,u} du=disS,u松弛了 v v v,根据引理3证毕。
  • 否则 w = 0 w=0 w=0
    • 若至少存在一个前驱 u u u满足从 u u u v v v S S S v v v最短路上的边不为 0 0 0
      则是上面的情况,证毕。
    • 否则所有的 u u u v v v在最短路上的边均为 0 0 0
      • 如果存在至少一个 u u u被标记:
        根据引理3证毕。

      • 不存在任何一个 u u u被标记:
        说明 v v v尚未被任意一个最短路上的前驱更新,根据引理3可知, d v > d i s S , v d_v>dis_{S,v} dv>disS,v
        那么一定可以找到一个点 x x x,使得:

        1. x x x被标记
        2. x x x在某条从 S S S v v v的最短简单路径上
        3. x x x在此条最短路径上的后继 y y y未被标记。

        我们一定能选出一个这样的点,因为满足限制 1 , 2 1,2 1,2的点是一定有的,而如果后继 y y y不满足未被标记,说明 y y y满足限制 1 , 2 1,2 1,2,则我们可以令 x ← y x\leftarrow y xy,由于简单路径的长度至多为 n n n,因此最多跳约 n n n次就会找到一个合法的点 x x x
        在这里插入图片描述因此我们知道 d x = d i s S , x d_x=dis_{S,x} dx=disS,x,那根据引理3我们就知道 d y = d i s S , y d_y=dis_{S,y} dy=disS,y,这样我们就会知道 d y = d i s S , y ≤ d i s S , v < d v d_y=dis_{S,y}\leq dis_{S,v}<d_v dy=disS,ydisS,v<dv,即 d y < d v d_y<d_v dy<dv,但是 y y y又未被标记,不满足引理 1 1 1,矛盾。

证毕。

实现

#include<iostream>
#include<vector>
#include<queue>
#include<vector>
#include<functional>
#include<map>
using namespace std;
const int N=1e5;
vector<pair<int,int>>a[N+5];
int d[N+5];
bool vis[N+5];
void Dijk(int s) {priority_queue<pair<int,int>,vector<pair<int,int>>,greater<pair<int,int>>> q;for(auto&i:d) i=1e9;q.push({0,s});d[s]=0;while(!q.empty()){int u=q.top().second;q.pop();if(vis[u]) continue;vis[u]=true;for(auto&i:a[u]) {int v=i.second,w=i.first;if(d[v]>d[u]+w) d[v]=d[u]+w,q.push({d[v],v});}}
}
int main(){int n,m,s;cin>>n>>m>>s;for(int i=1,u,v,w;i<=m;i++){cin>>u>>v>>w;a[u].push_back({w,v});}Dijk(s);for(int i=1;i<=n;i++) cout<<d[i]<<' ';
}

迪杰斯特拉算法事实上是一种优先队列bfs算法。其和朴素bfs算法的正确性保证是一致的:

  • 按照距离 S S S从近到远的点为前驱依次松弛,必定得到最短路。
  • 由于边权非负,当前距离 d u d_u du最小的未被标记的点一定是未被标记点中 d i s S , u dis_{S,u} disS,u最小的点

因此我们可以用优先队列快速求出 d i s S , u dis_{S,u} disS,u最小的未被标记点,这样可以保证每个点的出边至多只会松弛一次,这样就有了时间复杂度保证。

如果第二点不成立,那么我们无法找到目前 d i s S , u dis_{S,u} disS,u最小的未被标记点,我们就不能保证按照标记顺序松弛一定得到最短路,为了确保正确性,就不能保证被标记的点的出边不会再次用于松弛,因此时间复杂度分析就不成立了,也就得到了SPFA算法。

贝尔曼-福特算法(Bellman-Ford)

贝尔曼福特算法和最短路径快速算法(SPFA)都是一种用来求解有权有向图单源最短路径的算法。
由于这两种算法允许负权边的存在,因此应用范围更广一些,但是它们的时间复杂度较迪杰斯特拉也更劣。

负环

若有向图 G G G中只存在非负权边,则显然 S S S到任意可达点的最短路径长度为非负实数,是一个有限值。

但是如果存在这样的图:
在这里插入图片描述
则从 S S S T T T的路径上存在一个负环,那此时最短路径不再是简单路径,我们可以绕着负环不断行走,可以使得最短路径的权值变得越来越小,因此从 S S S T T T的最短路径长度为 − ∞ -\infty

最短路径存在定理

u u u v v v存在有限长度的最短路径的充要条件是, u u u可达 v v v u u u v v v的任何一条路径上不能存在负环。

证明:
必要性很显然,只证明充分性。
u u u v v v的任一路径均无负环是 u u u到可达点 v v v有最短路的充分条件。”
考虑其逆否命题:
u u u到可达点 v v v无有限长度的最短路,是 u u u v v v的至少一条路径上有负环的充分条件。”
假设 u u u v v v没有有限长度的最短路,说明 d i s u , v = − ∞ dis_{u,v}=-\infty disu,v=,由于有限条边构成的路径一定是有限长度,因此 u u u v v v的最短路经过的点数为 + ∞ +\infty +

把最短路经过的点的编号顺次排列为 a a a,然后我们可以删掉 a a a中所有零环,这样 a a a仍然是最短路序列。
由于只有 n n n个点,由于鸽巢原理,序列的前 n + 1 n+1 n+1项中至少有两个位置是相同的,设为 a x , a y a_x,a_y ax,ay,则必然满足从 x x x沿着对应的路径走到 y y y,经过的边权和一定 < 0 <0 <0,说明有负环。

逆否命题成立,说明原命题成立。证毕。
(也可以直接证,但是我当时没想到,不想再改步骤了。)

这说明:有向图 G G G上任意有序点对要么不可达,要么存在有限长度的最短路径的充要条件是,图 G G G中没有负环。

最短路DAG与最短路径树

方便起见假设以 S S S为起点可以到达任何点。

S S S为起点,对于有向边 ( u , v ) (u,v) (u,v),如果满足 d i s S , u + w = d i s S , v dis_{S,u}+w=dis_{S,v} disS,u+w=disS,v,就在新图上给 ( u , v ) (u,v) (u,v)连有向边,这样得到一张新图。

如果原图从 S S S到任一点的路径上没有零环、负环,则显然这张图是一个DAG(有向无环图)。

我们对这张图求出一个以 S S S为根的外向生成树,称为最短路径树。
这个生成树一定能求出来的,因为新图上只有 S S S点可能入度为 0 0 0,我们可以通过归纳来得到这个结论。(具体来说,我们考虑往图上添加一个入度不为0的点,或添加一个有向边,结论仍然成立)

那么根据引理3和归纳法,我们可以知道,如果从 S S S到任意点没有负环,对最短路径树进行宽度优先遍历,在遍历到节点 u u u的同时,用它与它父亲连接的那条边对 u u u在原图上进行松弛操作,会得到 d u = d i s S , u d_u=dis_{S,u} du=disS,u

贝尔曼-福特算法

对图中的每一条边都进行一遍松弛的过程(松弛顺序无所谓),我们称为进行了一次全局松弛。

贝尔曼福特算法证明:从 S S S开始到任意可达点的无负环的充要条件是,对图进行 n − 1 n-1 n1轮全局松弛后 d u = d i s S , u d_u=dis_{S,u} du=disS,u

贝尔曼福特算法同时也给出了 S S S到任意可达点有至少一条路径经过负环的充要条件,即再进行第 n n n轮全局松弛时,有至少一个点仍被松弛。

证明:
我们假设从 S S S u u u的经过的点数最少的最短路径的点数为 k + 1 k+1 k+1,我们可以归纳证明,点 u u u最后一次被松弛是在第 k k k轮全局松弛时,并此时 d i s S , u = d u dis_{S,u}=d_u disS,u=du。因此证完。

显然贝尔曼福特算法的复杂度为 O ( n m ) O(nm) O(nm)

贝尔曼-福特算法的实现:

  • 先重复进行 n − 1 n-1 n1轮全局松弛
  • 再进行一轮全局松弛,期间观察是否有点会被再次松弛,如果有,那么报告有负环。

贝尔曼-福特算法理论上码量比SPFA要小,但是我个人感觉SPFA好写,由于二者最劣复杂度一样,因此我们学习SPFA。

最短路径快速算法(SPFA)

最短路径快速算法的英文是“Shortest Path Faster Algorithm”,从这可以看出它在随机图下表现相当优秀,经过分析可以知道它在随机图下表现的复杂度大概为 O ( m + n log ⁡ 2 n ) O(m+n\log^2n) O(m+nlog2n),但是它的最劣复杂度仍为 O ( n m ) O(nm) O(nm)

SPFA算法是贝尔曼福特算法的队列优化版本,其核心思想在于,只有前驱在第 k − 1 k-1 k1轮全局松弛中被松弛的节点,才有可能在第 k k k轮松弛中被松弛,因此我们可以用队列来模拟这个过程,具体来说是:

  • 初始把 S S S入队
  • 取出队头 u u u
  • 如果 u u u已经入队 n n n次,那么报告有负环。
  • 用队头 u u u去松弛它的所有后继,并把所有松弛成功的不在队列内的后继入队。
  • 弹出队头
  • 重复这个过程直到队列为空

注意有负环一定是入队 n n n次,而不是松弛 n n n次。因为入队 n n n次表示目前进行的是第 n n n轮全局松弛,但是松弛 n n n次并不一定。

正确性证明:
如果把所有松弛成功的点直接入队,那么这个算法的正确性是显然的。
现在考虑为什么如果一个点 v v v在队列内,那么就不需要重复入队:
假设 v v v u u u松弛成功,此时实际上是第 k k k轮全局松弛,即将入队之时发现它已经在队列内,此时有两种可能性:

  • v v v是在第 k k k轮全局松弛时入队的:
    因此 v v v在队列表示,第 k + 1 k+1 k+1轮全局松弛需要用 v v v来松弛,因此显然 v v v不需要再入队一次。
  • v v v是在第 k − 1 k-1 k1轮全局松弛时入队的:
    我们考虑按照队序顺次执行第 k k k轮松弛,把最后一个成功松弛 v v v的点设为 x x x
    • 如果 x x x队序在 v v v之后:
      那么说明 v v v已经再次入队,并表示第 k + 1 k+1 k+1轮松弛需要用到 v v v来更新,此时显然正确。
    • 否则 x x x队序在 v v v之前:
      那么由于把 v v v在第 k + 1 k+1 k+1轮入队的唯一作用就是,计算在第 k k k轮中对 v v v松弛造成的影响。而这个影响可以直接由 v v v这个位置计算,因此是可以的。

QED.

实现

#include<iostream>
#include<vector>
#include<queue>
#include<map>
using namespace std;
const int N=1e5;
vector<pair<int,int>> a[N+5];
long long d[N+5];
bool vis[N+5];
int cnt[N+5];
int n,m,s;
void Dijk(int s) {for(auto&i:d) i=1e18;d[s]=0;queue<int>q;q.push(s);while(!q.empty()) {int u=q.front();q.pop();vis[u]=false;if(++cnt[u]>=n) throw;有负环for(auto&i:a[u]) {int v=i.second;long long w=i.first;if(d[v]>d[u]+w) {d[v]=d[u]+w;if(!vis[v])q.push(v);}}}
}
int main() {cin>>n>>m>>s;for(int i=1,u,v,w; i<=m; i++) {cin>>u>>v>>w;a[u].push_back({w,v});}Dijk(s);for(int i=1; i<=n; i++) cout<<min(d[i],(1ll<<31)-1)<<' ';
}

SPFA算法有很多的优化,例如dfs-SPFA就是用栈来模拟SPFA的过程,这种做法在判断负环上似乎有着不错的表现,但是最劣情况下仍然是指数级别的。除此以外,堆优化SPFA/SLF/LLL的优化在负权图上都有可能被卡成指数级算法。

弗洛伊德算法(Floyd)

弗洛伊德算法能够以 O ( n 3 ) O(n^3) O(n3)的复杂度求出有向有权图中任意两点之间的最短路径长度,并以 O ( n 3 ) O(n^3) O(n3)的复杂度判断负环,看起来用处不大,但是有的时候还是有用的。

弗洛伊德算法实质上是一种动态规划算法。

首先设 f k , i , j f_{k,i,j} fk,i,j表示起点为 i i i,终点为 j j j的所有路径中,不包括起点和终点,经过的点编号均在 [ 1 , k ] [1,k] [1,k]之间的最短路。

初值:

  • f k , i , i = 0 f_{k,i,i}=0 fk,i,i=0
  • 如果 i i i j j j有边, f 0 , i , j = w i , j f_{0,i,j}=w_{i,j} f0,i,j=wi,j,如果有重边,选择边权最小的一条边。
  • 其余 f k , i , j = + ∞ f_{k,i,j}=+\infty fk,i,j=+

转移:
f k , i , j = min ⁡ { f k − 1 , i , j , f k − 1 , i , k + f k − 1 , k , j } f_{k,i,j}=\min\{f_{k-1,i,j},f_{k-1,i,k}+f_{k-1,k,j}\} fk,i,j=min{fk1,i,j,fk1,i,k+fk1,k,j}

注意到 f k − 1 , i , k = f k , i , k , f k − 1 , k , j = f k , k , j f_{k-1,i,k}=f_{k,i,k},f_{k-1,k,j}=f_{k,k,j} fk1,i,k=fk,i,k,fk1,k,j=fk,k,j,并且有一个转移是从 f k − 1 , i , j → f k , i , j f_{k-1,i,j}\rightarrow f_{k,i,j} fk1,i,jfk,i,j,因此可把贡献写成这个形式:
k ∈ [ 1 , n ] : f i , j min ⁡ ← f i , k + f k , j k\in[1,n]:f_{i,j}\min\leftarrow f_{i,k}+f_{k,j} k[1,n]:fi,jminfi,k+fk,j
在这里插入图片描述
时间复杂度 O ( n 3 ) O(n^3) O(n3)

因为dp结束后, f i , j f_{i,j} fi,j的意义是,从 i i i j j j经过的边数不超过 n n n的最短路径,所以图上存在负环的充要条件是,dp结束后存在 f i , i < 0 f_{i,i}<0 fi,i<0

注意,必须要先枚举 k k k,如果不先枚举 k k k是一定会错的。

负环

我们讨论几个问题:

  • 负环判断问题
  • 无穷最短路径问题
  • 负环构造问题
  • 负环计数问题

负环判断问题

S S S为起点进行SPFA,容易判断是否存在 S S S可达的负环。很多情况下我们更想知道这张图中是否存在负环,这时候我们需要建立虚拟源点,让源点向着每个点连接一个长度为 0 0 0的有向边,这时候在以源点为起点跑SPFA,就可以判断图中是否存在负环。

尤其需要注意,由于我们建立的虚拟源点,因此事实上图中一共有 n + 1 n+1 n+1个点,因此入队次数并不是为 n n n即说明有负环,而是入队次数等于 n + 1 n+1 n+1说明有负环。

因此实际跑SPFA的过程中,我们可以多跑几层bfs,例如统一当入队次数为 n + 2 n+2 n+2时再报告负环。

无穷最短路径问题

无穷最短路径问题指的是,对于以 S S S为起点,对 S S S距离其有限长度的所有点求出最短路,并求出 S S S到哪些点的最短路为 − ∞ -\infty

事实上SPFA可以部分处理图中有负环的情况:
在这里插入图片描述
假设红色为负环,虽然负环可达的点的最短路无法求出,但是由于中止算法时已经是在模拟第 n n n轮全局松弛,剩余部分的最短路在中止算法时是已经求出了的。

如果我们能够求出负环上至少一点的位置,那么我们就可以通过缩点在DAG上dp的方法来得知,哪些点是负环可达的点。

负环位置定理

事实上有以下事实:
负环所在的强连通分量的点没有有限长度的最短路。
负环可达的强连通分量的点没有有限长度的最短路。
这都是显然的。

为了方便,我们称有负环存在的连通分量为负连通分量,那么我们就会知道:
不存在两个强负连通分量互相可达。

这也是显然的,因为不存在两个强连通分量互相可达。

如果一个强负连通分量不可以被其他任何强负连通分量达到,那么称这个强负连通分量为关键强负连通分量。具体来说就是“处于可达关系最上层的强负连通分量”,关键强负连通分量意味着它可达的所有点都无法求出有限长度的最短路:
在这里插入图片描述
用红色表示负环,这张图中有两个关键强负连通分量,用绿色圈起来了。

负环位置定理:
每个关键强负连通分量上至少有一点在SPFA算法中入队 n n n次(或在贝尔曼福特算法的第 n n n轮全局松弛中仍被松弛成功。)

换句话说:
强连通分量 X X X是关键强负连通分量,是 X X X上至少有一点在SPFA算法中入队 n n n次的充分条件。(注意不是必要条件,因为入队 n n n次的点也可能是负环可达的点。)

这个定理显然成立。

如果要找到每个 S S S可达的关键强负连通分量,那么我们就不能在发现有点入队次数为 n n n时立即break,而是将入队次数为 n n n的点标记下来,然后等到发现有点入队次数为 n + 1 n+1 n+1时,在break。

时间复杂度 O ( n m ) O(nm) O(nm)

负环构造问题

在一张图中,可能存在的负环数量是指数级的,因此想要找到一个复杂度非指数级的构造所有负环的算法是不可能的。
但是我们可以找到图中的某个负环。

非简单负环一定是由负环+负环/正环/零环构成,因此存在非简单负环的充要条件是存在简单负环,因此我们只需要找简单负环。

假如说 v v v最后一次是被点 u u u松弛,那么我们认为当前最短路径树上 u u u v v v的父亲,我们可以通过使用LCT维护当前最短路径树之类的办法来求出图中的一个简单负环。
因为求出一个负环之后最短路径树就不复存在了,因此不太能做。

如果我们想要求出图中多个负环,那么注意到简单负环一定是最小环,我们可以模仿求最小环的方法求出负环。

例如使用弗洛伊德算法以 O ( n 3 ) O(n^3) O(n3)的时间复杂度求出图中的若干个简单负环。

除此之外还有一些复杂度高达 O ( n m 2 ) O(nm^2) O(nm2)之类的奇怪办法求负环,大概的思想是:

  • 枚举一条边 ( u , v ) (u,v) (u,v),假设它在负环上
  • 在去除这条边的图上跑SPFA求出 v v v u u u的最短路
  • 如果发现 v v v u u u有负环,那么说明在没有这条边的情况下图仍有负环,永久删去这条边,在新图上递归找负环
  • 否则我们就找到了包含这条边的最小环,检验一下是否为负环,如果为负环那就找到了

但是这个东西的期望时间复杂度为 O ( n m log ⁡ 2 n ) O(nm\log^2n) O(nmlog2n)

有没有其他做法呢?

  • 如果找到有限个负环,并且实际上负环足够多的话,可以直接维护最短路径树来做。
  • 如果负环不够多呢?
  • 说明负环应该不太多,可以试试直接搜
  • 不知道

负环计数问题

负环计数问题明显是强于环计数问题的,环计数问题没有公认的多项式做法,所以负环计数问题也没有。

约翰逊算法

约翰逊算法以 O ( n m + k m log ⁡ m ) O(nm+km\log m) O(nm+kmlogm)的复杂度求出有权有向图中 k k k个起点的单源最短路径,它的时间复杂度事实上是一遍SPFA加 k k k遍迪杰斯特拉。

为了方便我们假设图中没有负环,如果图中有负环,那么关键强负连通分量可达的点可以直接删去。这是一个无穷最短路径问题。

约翰逊算法的关键在于重定边权,使得图中没有负权边,因此可以跑迪杰斯特拉算法。

假设我们以 S S S为超级源点求出单源最短路径 d d d,那么对于每条边都会有 ( u , v ) (u,v) (u,v)
d u + w ≥ d v d_u+w\geq d_v du+wdv

也即: d u − d v + w ≥ 0 d_u-d_v+w\geq 0 dudv+w0
所以我们令这条边的新边权为 w ′ = d u − d v + w w'=d_u-d_v+w w=dudv+w

容易发现这样得到的新图上从 S S S T T T的路径的长度实际为: d S − d u + 原图上对应路径的长度 d_S-d_u+原图上对应路径的长度 dSdu+原图上对应路径的长度

并且这条边的边权始终非负,因此可以对每个起点都跑迪杰斯特拉。

迪杰斯特拉算法求单源k短路径

显然在堆优化迪杰斯特拉算法节点 u u u k k k次出队时,对应的是从 S S S u u u的第 k k k短路径(非严格),因此我们可以使用堆优化迪杰斯特拉算法来求出第 k k k短路,其时间复杂度为 m k log ⁡ m k mk\log {mk} mklogmk

其只能应用于有向非负权图。想要将其应用于有向有权图上,一种简单的方法是,使用约翰逊方法重定边权。

如果我们要求出严格次短路,那我们就要找到第一次,出队时对应的距离严格大于最短路径,的时间。

启发式搜索算法(A*)

启发式搜索算法通常由于快速求出从源点 S S S到汇点 T T T的第 k k k短路,其关键在于,把迪杰斯特拉算法中小顶堆比较的权值由 d u d_u du改为 d u + d i s u , T d_u+dis_{u,T} du+disu,T,也就是加入估价函数值的优先队列bfs。

由于A*算法的正确性分析,这个算法是一定对的。
该算法平常表现不错,但是其最劣复杂度仍为 O ( m k log ⁡ m ) O(mk\log m) O(mklogm),也就是会被卡。

有向无环图的单源最短路径

有向无环图的单源最短路径可以DAG上dp得到。

具体来说,设 f u f_u fu表示 S S S u u u的最短路径,则 f v = min ⁡ u → v { f u + w } f_v=\underset{u\rightarrow v}\min\{f_u+w\} fv=uvmin{fu+w}

初始 f S = 0 , f u ≠ S = + ∞ f_S=0,f_{u\not=S}=+\infty fS=0,fu=S=+

时间复杂度 O ( n + m ) O(n+m) O(n+m)

可以求出拓扑序之后dp,或者进行记忆化搜索。

后记

于是皆大欢喜。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/598011.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

全志R128 SDK架构与目录结构

R128 S2 是全志提供的一款 M33(ARM)C906(RISCV-64)HIFI5(Xtensa) 三核异构 SoC&#xff0c;同时芯片内部 SIP 有 1M SRAM、8M LSPSRAM、8M HSPSRAM 以及 16M NORFLASH。本文档作为 R128 FreeRTOS SDK 开发指南&#xff0c;旨在帮助软件开发工程师、技术支持工程师快速上手&…

Kodi 开源多媒体播放器

Kodi (原名 XBMC) 是一款经典开源免费、跨平台且极其强大专业的多媒体影音播放器&#xff0c;包含专业的影音内容管理以及解码播放功能于一体&#xff0c;提供适合在手机/电视/投影/大屏幕上显示的全屏界面&#xff0c;无线手机遥控操作方式&#xff0c;以及功能相当丰富的插件…

Selenium-java元素等待三种方式

第二种方式需要写在创建driver时的代码下面 第三种则是对每个定位元素进行配置

Mybatis之多表查询

目录 一、简介 1、使用嵌套查询: 2、使用多个 SQL 语句&#xff1a; 3、使用关联查询&#xff1a; 4、使用自定义映射查询&#xff1a; 二、业务场景 三、示例 1、一对一查询 2、一对多查询 一、简介 MyBatis 是一个优秀的持久层框架&#xff0c;它提供了强大的支持来执…

/bin/bash: cannot execute binary file

容器内部无法执行二进制文件 原因是docker镜像的 入口点不能指向/bin/bash。移除ENTRYPOINT ["/bin/bash"]就足以使其正常工作。 如果是下载的镜像&#xff0c;不能修改ENTRYPOIN&#xff0c;可以使用dockerfile覆盖掉原来的ENTRYPOINT FROM ubuntu ENTRYPOINT [ …

常见的软件架构风格

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 以下是最常见的建筑风格&#xff1a; 整体式&#xff1a;将整个应用程序构建为一个单元&#xff0c;其中所有功能和组件都从一个位置进行管理和服务。整体架构的例…

使用“反向代理服务器”的优点是什么?

反向代理服务器是一种网络架构模式&#xff0c;通常位于客户端和实际服务器之间&#xff0c;用于处理客户端请求并转发到实际服务器。以下是使用反向代理服务器的优点&#xff1a; 1.安全性&#xff1a;反向代理服务器可以提供额外的安全层。通过在反向代理服务器上配置防火墙和…

Prometheus-blackbox

一. 部署 apiVersion: v1 kind: ConfigMap metadata:name: blackbox-confignamespace: monitor data:blackbox.yml: |-modules:http_2xx: # http 检测模块 Blockbox-Exporter 中所有的探针均是以 Module 的信息进行配置prober: httptimeout: 10shttp:valid_http_versions: […

Android中的Intent

一.显式Intent 显示Intent是明确目标Activity的类名 1. 通过Intent(Context packageContext, Class<?> cls)构造方法 2.通过Intent的setComponent()方法 3.通过Intent的setClass/setClassName方法 通过Intent(Context packageContext, Class<?> cls)构造方法 通…

羊大师解读,羊奶的口味更适合哪些人群?

羊大师解读&#xff0c;羊奶的口味更适合哪些人群&#xff1f; 羊奶作为一种营养丰富的乳制品&#xff0c;拥有许多独特的品质和口味&#xff0c;备受消费者的青睐。它不仅含有丰富的蛋白质、维生素和矿物质&#xff0c;还具有更易消化的特点&#xff0c;适合许多人群的饮用。…

【KingbaseES】实现MySql函数WEEKS_BETWEEN

WEEKS_BETWEEN CREATE OR REPLACE FUNCTION weeks_between(start_date date, end_date date) RETURNS integer AS $$ BEGIN RETURN EXTRACT(WEEK FROM end_date) - EXTRACT(WEEK FROM start_date); END; $$ LANGUAGE plpgsql IMMUTABLE;结果展示

【C语言】stdbool.h——有关bool的总结

在编程和日常生活中&#xff0c;经常需要一种只能具有两个值之一的数据类型&#xff0c;如是否、开关、真假等&#xff0c;因此&#xff0c;C 有一种bool数据类型&#xff0c;称为booleans。布尔值表示 或true的值false。 C 中的 bool 是大多数语言中的基本数据类型&#xff0…

图片上传下载

数据模型: imageUrl: , <el-form-item label"楼盘图片:" prop"pic" class"uploadImg" v-model"emp.pic"> <el-upload class"avatar-uploader" …

自制java工具实现 ctrl+c+c 翻译鼠标选中文本

前言 本功能的实现基于这篇笔记 http://t.csdnimg.cn/1I8ln&#xff0c;本文阅读过程中有疑惑都可以查看此笔记 实现思路&#xff1a;检测到按压ctrl c c 后&#xff0c;获取当前剪切板文字&#xff0c;调用百度翻译api。 实现结果&#xff1a; 完整代码在最后 实现过程 1 监控…

Java中请求生成唯一追溯TraceId

Java中请求生成唯一追溯TraceId 一&#xff1a;背景 因为是微服务架构,平常日志太多,看日志不太好查,所以想要从一整个链路当中获取一个唯一标识,比较好定位问题&#xff0c; 原理就是从gateway网关将标识传递到下游,下游服务拿到这个标识,响应结束后将traceId反向写入响应体…

[论文笔记] Megtron_LM 0、报错:vscode调试无法传进去参数 launch.json文件获取args参数

解决方法&#xff1a; 配置好launch.json文件后&#xff0c;应该点运行和调试里面的运行按钮。而不是直接点文件右上角的debug。 可以看到terminal中&#xff0c;如果没有正常加载launch.json&#xff0c;则参数中没有args的参数。 如果正常加载&#xff0c;可以看到args的很多…

Java中的IO与NIO篇----第四篇

系列文章目录 文章目录 系列文章目录前言一、NIO 的非阻塞二、Channel三、Buffer四、Selector前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 一、NIO 的非阻塞 I…

C++ OpenGL 3D GameTutorial 1:Making the window with win32 API学习笔记

视频地址https://www.youtube.com/watch?vjHcz22MDPeE&listPLv8DnRaQOs5-MR-zbP1QUdq5FL0FWqVzg 一、入口函数 首先看入口函数main代码&#xff1a; #include<OGL3D/Game/OGame.h>int main() {OGame game;game.Run();return 0; } 这里交代个关于C语法的问题&#x…

释放创造力:可视化页面渲染引擎在低代码开发平台的应用

本文由葡萄城技术团队发布。转载请注明出处&#xff1a;葡萄城官网&#xff0c;葡萄城为开发者提供专业的开发工具、解决方案和服务&#xff0c;赋能开发者。 什么是页面渲染引擎? 页面渲染引擎是低代码开发平台的核心组件之一&#xff0c;它负责将开发者设计的页面布局和用户…

计算机网络学习笔记(5)——运输层

本文继续整理计算机网络体系架构知识内容。今日主讲——运输层。 网络层只把分组发送到目的主机&#xff0c;但是真正通信的并不是主机而是主机中的进程。 运输层提供了应用进程间的逻辑通信。运输层向高层用户屏蔽了下面网络层的核心细节&#xff0c;使应用程序看 见的好像在两…