最短路问题 | 青训营笔记

183 阅读16分钟

最短路问题框架

image.png

注:

  • 单源就是求一个起点到其它所有点的最短路

  • 多源就是很多不同起点到其它所有点的最短路

  • n点数,m边数,可以看出朴素迪杰斯特拉算法的时间复杂度和边数无关,所以适合稠密图(稠密图用邻接矩阵来存),则堆优化版迪杰斯特拉算法适合稀疏图

  • 稠密图就是边数很多,m和n^2一个级别,稀疏图的m和n一个级别

  • 源点:起点
    汇点:终点

  • 最短路问题的难点不在于算法本身的理解,而在于如何从问题中抽象出最短路算法,如何去建图

  • 简要说明:dijkstra算法基于贪心、floyd算法基于动态规划、bellman-ford算法运用离散数学


一、朴素版dijkstra

1. 模板

时间复杂度是o(n^2 + m),n表示点数,m表示边数

int g[N][N];  // 存储每条边
int dist[N];  // 存储1号点到每个点的最短距离
bool st[N];   // 存储每个点的最短路是否已经确定

// 求1号点到n号点的最短路,如果不存在则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    for (int i = 0; i < n - 1; i ++ )
    {
        int t = -1;     // 在还未确定最短路的点中,寻找距离最小的点
        for (int j = 1; j <= n; j ++ )
            if (!st[j] && (t == -1 || dist[t] > dist[j]))
                t = j;

        // 用t更新其他点的距离
        for (int j = 1; j <= n; j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);

        st[t] = true;
    }

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

2. 理解

dijkstra算法应用于不含负权回路的图,与有向无向、有环无环无关

朴素dijkstra思路:

image.png

首先让所有点距离起点的最短距离初始化为无穷大

dist数组表示当前距离起点的最短路径是多少(表示之后可能还会更新)

进行n次迭代,每次迭代都能确定一个点距离起点的最短距离,并且要用这个点来更新其它点(这个点的出边的点)的最短距离(下次迭代就是确定剩下点的存储的最短距离的最小的一个的那个点为这个点距离起点的最短距离)

思想类似于dp,一步一步递推

最短路问题如果有重边只保留长度最短的边

3. 代码

/*
为什么使用dijkstra算法?
(1)求出1号点到n号点的最短距离:单源最短路问题
(2)所有边权均为正值
(3)n[1,500],m[1,10^5],边数多,稠密图
注意点:
(1)处理重边
(2)迭代的内部实现
*/
#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 550;

int n, m;
int g[N][N];//邻接表存储图
int dis[N];//存储每个点距起点的最短路
bool st[N];//表示这个点的最短路是否确定

int dijkstra()
{
    memset(dis, 0x3f, sizeof dis);//先将所有点距离1号点的最短路初始化为无穷大
    
    dis[1] = 0;//1号点距离1号点最短路是0
    
    //迭代n次
    for(int i = 1; i <= n; i ++ )
    {
        //确定一个点的最短路
        int t = -1;//用t存储找到的点
        for(int j = 1; j <= n; j ++ )
            if(!st[j])
                if(t == -1 || dis[j] < dis[t])
                    t = j;
        
        st[t] = true;
        
        //用确定的点更新其它点
        for(int j = 1; j <= n; j ++ )
            dis[j] = min(dis[j], dis[t] + g[t][j]);
    }
    
    if(dis[n] == 0x3f3f3f3f) return -1;//表示路径不存在
    return dis[n];//返回n号点距离1号点的最短路   
}

int main()
{
    scanf("%d%d", &n, &m);
    
    memset(g, 0x3f, sizeof g);
    //先将所有边初始化为无穷大,因为求的是最小,这样防止当使用未连接的两个点的边时影响答案
    
    //建立图
    while(m -- )
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        g[x][y] = min(g[x][y], z);//处理重边
    }
    
    int t = dijkstra();
    
    printf("%d", t);
    
    return 0;
}

重点:外部循环只用循环n-1次

