图论之最短路基础总结(单源最短路(Dijkstra、堆优化版Dijkstra、Bellman-Ford、SPFA)+多源汇最短路(Floyd))

最短路

结构

  • 单源最短路
    • 所有边权都是正数
      • 朴素Dijkstra
        • O ( n 2 ) O(n^2) O(n2)
      • 堆优化版Dijkstra
        • O ( m l o g ( n ) ) O(mlog(n)) O(mlog(n))
    • 存在负权边
      • Bellman-Ford算法
        • O ( n m ) O(nm) O(nm)
        • 可以处理有负权边、有负权回路的图
      • SPFA
        • O ( m ) O(m) O(m)
        • 可以处理负权边,但是不能处理有负权回路的图
  • 多源汇最短路
    • Floyd算法
      • O ( n 3 ) O(n^3) O(n3)

Tips: bellman-ford算法基本上不会用。


单源最短路

朴素Dijkstra

时间复杂度: O ( n 2 ) O(n^2) O(n2)

算法流程
  1. 初始化dist[start] = 0,其余节点的dist值为无穷大。
  2. 找出一个未被标记的、dist[x]最小的节点x,然后将标记节点x。
  3. 扫描节点x的所有出边(x, y, z),若dist[y] > dist[x] + z,则使用dist[x] + z更新dist[y]
  4. 重复上述2-3两个步骤,直到所有节点都被标记。
原理

Dijkstra算法基于贪心思想,它只适用于所有边的长度都是非负数的图。当边长z都是非负数时,**全局最小值不可能再被其他节点更新,故在第2步中选出的节点x必须满足:dist[x]已经是起点到x的最短距离。**这个也是Dijkstra算法不能解决负权边的图,每个点被确定st[j] = true后,dist[j]就是最短距离了,之后就不能再被更新了,而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了。

我们不断选择全局最小值进行标记和扩展,最终可得到起点到每个结点的最短路径。

基本代码
int dist[N]; // 节点i到起点的最短距离。
bool st[N]; // 标记是否已经被用过

void dijkstra(int start)
{
    memset(dist, 0x3f3f, sizeof dist);
    memset(st, 0, sizeof st);
    
    dist[start] = 0;
 	// 循环n-1次就可以
    for (int i = 1; i < n; i ++)
    {
    	int t = -1;
        for (int j = 1; j <= n; j ++)
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;
        
        st[t] = true;
        for (int j = 1; j <= n; j ++)
			dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
}

堆优化版Dijkstra

原理

朴素版的Dijkstra算法的瓶颈主要在于第2步的寻找全局最小值的过程,每次都需要花费O(n)的代价。可以用二叉堆对dist数组进行维护,用 O ( l o g n ) O(logn) O(logn)的时间获取最小值并从堆中删除,用 O ( l o g n ) O(logn) O(logn)的时间执行一条边的扩展和更新,最终可在 O ( m l o g n ) O(mlogn) O(mlogn)的时间内实现Dijkstra算法。

基本代码

题目链接:https://www.acwing.com/problem/content/852/

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>

using namespace std;

typedef pair<int, int> PII;
const int N = 150000 + 100;

int h[N], w[N], ne[N], e[N], idx;
int d[N];
bool st[N];
int n, m;

void add(int x, int y, int z)
{
    e[idx] = y;
    ne[idx] = h[x];
    w[idx] = z; // w存储x到y的权值
    h[x] = idx ++;
}

int dijkstra()
{
    // 优先队列,小根堆(默认是大根堆)
    // 或者直接使用priority_queue<PII>, 存储权值的相反数。
    priority_queue<PII, vector<PII>, greater<PII>> q;

    d[1] = 0; // 第一节点初始化为0

    // 注意:PII 第一个元素是路径,第二个元素是节点编号
    q.push({0, 1});

    while(!q.empty())
    {
        // 取出队列中第一个节点(最短路径的节点)
        auto head = q.top();
        q.pop();

        int distance = head.first, t = head.second;
        // 若用此节点的最短路径更新过其他节点,则跳出。
        if (st[t]) continue;
        st[t] = true;
        // 找到此节点能到达的节点,然后更新他们到起始点的最短距离。
        for (int i = h[t]; ~i; i = ne[i])
        {
            int j = e[i];
            if (d[j] > w[i] + distance)
            {
                d[j] = w[i] + distance;
                q.push({d[j], j});
            }
        }
    }

    if (d[n] == 0x3f3f3f3f) return -1;
    else return d[n];
}

int main()
{
    scanf("%d%d", &n, &m);

    memset(h, -1 , sizeof h);
    memset(d, 0x3f, sizeof d);

    for (int i = 0; i < m; i ++)
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        add(x, y, z);
    }

    printf("%d\n", dijkstra());
    return 0;
}

