最短路算法

Bellman-Ford算法

适用范围:单源最短路,无负环,可以有负权。

记从起点 s s s出发到节点 i i i的最短距离为 d i d_i di,则下述等式成立。
d i = min ⁡ e = ( j , i ) ∈ E { d j + l e n ( j , i ) } d_i = \min_{e = (j, i)\in E}\{d_j + len(j, i)\} di=e=(j,i)Emin{dj+len(j,i)}
设初值 d s = 0 d_s = 0 ds=0 d i = inf ⁡ d_i = \inf di=inf,不断利用这条递推关系式更新 d d d的值,就可以算出新的 d d d

只要原图中不存在负环,更新的次数就是有限的。

时间复杂度: O ( ∣ V ∣ × ∣ E ∣ ) O(|V| \times|E|) O(V×E)

空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

代码
int n, m, s;
LL d[N];
vector<Edge> e[N];

void Bellman_Ford(){
    rep1(i, n + 1) d[i] = inf;
    d[s] = 0;
    while (true){
        bool update = false;
        rep1(i, n + 1) if (d[i] != inf){
            for (Edge u : e[i]) if (d[i] + u.len < d[u.to]){
                d[u.to] = d[i] + u.len;
                update = true;
            }
        }
        if (!update) break;
    }
}
判负环

由于最短路最多只会经过 ∣ V ∣ − 1 |V| - 1 V1条边,所以上述更新最多进行 ∣ V ∣ − 1 |V| - 1 V1轮。

因此如果在第 ∣ V ∣ |V| V轮依然更新了 d d d的话,就说明存在负环。

初始化时将所有的 d i d_i di赋为 0 0 0,据说这样可以求出所有负环(然而我还不会qwq)。

代码
int n, m, d[N];
vector<Edge> e[N];

bool find_negative_loop(){
    rep1(i, n + 1) d[i] = 0;
    rep(cnt, n){
        bool update = false;
        rep1(i, n + 1) for (Edge u : e[i]) if (d[i] + u.len < d[u.to]){
            d[u.to] = d[i] + u.len;
            update = true;
            if (cnt == n - 1) return true;
        }
        if (!update) break;
    }
    return false;
}
SPFA

很容易看出只有被松弛的点才有可能去松弛其他点,所以用一个队列存储一下哪些点被松弛了。

int n, m, s;
LL d[N];
vector<Edge> e[N];

bool in_queue[N];
void SPFA(){
    rep1(i, n + 1) d[i] = inf, in_queue[i] = false;
    d[s] = 0;
    queue<int> que;
    que.push(s);
    in_queue[s] = true;
    while (!que.empty()){
        int p = que.front(); que.pop(); in_queue[p] = false;
        for (Edge u : e[p]) if (d[p] + u.len < d[u.to]){
            d[u.to] = d[p] + u.len;
            if (!in_queue[u.to]){
                que.push(u.to);
                in_queue[u.to] = true;
            }
        }
    }
}

Dijkstra算法

D i j k s t r a Dijkstra Dijkstra适用于不存在负权边的情况。可以对算法进行如下修改。

  1. 对于找到最短距离的点,从它出发更新相邻节点的距离。
  2. 此后不需要再关心已经确定最短距离的点。

使用 p r i o r i t y _ q u e u e priority\_queue priority_queue优化,每次取出当前未确定最短距离的点中最近的点,更新从它出发的相邻节点。

时间复杂度: O ( ∣ E ∣ log ⁡ ∣ V ∣ ) O(|E|\log|V|) O(ElogV)

空间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V| + |E|) O(V+E)

代码
int n, m, s;
LL d[N];
vector<Edge> e[N];

void Dijkstra(){
    rep1(i, n + 1) d[i] = inf;
    d[s] = 0;
    priority_queue<pair<LL, int> > pq;
    pq.push(MP(-d[s], s));
    while (!pq.empty()){
        int cur = pq.top().second;
        LL curdis = -pq.top().first;
        pq.pop();
        if (curdis > d[cur]) continue;
        for (Edge u : e[cur]) if (curdis + u.len < d[u.to]){
            d[u.to] = curdis + u.len;
            pq.push(MP(-d[u.to], u.to));
        }
    }
}

Floyd-Warshall算法

F l o y d Floyd Floyd用于求任意两点之间的最短路。

d p ( k , i , j ) dp(k, i, j) dp(k,i,j)表示只经过点 1 ∼ k 1\sim k 1k i , j i, j i,j的情况下 i i i j j j的最短距离,初始 d p ( 0 , i , j ) = l e n ( i , j ) dp(0, i, j) = len(i, j) dp(0,i,j)=len(i,j),如果不存在从 i i i j j j的边则 d p ( 0 , i , j ) = inf ⁡ dp(0, i, j) = \inf dp(0,i,j)=inf

分两种情况讨论。

  1. 最短路不经过 k k k,则 d p ( k , i , j ) = d p ( k − 1 , i , j ) dp(k, i, j) = dp(k - 1, i, j) dp(k,i,j)=dp(k1,i,j)
  2. 最短路经过 k k k,则 d p ( k , i , j ) = d p ( k − 1 , i , k ) + d p ( k − 1 , k , j ) dp(k, i, j) = dp(k - 1, i, k) + dp(k - 1, k, j) dp(k,i,j)=dp(k1,i,k)+dp(k1,k,j)