循环n-1次可不可以理解为当前面n-1个结点已经被访问(确定移除集合S)的时候,最后一个结点的最短路径已经被前面涉及的操作确定了。或者说因为如果图是连通的那么在选出一个距离最近的节点,尝试优化它相邻点的距离这一操作被执行n-1次时,第n个点的距离一定会在这些操作中被确定。
感觉也可以用反证法,如果执行最后一次操作会改变前面某个节点j到起点的距离(dist[j]),即通过最后某一个节点i可以使编号为j的节点到起点的距离变小。也就是start->i->j的距离小于start->j的距离,由于没有负权边,start->i的距离一定小于start->j。如果节点i到起点的距离小于j到起点的距离,那么节点i一定会在j之前被选出,这与j先于i被选出矛盾。因此最后一次操作(即第n次操作)不会影响各个节点的最短距离。
想了一会感觉应该八九不离十吧。先说结论,对于外层循环来说每次,都会确定一个最小的距离,且本次确定距离一定比上次确定的距离长,这就是为什么第n个点的最短距离不需要再次确定的原因。
第一,先想第一次循环时,点1的距离为0其余为无穷大,那么本次执行的时候,必然会确定点1的距离且会初始化所有的距离
第二,第二次循环时,找出所有点中的初始化最短的距离,比如本次找出的点为点3,那么从点1到点3的距离肯定会小于第1到其他点再从其他点到点3的距离,这样就可以确定本次距离最短的点。同时更新点1到其他点的距离,这些点的距离一定会大于1到点3的距离。(因为更新时,以前的距离大于点1到点3,点1到点3+点3到其他大于点1到点3)
假设结论成立,那么我们来验证一下第n-1次和第n次的情况
首先进行第n-1次循环时,已经有n-2次已经确定了n-2个数,找到本次最小数点q时,点1到点q的距离一定会小于点1到剩余点再到点q的距离且会大于点1到确定点的距离(一个递归的过程),然后更新最后一个点的距离
当n次循环时,就只剩下了一个点,点1到这个点的距离一定会大于之前所有点的距离,所以最后一个更新对整个结果没有帮助就不用进行

二、堆优化版dijkstra

1. 模板

时间复杂度o(mlogn)

typedef pair<int, int> PII;

int n;      // 点的数量
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储所有点到1号点的距离
bool st[N];     // 存储每个点的最短距离是否已确定

// 求1号点到n号点的最短距离,如果不存在,则返回-1
int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});      // first存储距离,second存储节点编号

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > distance + w[i])
            {
                dist[j] = distance + w[i];
                heap.push({dist[j], j});
            }
        }
    }

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

2. 理解

适用:稀疏图

数据结构:邻接表存图、堆存距离

用邻接表存储就不用考虑重边问题了

在朴素版代码中,最慢的一步就是确定最小距离,而在一堆数中找一个最小值可以用堆来实现,堆可以手写也可以用优先队列,手写堆的好处是支持修改任意一个元素、只会用到n个数,缺点是代码实现太麻烦,优先队列好处是使用方便,坏处是有数据冗余、会用到m个数

3. 代码

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

typedef pair<int, int> PII;

const int N = 1e6 + 10;

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

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

int dijkstra()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    heap.push({0, 1});

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.second, distance = t.first;

        if (st[ver]) continue;
        st[ver] = true;

        for (int i = h[ver]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > d + w[i])
            {
                dist[j] = d + w[i];
                heap.push({dist[j], j});
            }
        }
    }

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

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

    memset(h, -1, sizeof h);
    
    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    printf("%d\n", dijkstra());

    return 0;
}

在pair中把存储距离放在first,节点编号放在second是为了能直接让小根堆的堆顶元素就是在朴素算法中第一个for循环中找的最小的j

注:q.push({x, y}) 可以简写成 q.emplace(x, y)

三、Bellman-Ford算法

时间复杂度o(mn)

1. 模板

int n, m;       // n表示点数,m表示边数
int dist[N];        // dist[x]存储1到x的最短路距离

struct Edge     // 边,a表示出点,b表示入点,w表示边的权重
{
    int a, b, w;
}edges[M];

// 求1到n的最短路距离,如果无法从1走到n,则返回-1。
int bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    // 如果第n次迭代仍然会松弛三角不等式,就说明存在一条长度是n+1的最短路径,由抽屉原理,路径中至少存在两个相同的点,说明图中存在负权回路。
    for (int i = 0; i < n; i ++ )
    {
        for (int j = 0; j < m; j ++ )
        {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if (dist[b] > dist[a] + w)
                dist[b] = dist[a] + w;
        }
    }

    if (dist[n] > 0x3f3f3f3f / 2) return -1;
    return dist[n];
}