Bellman-Ford算法

基于松弛操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。

对于边(u,v),松弛操作对应下面的式子: d i s ( v ) = m i n ( d i s ( v ) , d i s ( u ) + w ( u , v ) ) dis(v) = min(dis(v), dis(u) + w(u, v)) dis(v)=min(dis(v),dis(u)+w(u,v))

该算法不断尝试对图上的每条边进行松弛,我们没进行一轮循环,就对图上所有的边都尝试进行一次松弛操作,当一次循环中没有成功的松弛操作时,算法停止。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZkffBVH2-1665140934535)(综述.assets/image-20221007151803992.png)]

算法流程

给定一张有向图,若对于图中的某一条边(x,y,z),有dist[y] <= dist[x] + z成立,则称该边满足三角形不等式。若所有边都满足三角不等式,则dist数组就是所求最短路。

利用反证法很容易证明这个结论。

  1. 扫描所有边(x, y, z),若dist[y] > dist[x] + z,则用dist[x] + z更新dist[y]
  2. 重复上述步骤,直到没有更新操作发生。

算法复杂度: O ( n m ) O(nm) O(nm)

该算法效率较差,但适合做有边数限制的最短路。

例题:https://www.acwing.com/problem/content/855/

基本代码
#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int N = 510, M = 10010, INF = 0x3f3f3f3f;

// 存储边以及对应的权重
struct Edge{
    int a, b, c;
}e[M];

// d存储每个节点到起点的距离
// backup为上一次迭代的d(备份),防止出现串联反应。
int d[N], backup[N];
int n, m, k;

int bellman_ford()
{
    d[1] = 0;
    for (int i = 0; i < k; i ++)
    {
        memcpy(backup, d, sizeof d);
        for (int j = 0; j < m; j ++)
        {
            int a = e[j].a, b = e[j].b, c = e[j].c;
            d[b] = min(d[b], backup[a] + c);
        }
    }
    return d[n];
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);

    memset(d, 0x3f, sizeof d);

    for (int i = 0; i < m; i ++)
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        e[i] = {x, y, z};
    }

    int res = bellman_ford();
    if (res > INF / 2) puts("impossible");
    else printf("%d\n", res);
}

SPFA

SPFA是shortest Path Fast Algorithm的缩写,在国际上通称为“队列优化的Bellman-Ford算法”,仅在中国大陆流行“SPFA算法”的称谓。

算法流程
  1. 建立一个队列,最初队列中只含有起点start
  2. 取出队头节点x,扫描它的所有出边(x,y,z),若dist[y] > dist[x] + z,则使用dist[x] + z来更新dist[y]。同时,若y不在队列中,则把y入队。
  3. 重复上述步骤,直到队列为空。
原理

