主要想法是,通过逐渐增加允许经过的节点,来更新最短路,本质上是动态规划方法
求取图中任意两点之间的距离
f[x][k]
和 f[k][y]
不会被更新,因为 k
是路径上的端点(端点本来就允许,现在允许经过,和之前没有区别)
f[k][x][k] = min(f[k - 1][x][k], f[k - 1][x][k] + 0) = f[k - 1][x][k]
,可见f[x][k]
不变f[k][y]
不变算法应用
// n 是点的个数 , dis是距离数据, dis[i][j]: i 与 j 当前的最短距离
void Floyd(int n, int** dis){
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 + m ) log n ) O((n + m)\log{n}) O((n+m)logn)
void dijkstra(int s){
priority_queue, greater> q; // 优先队列,从小到大排序
for(int i = 1; i <= n; i++) dis[i] = inf, vis[i] = 0; // vis[i] = 1 代表 i 不用再访问了
dis[s] = 0;
q.push(make_pair(0, s)) // 前面是距离,后面是节点
while(!q.empty()){
int x = q.top().second;
q.pop();
if(vis[x]) continue; // 这保证了每个点只会进行下列操作一次
vis[x] = 1;
for(int i = point[x]; i != 0; i = nxt[i]) // 遍历所有邻接节点
if(dis[v[i]] > dis[x] + w[i]){
dis[v[i]] = dis[x] + w[i]; // 松弛
q.push(make_pair(dis[v[i]], v[i]));
}
}
}
为什么一个节点会被多次加入堆中?
为什么每个点只会被弹出最小堆一次?
vis[x] = 1
,下次就会直接 continue
了。这样合理么?
x
的时候,说明,目前dis[x]
是堆中最小的了。我们通过堆中其他节点,不能够以更小的距离再次到达 x
。所以,只要我们将 x
从堆中弹出,我们就找到了最短的 dis[x]
。第二次弹出的时候,没必要再松弛邻居节点了,因为肯定不如第一次松弛时短。x
对其邻接节点进行松弛 1 次。这个操作导致我们不能处理负边为什么不能处理负边?:为了降低算法复杂度
(10, B)
和 (7, C)
放入最小堆中(7, C)
,并令 vis[C] = 1, dis[C] = 7
,扩张一次,令 vis[D] = 7 + 3 = 10
B
走到 C
时,会更新 vis[C] = 10 - 5 = 5
,并将 (5, C)
放入堆(5, C)
的时候,但是不会利用其进行扩张了,因为 vis[C] = 1
。A->B->C->D
这条更短的路,影响了 C
之后点的最短距离的更新vis
,不论有没有弹出过,我们都可以以此为节点,来松弛其邻接节点,这样就可以应对负边。但是这样复杂度会变大。为了保证算法的高效,我们只在所有边权都是正的情况下应用此算法。这样就能保证,当我们弹出一个节点x
时,不会再找到一条到达x
的更短的路,从而避免更新其后面的节点我们设初始点为 s
核心思想:我们以随机的顺序进行边的松弛,每次都对所有边进行松弛。如果 s
到 u
的最短路经过 k
条边,那么在第 k
轮松弛后,我们就能找到这条最短路。
s->a
和 s->c
。因为他们的最短路都是一条边的s->a->b
。因为当我们松弛边ab
时,一定能够将b
的最距离更新到最短。(也有可能一轮就找到,如果我们在松弛 ab
之前先松弛过 sa
的话,两轮是一定可以)b
后面加个 c
,因为 s->b
的最短路已经找到,所以可以确定 s->b + b->c
是 s->c
的最短路s
到 u
的最短路经过 k
条边,那么在第 k
轮松弛后,我们一定能找到这条最短路。我们介绍完了核心思想,再给出一个引理:
这直观上容易理解。
我们默认,图中不存在负权环,那么,松弛 n - 1
轮(因为,最短路的边数不会超过 n - 1
),一定能找到单源最短路
for(int i = 1; i <= n; i++)
dis[i] = INF, pre[i] = 0; // pre[i] 代表是从哪走过来的,i节点的前一个是谁
dis[s] = 0; // 起点距离为 0
for(int k = 1; k < n; k++) // 第 k 轮松弛
for(int i = 1; i <= m; i++) // 对所有边进行松弛
if(dis[v[i]] > dis[u[i]] + w[i]){
dis[v[i]] = dis[u[i]] + w[i]; // 松弛成功
pre[v[i]] = u[i]; // 更新父节点
}
Bellman-ford 算法能够解决负边权问题,但是复杂度比较高
我们注意到,每一轮松弛都有很多无效的松弛操作,因为有些最短路在之前的松弛中已经确定了,不用再松弛了
通过观察,可得,松弛操作仅仅发生在最短路径前导结点中已经成功松弛过的结点上。因为如果 u
成功松弛了,则 dis[u]
就变小了,通过 u
连接的其他结点的 dis
也会变小(dis[v] = dis[u] + u->v
)
因此,我们每次只做有效的松弛操作
void spfa(int s){
for(int i = 1; i <= n; i++) vis[i] = 0, dis[i] = inf;
dis[s] = 0; vis[s] = 1;
queue p;
p.push(s);
while(!p.empty()){
int now = p.front(); p.pop();
for(int i = point[now]; i != 0; i = nxt[i])
if(dis[v[i]] > dis[now] + len[i]){
dis[v[i]] = dis[now] + len[i];
pre[v[i]] = now;
if(!vis[v[i]]){ // 如果 v[i] 不在队列中,才放进去
vis[v[i]] = 1;
p.push(v[i]);
}
}
vis[now] = 0;
}
}
个人觉得和上文说的去掉 vis
的 Dijkstra
很像,只不过因为有负边的存在,我们不用取队列中的最小值
上文取最小值是为了减少复杂度
但是这里,比如dis[u] = 10
,dis[v] = 11
, v 可以通过走负的边来到达 u,我们先取出 u
不代表到 u
的最短路已经找到了。因此,没必要去最小值
总结起来就是,哪里更新过了(变小了),就可能影响后续的值,我们就要对其邻接结点进行松弛
有些时候,是无解的
dis = INF
dis
是负无穷前面一种情况是容易判断的,我们如何判断图中存在负环呢?
对于 Bellman-ford 来说,若松弛完 n - 1
轮后,在第 n
轮松弛时,还有边能够被成功松弛,则有负环。
对于 SPFA,我们用 cnt[x]
来表示当前到达 x
的最短路的边数,如果 cnt[x] >= n
,则存在负环。
void spfa(int s){
for(int i = 1; i <= n; i++) vis[i] = 0, dis[i] = inf, cnt[i] = 0;
dis[s] = 0; vis[s] = 1;
queue p;
p.push(s);
while(!p.empty()){
int now = p.front(); p.pop();
for(int i = point[now]; i != 0; i = nxt[i])
if(dis[v[i]] > dis[now] + len[i]){
dis[v[i]] = dis[now] + len[i];
cnt[v[i]] = cnt[now] + 1;
if(cnt[v[i]] >= n){
// 有负环
}
pre[v[i]] = now;
if(!vis[v[i]]){ // 如果 v[i] 不在队列中,才放进去
vis[v[i]] = 1;
p.push(v[i]);
}
}
vis[now] = 0;
}
}