2. 理解

数学思维:抽屉原理

存储:结构体

思路:

image.png

每次循环执行的叫“松弛”操作

循环完必定满足三角不等式

在解决边数限制问题时由于串联每次循环前要备份

如果图里存在负权回路,那么最短路是负无穷,不存在

可以用此算法求负环,但时间复杂度较高,一般不用

如果问题有边数限制,那么有负环也无所谓

spfa算法各方面都比bf算法好,但有边数限制问题只能用bf算法

3. 为何dj算法无法处理负权图

一句话:dijstra算法基于贪心思想,当有负权边时,局部最优不一定是全局最优

4. 代码

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 510, M = 10010;

struct Edge
{
    int a, b, c;
}edges[M];

int n, m, k;
int dist[N];
int last[N];

void bellman_ford()
{
    memset(dist, 0x3f, sizeof dist);

    dist[1] = 0;
    for (int i = 0; i < k; i ++ )
    {
        memcpy(last, dist, sizeof dist);
        for (int j = 0; j < m; j ++ )
        {
            auto e = edges[j];
            dist[e.b] = min(dist[e.b], last[e.a] + e.c);
        }
    }
}

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

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        edges[i] = {a, b, c};
    }

    bellman_ford();

    if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
    else printf("%d\n", dist[n]);

    return 0;
}

注:

在代码中,是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
串联:由于这个算法的特性决定,每次更新得到的必然是在多考虑 1 条边之后能得到的全局的最短路。而串联指的是一次更新之后考虑了不止一条边:由于使用了松弛,某节点的当前最短路依赖于其所有入度的节点的最短路;假如在代码中使用dist[e.b]=min(dist[e.b],dist[e.a] + e.c);我们无法保证dist[e.a]是否也在本次循环中被更新,如果被更新了,并且dist[e.b] > dist[e.a] + e.c,那么会造成当前节点在事实上“即考虑了一条从某个节点指向a的边,也考虑了a->b”,共两条边。而使用dist[e.b]=min(dist[e.b],last[e.a] + e.c);,可以保证a在dist更新后不影响对b的判定,因为后者使用last数组,保存着上一次循环中的dist的值。

四、Spfa算法

1. 模板

  • 队列优化的Bellman-Ford算法

时间复杂度,平均情况下o(m),最坏情况下o(nm)

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N];        // 存储每个点到1号点的最短距离
bool st[N];     // 存储每个点是否在队列中

// 求1号点到n号点的最短路距离,如果从1号点无法走到n号点则返回-1
int spfa()
{
    memset(dist, 0x3f, sizeof dist);
    dist[1] = 0;

    queue<int> q;
    q.push(1);
    st[1] = true;

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                if (!st[j])     // 如果队列中已存在j,则不需要将j重复插入
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    if (dist[n] == 0x3f3f3f3f) return -1;
    return dist[n];
}
  • spfa判断图中是否存在负环

时间复杂度o(nm)

int n;      // 总点数
int h[N], w[N], e[N], ne[N], idx;       // 邻接表存储所有边
int dist[N], cnt[N];        // dist[x]存储1号点到x的最短距离,cnt[x]存储1到x的最短路中经过的点数
bool st[N];     // 存储每个点是否在队列中