Bellman_ford算法会遍历所有的边,而SPFA只用遍历那些到源点距离变小的点所连接的边,只有当一个点的前驱结点更新了,该节点才会得到更新;利用队列每次加入距离被更新的结点,在任意时刻,该算法的队列都保存了待扩展的节点。每次入队相当于完成一次dist数组的更新操作,使其满足三角形不等式。**一个节点可能会入队、出队多次。**最终,图中节点收敛到全部满足三角形不等式的状态。这个队列避免了Bellman-Ford算法中对不需要扩展的节点的冗余扫描,在随即图上运行效率为 O ( k m ) O(km) O(km)级别,其中k是一个较小的常数(一般是2~3)。但在特殊构造的图上,该算法很可能退化为 O ( n m ) O(nm) O(nm),必须谨慎使用。

基本代码
void spfa(int start)
{
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    
    dist[start] = 0;
    st[start] = true;
    
    q.push(start);
    while(!q.empty())
    {
        int x = q.front(); q.pop();
        // 弹出后,标记为false,因为该节点有可能会再次入队。
      	st[x] = false;
        for (int i = h[x]; ~i; i = ne[i])
        {
            int j = e[i];
            if (d[j] > d[x] + w[i])
            {
                d[j] = d[x] + w[i];
                if (!st[j])// 如果发生更新,并且没有该节点没有在队列里,就把他加入到队列中。
                {
                    q.push(j);                    
                    st[j] = true;// 标记已经在队
                }
            }
        }
    }
}

注意:

  1. **st数组的作用:**判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。即便不使用st数组最终也没有什么关系,但是使用的好处在于可以提升效率。
  2. Bellman_ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
其他优化

Bellman-Ford 的其他优化

除了队列优化(SPFA)之外,Bellman-Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。

  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。

  • LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。

  • SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。

  • D´Esopo-Pape 算法:将普通队列换成双端队列,如果一个节点之前没有入队,则将其插入队尾,否则插入队首。

更多优化以及针对这些优化的 Hack 方法,可以看 fstqwq 在知乎上的回答


多源汇最短路

Floyd算法

求图中任意两点间的最短路径,可以把每个点当作起点,求解N次单源最短路。不过,在任意两点间最短路问题中,图一般比较稠密。

该算法本质上是动态规划,用来求任意两个结点之间的最短路,复杂度比较高 O ( n 3 ) O(n^3) O(n3),但是常数小,容易实现。适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有个负环)

原理

d[i][j][k]:表示经过若干个编号不超过k的节点从i到j的最短路长度。

该问题可以划分为两个子问题:

  1. 经过编号不超过k-1的节点从i到j。
  2. 从i先到k再到j。

于是:

d[i][j][k] = min(d[i][j][k-1], d[i][k][k-1] + d[k][j][k - 1])

初值为d[0][i][j] = A[i][j](邻接矩阵)

抽象来说:即判断从i走到j的路径中,途中经过第k个点路径是否会更短?

通俗来讲:

  • 当k=3时,d[i][j]表示下面两种情况的最小值
    1. i直接到j(这种情况路径就是输入时的边权)
    2. i经过2,然后从2到达j
基本代码
void floyd() {
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}

其他细节

SPFA VS Dijstra

SPFA算法看上去和Dijstra算法长得有一些像,但是其中的意义还是相差甚远的:

  • Dijkstra算法中的st数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为true后改变为false);SPFA算法中的st数组仅仅只是表示的当前发生过更新的点,且spfa中的st数组可逆(可以在标记为true之后又标记为false)。顺带一提的是BFS中的st数组记录的是当前已经被遍历过的点。
  • Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。

参考文献

  • 《算法竞赛进阶指南》
  • https://www.acwing.com/solution/content/9306/
  • https://oi-wiki.org/