于是得到 d p ( k , i , j ) = min ⁡ { d p ( k − 1 , i , j ) , d p ( k − 1 , i , k ) + d p ( k − 1 , k , j ) } dp(k, i, j) = \min\{dp(k - 1, i, j), dp(k - 1, i, k) + dp(k - 1, k, j)\} dp(k,i,j)=min{dp(k1,i,j),dp(k1,i,k)+dp(k1,k,j)}

同时,可以使用滚动数组来优化空间,可以转化为 d p ( i , j ) = min ⁡ { d p ( i , j ) , d p ( i , k ) + d p ( k , j ) } dp(i, j) = \min\{dp(i, j), dp(i, k) + dp(k, j)\} dp(i,j)=min{dp(i,j),dp(i,k)+dp(k,j)}

时间复杂度: O ( ∣ V ∣ 3 ) O(|V|^3) O(V3)

空间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(V2)

代码
int n, m;
LL d[N][N];
vector<Edge> e[N];

void Floyd(){
    rep1(i, n + 1) rep1(j, n + 1) d[i][j] = inf;
    rep1(i, n + 1) for (Edge u : e[i]) d[i][u.to] = min(d[i][u.to], u.len); // 防重边
    rep1(i, n + 1) d[i][i] = 0;
    rep1(k, n + 1) rep1(i, n + 1) rep1(j, n + 1)
        if (d[i][k] != inf && d[k][j] != inf)                  // 防负权边
        	d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

Johnson算法

J o h n s o n Johnson Johnson是全源最短路算法,相比于 F l o y d Floyd Floyd,它有更高的代码难度但是快很多的效率。在时间允许的情况下,还是建议使用 F l o y d Floyd Floyd算法。

首先注意到如果图中无负权边,那么我们可以枚举每个点作为起点跑 V V V D i j k s t r a Dijkstra Dijkstra,从而得到两两之间的最短路,时间复杂度为 O ( ∣ V ∣ ∣ E ∣ log ⁡ ∣ V ∣ ) O(|V||E|\log|V|) O(VElogV)。可是如果原图中有负权边该怎么办,这就将问题转化为了如何消除图中的负权边。

我们设一个虚拟节点( 0 0 0号节点),从它向每个节点 i i i 1 ≤ i ≤ V 1 \le i \le V 1iV)连一条权值为 0 0 0的单向边。定义一个点的 h i h_i hi为它到 0 0 0号节点的最短距离,可以从 0 0 0号节点 S P F A SPFA SPFA求出所有点的势。

这时,我们重新定义一条从 u u u v v v权值为 w w w的边的长度为 w + h u − h v w + h_u - h_v w+huhv。由于有三边形不等式 h u + w ≥ h v h_u + w \ge h_v hu+whv,可得 w + h u − h v ≥ 0 w + h_u - h_v \ge 0 w+huhv0,所以所有的边权都是非负的。

来看一下,重新定义边权后, s → t s \to t st的最短距离为
( w 1 + h s − h v 1 ) + ( w 2 + h v 1 − h v 2 ) + ⋯ + ( w n + h v n − 1 − h t ) =   w 1 + w 2 + ⋯ + w n ⏟ o r i g i n a l   s h o r t e s t   p a t h + h s − h t \begin{aligned} (&w_1 + h_s - h_{v_1}) + (w_2 + h_{v_1} - h_{v_2}) + \cdots + (w_n + h_{v_{n - 1}} - h_t)\\ =~&\underbrace{w_1 +w_2 + \cdots + w_n}_{original~shortest~path} + h_s - h_t \end{aligned} (= w1+hshv1)+(w2+hv1hv2)++(wn+hvn1ht)original shortest path w1+w2++wn+hsht
由于 h s − h t h_s - h_t hsht为一个定值,所以我们求得的就是 s → t s\to t st的最短路,最后统一减去 h s − h t h_s - h_t hsht就行了。

时间复杂度: O ( ∣ V ∣ ∣ E ∣ log ⁡ ∣ V ∣ ) O(|V||E|\log|V|) O(VElogV)

空间复杂度: O ( ∣ V ∣ 2 + ∣ E ∣ ) O(|V|^2 + |E|) O(V2+E)

代码
int n, m;
LL dist[N][N];
vector<Edge> e[N];

LL h[N];
bool in_queue[N];
void Johnson(){
    rep1(i, n + 1) e[0].PB(Edge{i, 0});
    rep(i, n + 1) h[i] = inf;
    h[0] = 0;
    queue<int> Q;
    Q.push(0);
    in_queue[0] = true;
    while (!Q.empty()){
        int p = Q.front(); Q.pop(); in_queue[p] = false;
        for (Edge u : e[p]) if (h[p] + u.len < h[u.to]){
            h[u.to] = h[p] + u.len;
            if (!in_queue[u.to]){
                Q.push(u.to);
                in_queue[u.to] = true;
            }
        }
    }

    priority_queue<pair<LL, int> > pq;
    rep1(s, n + 1){
        rep1(i, n + 1) dist[s][i] = inf;
        dist[s][s] = 0;
        pq.push(MP(-dist[s][s], s));
        while (!pq.empty()){
            int cur = pq.top().second;
            LL curdis = -pq.top().first;
            pq.pop();
            if (curdis > dist[s][cur]) continue;
            for (Edge u : e[cur])
                if (curdis + u.len + h[cur] - h[u.to] < dist[s][u.to]){
            	    dist[s][u.to] = curdis + u.len + h[cur] - h[u.to];
             	    pq.push(MP(-dist[s][u.to], u.to));
            	}
        }
        rep1(i, n + 1) dist[s][i] -= h[s] - h[i];  // 默认图是联通的,如不联通,减完后可能不是inf
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值