// 如果存在负环,则返回true,否则返回false。
bool spfa()
{
    // 不需要初始化dist数组
    // 原理:如果某条最短路径上有n个点(除了自己),那么加上自己之后一共有n+1个点,由抽屉原理一定有两个点相同,所以存在环。

    queue<int> q;
    for (int i = 1; i <= n; i ++ )
    {
        q.push(i);
        st[i] = true;
    }

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;
                if (cnt[j] >= n) return true;       // 如果从1号点到x的最短路中包含至少n个点(不包括自己),则说明存在环
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

2. 理解

要求:不能有负环回路

dist[b] = min(dist[b], dist[a] + w);

只有a变小时,b才会变小

spfa从这一点做优化,只有变小的点才用更新它的出边

队列里存的就是所有变小的节点

spfa算法不仅可以处理负权图,也可以处理正权图

由于时间效率稍快,所以在时间复杂度允许的情况,一般也用spfa取代dijkstra

3. 代码

#include <iostream>
#include <cstring>
#include <queue>

using namespace std;

const int N = 1e5 + 10;

int n, m;//n个点,m条边
int h[N], e[N], w[N], ne[N], idx;//邻接表存图
int dist[N];//存储每个节点的最短路
bool st[N];//表示当前点是否在队列中

void add(int x, int y, int z)//从点x到点y连一条边,边权重为z
{
    e[idx] = y;
    w[idx] = z;
    ne[idx] = h[x];
    h[x] = idx;
    idx ++;
}

int spfa()
{
    memset(dist, 0x3f, sizeof dist);//初始化dist数组
    dist[1] = 0;//初始化1号节点
    queue<int> q;//使用队列来存储每个被更新过的节点
    q.push(1);//将1号节点插入队列
    st[1] = true;//改变状态
    
    //迭代更新最短路
    while(q.size())//当队列不为空时
    {
        int t = q.front();//取出队头的节点
        q.pop();//弹出队头
        st[t] = false;
        
        //遍历当前节点所有的出边
        for(int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if(dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                //将更新的节点插入队列
                if(!st[j])//前提是队列不能已经有
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }
    
    return dist[n];
}

int main()
{
    scanf("%d%d", &n, &m);
 
    memset(h, -1, sizeof h);
    
    while(m -- )
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        add(x, y, z);
    }
    
    int t = spfa();
    
    if (t == 0x3f3f3f3f) puts("impossible");
    else printf("%d", t);
    
    return 0;
}
  • 不需要dist[n] > 0x3f3f3f3f / 2:

    因为队列里都是由起点更新到的点,不存在bellman-ford算法中未更新的点同样被边更新的情况。

    spfa遵循了一个拓扑顺序,而bellman-ford算法没有。

  • 曾经加入过队列的点出队后,可能会再次被加入队列

    所有会有st[t] = false

  • 判断为什么不直接写成dist[j]=min(dist[j],dist[t]+w[i]);

    因为这样st 数组的更新就不好判断了

4. 如何利用spfa算法判断负环

数学思维:抽屉原理

开一个cnt数组,维护每一个节点的最短路的边数

如果过程中出现cnt>=n,就一定有负环,假设cnt等于n,说明最短路的边数是n条,连接的节点有n+1个,根据抽屉原理,那么一定有一个节点被遍历了两次,则一定存在环,且是负环,如果是正环,dist是增加的,不会更新最短路,不会更新边数

代码

#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 2010, M = 10010;

int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];

void add(int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

bool spfa()
{
    queue<int> q;

    for (int i = 1; i <= n; i ++ )
    {
        st[i] = true;
        q.push(i);
    }

    while (q.size())
    {
        int t = q.front();
        q.pop();

        st[t] = false;

        for (int i = h[t]; i != -1; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[t] + w[i])
            {
                dist[j] = dist[t] + w[i];
                cnt[j] = cnt[t] + 1;

                if (cnt[j] >= n) return true;
                if (!st[j])
                {
                    q.push(j);
                    st[j] = true;
                }
            }
        }
    }

    return false;
}

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

    memset(h, -1, sizeof h);

    while (m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        add(a, b, c);
    }

    if (spfa()) puts("Yes");
    else puts("No");

    return 0;
}
  • 问:为什么dt数组不用初始化为0x3f3f3f3f,以及为什么初始化要把所有点入队?

  • 答:dt数组的初始值是多少都不影响,因为dt数组在这里记录的不是最短路径。首先,我们理解初始化时为什么把所有点都加入队列中,在求1开始到n的最短路时,我们只把1入队了且让dt[1] = 0,目的是让1成为开始时唯一一个更新了dt数组的点,然后在根据已更新dt数组的这些点去更新他的出边(这就是spfa改良bellman的精髓)。但是负环可能不在点1的后继上(可以自行构造,把1放在拓扑图的中断位置,负环在点1的前面),所以要把所有点入队。所有看到这就懂了,dt数组的意义不是记录最短路径,而且来更新后继节点的,如果某个点的dt更新过了,那么就可以用这个点来更新他的后继节点(在求最短路问题里,一个点距离初始点的距离边短了,是不是尝试用这个点去更新他的后继节点,可能使得后继节点的最短距离也变小)。把所有点都入队,那第一次更新的时候是哪些点被更新了呢,答案是负权边的入边那个点被更新了,假设dt数组初始化为0,那加上一个负边使得dt变小。所以如果要解释dt数组的含义的话,假设图里不存在负权环,那dt[i]的含义就是一个点走过有限次边到达i,且走过的距离最小,这个距离就是dt[i]。

  • 问:题目没有说一定是一个连通图,如果是非连通图那么cnt是不是不一定必须要大于n才有负环,n这个值是不是限定大了?

  • 答:如果有环的话,n只要设为一个大于该连通块的点点数的值就行,而且更大都行,因为负环会使一个点被一直更新下去,是一个死循环,所有只要设定一个阈值来判断它就行

  • 问:这里为什么不用初始化dist数组?

  • 答:dist数组一开始已经是0了,所以接下来只会更新小于0的边权,也就是负权边,存在负环的话就会不停的更新。

    因为你是把所有的顶点都入队了,所以可以看成所有顶点都是初始起点,如果你只设例如1为起点,那样对负权回路的判断就很困难,但是如果你设所有顶点都为起点,那样一定有起点处于负权回路中,而且我们是要求是否存在负权回路,所以初始化就没有那么重要了,因为如果有父权回路那样q.size()>=1恒成立。