好的,下面是使用C语言实现Dijkstra算法SPFA算法Bellman-Ford算法的代码示例: 1. Dijkstra算法: ```c #include <stdio.h> #include <stdbool.h> #define INF 99999 #define V 6 int minDistance(int dist[], bool visited[]) { int min = INF, min_index; for (int v = 0; v < V; v++) { if (visited[v] == false && dist[v] <= min) { min = dist[v]; min_index = v; } } return min_index; } void printSolution(int dist[]) { printf("Vertex\tDistance from Source\n"); for (int i = 0; i < V; i++) { printf("%d\t%d\n", i, dist[i]); } } void dijkstra(int graph[V][V], int source) { int dist[V]; bool visited[V]; for (int i = 0; i < V; i++) { dist[i] = INF; visited[i] = false; } dist[source] = 0; for (int count = 0; count < V - 1; count++) { int u = minDistance(dist, visited); visited[u] = true; for (int v = 0; v < V; v++) { if (!visited[v] && graph[u][v] && dist[u] != INF && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; } } } printSolution(dist); } int main() { int graph[V][V] = { {0, 4, 0, 0, 0, 0}, {4, 0, 8, 0, 0, 0}, {0, 8, 0, 7, 0, 4}, {0, 0, 7, 0, 9, 14}, {0, 0, 0, 9, 0, 10}, {0, 0, 4, 14, 10, 0} }; dijkstra(graph, 0); return 0; } ``` 2. SPFA算法: ```c #include <stdio.h> #include <stdbool.h> #include <limits.h> #define V 5 // 图中顶点的数量 typedef struct { int u; int v; int weight; } Edge; void printSolution(int dist[]) { printf("Vertex\tDistance from Source\n"); for (int i = 0; i < V; i++) { printf("%d\t%d\n", i, dist[i]); } } void spfa(int graph[V][V], int source) { int dist[V]; bool inQueue[V]; for (int i = 0; i < V; i++) { dist[i] = INT_MAX; inQueue[i] = false; } dist[source] = 0; // 创建一个队列,用于存储顶点索引 int queue[V]; int front = -1; int rear = -1; // 将起始顶点加入队列 queue[++rear] = source; inQueue[source] = true; while (front != rear) { int u = queue[++front]; inQueue[u] = false; for (int v = 0; v < V; v++) { if (graph[u][v] && dist[u] != INT_MAX && dist[u] + graph[u][v] < dist[v]) { dist[v] = dist[u] + graph[u][v]; if (!inQueue[v]) { queue[++rear] = v; inQueue[v] = true; } } } } printSolution(dist); } int main() { int graph[V][V] = { {0, 4, 0, 0, 0}, {4, 0, 8, 0, 0}, {0, 8, 0, 7, 0}, {0, 0, 7, 0, 9}, {0, 0, 0, 9, 0} }; spfa(graph, 0); return 0; } ``` 3. Bellman-Ford算法: ```c #include <stdio.h> #include <stdbool.h> #include <limits.h> #define V 5 // 图中顶点的数量 #define E 8 // 图中边的数量 typedef struct { int u; int v; int weight; } Edge; void printSolution(int dist[]) { printf("Vertex\tDistance from Source\n"); for (int i = 0; i < V; i++) { printf("%d\t%d\n", i, dist[i]); } } void bellmanFord(Edge edges[], int source) { int dist[V]; for (int i = 0; i < V; i++) { dist[i] = INT_MAX; } dist[source] = 0; for (int i = 1; i < V; i++) { for (int j = 0; j < E; j++) { int u = edges[j].u; int v = edges[j].v; int weight = edges[j].weight; if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) { dist[v] = dist[u] + weight; } } } // 检查是否存在负权回路 for (int i = 0; i < E; i++) { int u = edges[i].u; int v = edges[i].v; int weight = edges[i].weight; if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) { printf("Graph contains negative weight cycle\n"); return; } } printSolution(dist); } int main() { Edge edges[E] = { {0, 1, -1}, {0, 2, 4}, {1, 2, 3}, {1, 3, 2}, {1, 4, 2}, {3, 2, 5}, {3, 1, 1}, {4, 3, -3} }; bellmanFord(edges, 0); return 0; } ``` 以上是使用C语言实现Dijkstra算法SPFA算法Bellman-Ford算法的代码示例。希望对你有所帮助!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Honyelchak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值