图的最短路径算法主要包括:
关于简单路径的问题:
事实上图上任意一条长度不为 − ∞ -\infty −∞的最短路径一定是简单路径,并且任意一条长度不为 + ∞ +\infty +∞的最长路径也一定是简单路径。显然。
我们把所有边的边权取反,这样原来的最长路径就成为了最短路径,原来的最短路就成为了现在的最长路。
bfs算法求出有向无权图上的单源最短路径。
bfs算法的过程是这样的:
很容易使用归纳的方法证明bfs算法的正确性:
假设目前标记了所有 d i s S , u ≤ k dis_{S,u}\leq k disS,u≤k的点,并正确计算了其距离,显然所有 d i s S , u = k + 1 dis_{S,u}=k+1 disS,u=k+1的点都会在下一轮当中被扩展。
可以使用队列来维护这个过程,具体来说是:
时间复杂度 O ( n + m ) O(n+m) O(n+m),其中 n n n为图的点数, m m m为边数。
在一般的求解单源最短路的过程中,我们需要维护 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,du∈S=+∞
对于图中一条从 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+w≥disv,因此如果当前的 d u + w < d v d_u+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是从 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显然。
假设 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
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,v≥disu,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的定义矛盾
若从 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,v≥disu,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
如果存在至少一个 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 , 2 1,2 1,2的点是一定有的,而如果后继 y y y不满足未被标记,说明 y y y满足限制 1 , 2 1,2 1,2,则我们可以令 x ← y x\leftarrow y x←y,由于简单路径的长度至多为 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}
证毕。
#include
#include
#include
#include
#include
#include
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算法的正确性保证是一致的:
因此我们可以用优先队列快速求出 d i s S , u dis_{S,u} disS,u最小的未被标记点,这样可以保证每个点的出边至多只会松弛一次,这样就有了时间复杂度保证。
如果第二点不成立,那么我们无法找到目前 d i s S , u dis_{S,u} disS,u最小的未被标记点,我们就不能保证按照标记顺序松弛一定得到最短路,为了确保正确性,就不能保证被标记的点的出边不会再次用于松弛,因此时间复杂度分析就不成立了,也就得到了SPFA算法。
贝尔曼福特算法和最短路径快速算法(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中没有负环。
方便起见假设以 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 n−1轮全局松弛后 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)
贝尔曼-福特算法的实现:
贝尔曼-福特算法理论上码量比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 k−1轮全局松弛中被松弛的节点,才有可能在第 k k k轮松弛中被松弛,因此我们可以用队列来模拟这个过程,具体来说是:
注意有负环一定是入队 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轮全局松弛,即将入队之时发现它已经在队列内,此时有两种可能性:
QED.
#include
#include
#include
#include
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的优化在负权图上都有可能被卡成指数级算法。
弗洛伊德算法能够以 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 , 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{fk−1,i,j,fk−1,i,k+fk−1,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} fk−1,i,k=fk,i,k,fk−1,k,j=fk,k,j,并且有一个转移是从 f k − 1 , i , j → f k , i , j f_{k-1,i,j}\rightarrow f_{k,i,j} fk−1,i,j→fk,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,jmin←fi,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)之类的奇怪办法求负环,大概的思想是:
但是这个东西的期望时间复杂度为 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+w≥dv
也即: d u − d v + w ≥ 0 d_u-d_v+w\geq 0 du−dv+w≥0
所以我们令这条边的新边权为 w ′ = d u − d v + w w'=d_u-d_v+w w′=du−dv+w
容易发现这样得到的新图上从 S S S到 T T T的路径的长度实际为: d S − d u + 原图上对应路径的长度 d_S-d_u+原图上对应路径的长度 dS−du+原图上对应路径的长度
并且这条边的边权始终非负,因此可以对每个起点都跑迪杰斯特拉。
显然在堆优化迪杰斯特拉算法节点 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。
其只能应用于有向非负权图。想要将其应用于有向有权图上,一种简单的方法是,使用约翰逊方法重定边权。
如果我们要求出严格次短路,那我们就要找到第一次,出队时对应的距离严格大于最短路径,的时间。
启发式搜索算法通常由于快速求出从源点 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=u→vmin{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,或者进行记忆化搜索。
于是皆大欢喜。