五、Floyd算法

1. 模板

时间复杂度是o(n^3),n表示点数

初始化:
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= n; j ++ )
            if (i == j) d[i][j] = 0;
            else d[i][j] = INF;

// 算法结束后,d[a][b]表示a到b的最短距离
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]);
}

2. 理解

要求:

可以处理负权边,但不能有负环回路

存储:

邻接矩阵

原理:

floyd本身是个动态规划算法,在代码实现的时候省去了一维状态。 原状态是:f[i, j, k]表示从i走到j的路径上除了i, j以外不包含点k的所有路径的最短距离。那么f[i, j, k] = min(f[i, j, k - 1), f[i, k, k - 1] + f[k, j, k - 1]。 因此在计算第k层的f[i, j]的时候必须先将第k - 1层的所有状态计算出来,所以需要把k放在最外层。

3. 代码

对于无穷大的设定:用memset就用0x3f,循环里赋值就用INF(=1e9)

#include <iostream>

using namespace std;

const int N = 210, INF = 1e9;;

int n, m, k;
int g[N][N];

void floyd()
{
    for(int k = 1; k <= n; k ++ )
        for(int i = 1; i <= n; i ++ )
            for(int j = 1; j <= n; j ++ )
                g[i][j] = min(g[i][j], g[i][k] + g[k][j]);
}

int main()
{
    scanf("%d%d%d", &n, &m, &k);
    
    //初始化邻接矩阵
    for(int i = 1; i <= n; i ++ )
        for(int j = 1; j <= n; j ++ )
            if(i != j)//i == j 时是自环,等于0
            g[i][j] = INF;
    
    while(m -- )
    {
        int x, y, z;
        scanf("%d%d%d", &x, &y, &z);
        g[x][y] = min(g[x][y], z);//处理重边
    }
    
    floyd();
    
    while(k -- )
    {
        int x, y;
        scanf("%d%d", &x, &y);
        if(g[x][y] > INF / 2) puts("impossible");//边权可能为负
        else printf("%d\n", g[x][y]);
    }
    
    return 0;
}
  • 问:为什么朴素Dijkstra初始化的时候 (i==j)时不用初始化为0 而这里需要呢

  • 答:理解成源点到源点本身的距离为0,在朴素Dijkstra中确定了源点为1,所以初始化dist[1]=0,就可以了。而Floyd为多源点,所以要初始化i==j的所有可能的源点,在朴素Dijkstra初始化所有源点也是可以的

  • 问:什么时候跟INF/2比较,啥时候跟INF比较?

  • 答:具体情况具体分析,本题中可能存在如下情况:不能走到终点,但由于负数边权的存在,终点的距离可能被其他长度是正无穷的距离更新。跟INF / 2比较可以处理这种情况。

    注:涉及到负权边的最短路问题都要想到这样一个问题:到不了的几个点之间可能存在负权边。使得到不了的点的距离一定范围内减小,小于了INF。这时可以使用小一点的INF/2来做判断,大于INF/2则认为不存在通路。