搜索与图论 --- 最短路 - 朴素版 DijKstra 算法、堆优化版 DijKstra 算法、Bellman-Ford 算法

最短路涉及了很多算法,不同算法对应了最短路问题里面不同的情景

常见的最短路问题一般来说可以分成两大类,单源最短路问题和多源汇最短路问题

规定 n 表示图里面点的数量,m 表示边的数量

贪心算法、DijKstra算法

分支定界思想

单源最短路问题:一般来说是求从一个点到其他所有点的最短距离,常见的题型就是求从 1 号点到 n 号点的最短路,只要求出来从 1 号点到其他所有点的最短路之后,显然从 1 号点到 n 号点的最短路也就求出来了

多源汇最短路问题:源点就是起点,汇点就是终点,多源汇最短路也就是说可能不止一个起点,可能会有很多个询问,每个询问问我们任选两个点,从其中一个点走到另外一个点的最短距离,起点和终点都是不确定的,这种就叫作多源汇最短路

在这两大类的基础上再细分

单源最短路可以再细分成两大类,①所有边的权重都是正数   ②存在负权边,图里面某些边的权重可能是负数

(一)所有边的权重都是正数有两个算法可以使用,不同的方式适用于不同的场景

朴素版 DijKstra 算法的时间复杂度为 O(n^2) ,n 是指点数,朴素版 DijKstra 算法的时间复杂度与边数 m 无关,比较适合于稠密图,当边数比较多的时候,比方说边数和 O(n^2) 是一个级别的时候,应该使用朴素版的 DijKstra 算法,如果稠密图使用堆优化版的 DijKstra 算法时间复杂度为 O(mlogn),m 和 n^2 是一个级别,时间复杂度就会变成 O(n^2logn),因此稠密图尽量使用朴素版 DijKstra 算法

堆优化版 DijKstra 算法的时间复杂度为 O(mlogn),如果是一个稀疏图,m 和 n 是一个级别的,n 和 m ≤ 10^5,此时就不能用朴素版 DijKstra 算法,因为 n^210^{10} 会导致超时,但是可以使用堆优化版的 DijKstra 算法,时间复杂度为 O(mlogn)

(二)图里面存在负权边有两个算法可以使用

Bellman-Ford 算法,时间复杂度为 O(nm),点数n × 边数m

SPFA 算法其实是对 Bellman-Ford 算法进行了优化,一般情况下时间复杂度是线性的,时间复杂度为 O(m),最坏情况下时间复杂度为 O(nm),但是效率一般比 Bellman-Ford 算法高,虽然SPFA 算法是Bellman-Ford 算法的优化,但并不是所有情况下用 SPFA 算法都可以解决。如果对最短路经过的边数做一个限制,求经过的边数小于等于 k 的最短路,就只能用 Bellman-Ford 算法来解决

多源汇最短路有一个算法可以使用,Floyd 算法,时间复杂度为 O(n^3)

单源:求一个起点到其他所有点的最短路

多源:求很多不同起点到其他所有点的最短路

边数 m 和 n^2 是一个级别,稠密图

边数 m 和 n  是一个级别,稀疏图

考察最短路算法一般要求把原问题抽象成最短路问题,如何定义点和边是很重要的,使得这个问题变成最短路问题,难点在建图,不在算法原理,不要求证明算法的正确性

存在负权边一般使用 SPFA 算法,极个别情况使用 Bellman-ford 算法

朴素版 DijKstra 算法

单源最短路,求 1 号点到其他所有点的最短距离

①初始化距离,第一步只有起点被遍历到了,只有起点的距离是确定的,其他所有点的距离都是没有确定的。dist[1] = 0,让 1 号点到起点的距离是 0,其他所有的点都等于正无穷,在实现算法的时候,用一个很大的数表示正无穷

②迭代、循环的过程,i 从 0 到 n,循环 n 次,把所有当前已经确定了最短距离的点放到集合 s 里面。第一步,找到不在 s 中的距离最近的点 t,第二步,把 t 加到 s 里面去,第三步,用 t 来更新其他点的距离。其实就是看一下从 t 出去的所有的边组成的路径能不能更新其他点的距离

t → x 有一条边,可以从 t 走到 x,看一下当前 x 到 1 号点的距离,能不能用从 t 到 1 号点的距离更新,也就是看一下 dist[x] 是不是大于 dist[t] + w,如果大于的话就把 x 的距离更新为 dist[t] + w

每一次迭代都可以确定一个点到起点最短距离(确定的点是当前还没有确定的点当中距离最小的一个点,这个点的距离一定是最短路,基于贪心思想 贪心算法、DijKstra算法),迭代 n 次就可以确定 n 个点的最短距离,就可以确定出每一个点到起点的最短距离

Dijkstra 求最短路 I

题目有 500 个点,10 w 条边,是一个稠密图,一般用邻接矩阵来存储

存在重边和自环,如果所有边是正的,自环很明显不会出现在最短路里面,如果有重边,两个点只要保留长度最小的边就可以了

参考 AcWing 849 Dijkstra求最短路 I

一开始要初始化邻接矩阵g,距离为0x3f3f3f(无穷大),由于memset 按字节赋值,所以memset 0x3f 就等价与赋值为0x3f3f3f3f

下面举一个例子

1 号点到 2 号点的距离是 2,2 号点到 3 号点的距离是 1,1 号点到 3 号点的距离是 4

第一步,初始化距离, 1 号点的距离是 0,其他所有点的距离都是 +\infty,用红色表示待定的点,用绿色表示已经确定的点

第二步迭代 n 次,每次迭代的时候,先找到【当前所有没有确定的点当中的最小值】由于另外两个点的距离都是 +\infty,当前这个点的距离是 0,所以找到的【没有确定的点当中的最小值】是 1 号点,找到 1 号点之后,1 号点的最短距离就确定了,1 号点的最短距离一定是 0,这就是迪杰斯特拉算法的原理,参考 最短路 - OI Wiki,找完这个点之后,再用这个点去更新其他所有点到起点的距离,1 号点一共出去两条边,所以可以更新两个点,更新的第一个点是 2 号点,从 1 号点走到 2 号点的一个最短距离是 0 + 2 = 2 <+\infty,所以可以把 +\infty更新为 2,因为 2 更小,同理,我们可以把 3 号点的距离改成 4,因为从 1 号点到 3 号点的边 长度是 4, 从 1 号点走到 3 号点的一个最短距离是 0 + 4 = 4 < +\infty,所以可以把 +\infty更新为 4

第二次迭代,再从没有确定的点当中找到一个最小值,2 和 4 的最小值是 2,所以第 2 个点的最短路就确定了,因为它最小,所以一定是 2,把它加到集合里面,用它来更新其他点到起点的最短路径,由于 2 号点的出边只有一个,只有从 2 → 3,当前从起点走到 3 的最短距离是 4,用 2 更新从起点到 3 号点的距离,1 号点到 2 号点的距离是 2,2 号点到 3 号点的距离是 1,因此当前的距离是比从 1 号点到 3 号点的路径长度更小,所以需要更新 3 号点到起点的距离为 3

最后一轮迭代,只剩下一个点,所以这个点一定可以确定,最短路的长度一定是 3

一共循环 n 次,确定了 n 个点的最短路径,每个点到起点的距离就确定了

用 1 ~ t 这个路径的长度加上 t ~ j 这条边的长度来更新 1 ~ j 这条路径的长度,以上就是 Dijkstra 算法的基本思路,一开始初始化只有起点到起点的距离是 0,其他所有点的距离都是正无穷,n 次迭代,每次找到距离的最小值,用这个最小值去更新其他所有点到起点的距离

时间复杂度分析

外层循环 n 次,内层找最小值循环 n 次,更新也是循环 n 次,两重循环,总的时间复杂度为 O(n^2)

以下参考

Dijkstra求最短路算法

常见问题合集

1.0x3f为什么赋值的时候可以memset(dist,0x3f,sizeof dist)但是到后面验证的时候必须是if(dist[n]==0x3f3f3f3f)而不能是 if(dist[n]==0x3f)?
    回答:memset是按字节来初始化的,int包含4个字节,所以初始化之后的值就是0x3f3f3f3f。
2.为什么要用memset(dist,0x3f,sizeof dist)来初始化?
    回答:0x3f3f3f3f的十进制是1061109567,是1e9级别的(和0x7fffffff一个数量级,0x7fffffff代表了32-bit int的最大值),而一般场合下的数据都是小于1e9的,所以它可以作为无穷大使用而不致出现数据大于无穷大的情形。 另一方面,由于一般的数据都不会大于10^9,所以当我们把无穷大加上一个数据时,它并不会溢出(这就满足了"无穷大加一个有穷的数依然是无穷大"),事实上0x3f3f3f3f+0x3f3f3f3f=2122219134,这非常大但却没有超过32-bit int的表示范围,所以0x3f3f3f3f还满足了我们"无穷大加无穷大还是无穷大"的需求。
3.for(int i=0;i<n;i++) { t=-1;} 这里为什么t要赋值为 -1?
    回答:由于每一次都要找到还没有确定最短路距离的所有点中,距离当前的点最短的点。t = - 1是为了在st这个集合中找第一个点更新时候的方便所设定的。
4.如果是问编号a到b的最短距离该怎么改呢? (好问题)
    回答: 初始化时将 dist[a]=0,以及返回时return dist[b]。

5.图里面存在重边和自环,什么叫重边?什么叫自环?

    回答: 自环是一条从自己出发又回到自己的边,重边是指两个点之间有多条边。


6.自环和重边对 Dijkstrea算法有影响吗?
    回答: 自环在朴素版 dijkstra 算法中是没有任何影响的,在最短路问题里面,如果所有边都是正的,自环显然不会出现在最短路里面。因此自环的权值是多少都可以,只要不是负数就行。如果是重边的话,我们取重边中的最小值(两个点之间只要保留一条长度最短的边就可以了)即代码g[x][y]=min(g[x][y],z)。
7.为什么要用邻接矩阵去存储,而不是邻接表?
    回答: 我们采用邻接矩阵还是采用邻接表来表示图,需要判断一个图是稀疏图还是稠密图。稠密图指的是边的条数|E|接近于|V|²,稀疏图是指边的条数|E|远小于于|V|²(数量级差很多)。本题是稠密图,有 500 个点、10w 条边,显然稠密图用邻接矩阵存储比较节省空间,如果是稀疏图则用邻接表存储。

8.为什么没有区分有向边和无向边,有向图和无向图的最短路有什么区别吗?

    回答: 无向图其实是一种特殊的有向图,最短路算法只需要考虑有向图就可以了,我们可以用有向图的算法来直接解决无向图的问题。假设告诉我们 a 和 b 之间有一条无向边的话,我们只要连接一条 a → b 的边,再连接一条 b → a 的边就可以了,等价于一条无向边。

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

using namespace std;

const int N = 510;

int n, m;
//存储图
int g[N][N];
//dijkstra算法里面的距离 表示从1号点走到每个点 当前的最短距离是多少
int dist[N];  
//表示每个点的最短路是不是已经确定了
bool st[N];

//dijkstra算法实现
int dijkstra()
{
    //先把所有的距离初始化为正无穷
    memset(dist, 0x3f, sizeof dist);
    //把1号点的距离初始化成0
    dist[1] = 0;
    //迭代n次 每次第一步先找最小值->找到当前没有确定最短路长度的点当中距离最小的那个
    for(int i = 0; i < n; i ++ )
    {
        //t等于-1 表示还没有确定节点编号
        int t = -1;
        //遍历所有点
        for(int j = 1; j <= n; j ++ )
            //如果当前这个点还没有确定最短路的话 dist[t]>dist[j]说明当前这个节点编号的dist[t]不是最短的
            if(!st[j] && (t == -1 || dist[t] > dist[j]))
                //从节点编号1开始
                t = j;
        //如果t等于n可以直接break 说明已经找到最短距离
        if(t == n) break;
        //把t加到集合里面去
        st[t] = true;
        //拿t更新其他所有点的距离
        for(int j = 1; j <= n;j ++ )
            dist[j] = min(dist[j], dist[t] + g[t][j]);
    }
    //如果dist[n]等于正无穷,说明1和n是不连通的 返回-1
    if(dist[n] == 0x3f3f3f)return -1;
    //否则返回n的最短距离
    return dist[N];
}

int main()
{
    //读入点数和边数
    scanf("%d%d", &n, &m);
    //初始化邻接矩阵 
    memset(g, 0x3f, sizeof g);
    //读入m条边 a和b之间可能有多条边 
    while(m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        //如果存在多条边只保留长度最短的一条边
        g[a][b] = min(g[a][b], c);
    }
    //输出1号点到n号点的最短距离
    int t = dijkstra();
    printf("%d\n", t);
    return 0;
}

堆优化版 DijKstra 算法

如果是一个稀疏图,m 和 n 是一个级别的,n 和 m ≤ 10^5,此时就不能使用朴素版 DijKstra 算法,因为 n^210^{10} 会导致超时,但是可以使用堆优化版的 DijKstra 算法,时间复杂度为 O(mlogn)

接下来看一下整个过程哪些部分可以做优化,可以发现发现迪杰斯特拉算法最慢的一步是【找不在 s 中距离最近的点,时间复杂度是 n^2】在一堆数中找一个最小的数可以用堆来实现,如果用堆来做,这一步的时间复杂度就可以变成 O(1),每次修改一个数,在堆当中修改一个数的时间复杂度是 logn,一共要修改 m 次,所以这一步的总共的计算量就会变成 mlogn,所以我们只要用堆存储所有点到起点的距离,可以把时间复杂度优化为 O(mlogn)

Trie 树、并查集、堆

STL 中的堆就是优先队列,且不支持删除任意一个元素和修改任意一个元素的操作,自己实现的堆可以有这样的操作

堆有两种实现方式,第一种方式手写堆,手写堆可以时时刻刻保证堆里面只有 n 个数,这个写法支持修改堆当中任意一个元素,实现比较复杂,需要写一个映射。第二种方式可以用优先队列,但是不支持修改堆当中任意一个元素这样的操作,它的实现方式是冗余,每次修改都往堆当中插入一个新的数,好处是不用手写堆,坏处是堆里面总共的元素可能有 m 个,如果使用优先队列,DijKstra 算法的时间复杂度就会变成 O(mlogm),但是 m 一般来说都小于 n^2,所以 logm ≤ logn^2logn^2  等于 2logn,所以 logmlogn 是一个级别的,所以 mlogmmlogn 时间复杂度是一样的,可以直接写成 mlogn

所以一般来说,堆优化 DijKstra 算法不用手写堆,直接用 c++  里面的优先队列就可以了,但是优先队列里面可能会存在很多冗余,例如 1 号点存储了一个距离 10,又存储了一个距离 15。在我们找最小值的时候,由于有冗余的存在,所以说我们当前找到的最小值有可能之前已经确定过了,直接用 st[ ] 数组去判断就可以了,如果之前确定过的话就直接 continue,把它扔掉就可以了

堆优化版的 DijKstra 算法其实就是用堆对朴素版的一些操作进行优化,接下来看一下代码实现

由于是一个稀疏图,所以需要改变存储方式,存储方式改成邻接表的形式

Dijkstra 求最短路 II

用邻接表存储,重边无所谓,由于最短路算法保证了一定会选择最短的边,所以不需要对重边做特殊的处理

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

using namespace std;

const int N = 100010;

//用一个堆来维护所有点的距离 维护距离的时候需要知道节点编号是多少 所以堆里面其实存储的是一个pair
typedef pair<int, int> PII; 

int n, m;
//邻接表的方式存储图
int h[N], e[N], ne[N], idx;
//邻接表里面需要存储一个权值 用w来表示权重
int w[N];
//迪杰斯特拉算法里面的距离 表示从1号点走到每个点当前的最短距离是多少
int dist[N];  
//表示每个点的最短路是不是已经确定了
bool st[N];


//插入一条 a → b 的边 其实就是在 a 所对应的邻接表里面插入一个节点 b
void add(int a, int b, int c)
{
    //存储节点 3 的值
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ; 
    w[idx] = c;
}

//dijkstra算法实现
int dijkstra()
{
    //先把所有的距离初始化为正无穷
    memset(dist, 0x3f, sizeof dist);
    //把1号点的距离初始化成0
    dist[1] = 0;
   
    //小根堆 补齐参数
    priority_queue<PII, vector<PII>, greater<PII>> heap;
    //首先应该把1号点放上去,因为1号点已经知道最短距离了,需要把1号点放进去来更新其他所有点
    //把1号点放进来 距离是0 编号是1
    hash.push({0, 1});
    //当堆不为空
    while(heap.size())
    {
        //每次找到当前距离最小的点也就是堆的起点
        auto t = heap.top();
        heap.pop();
        //ver表示点的编号 distance表示距离
        int ver = t.second, distance = t.first;
        //如果ver这个点之前出来过,说明当前这个点是一个冗余备份 没有必要再处理它 直接continue就可以了
        if(st[ver]) continue;
        //用当前这个点来更新其他所有点 遍历这个点的所有邻边
        for(int i = h[ver]; i != -1; i = ne[i])
        {
            //用j存储点的编号
            int j = ne[i];
            if(dist[j] > distance + w[i])
            {
                //更新方式和朴素迪杰斯特拉算法一样
                dist[j] = distance + w[i];
                //把j这个点放到优先队列里面去
                heap.push({dist[i], j});
            }
        }
    }
    //如果最后存在正无穷,说明1和n是不连通的 返回-1
    if(dist[n] == 0x3f3f3f)return -1;
    //否则返回n的最短距离
    return dist[N];
}

int main()
{
    //读入点数和边数
    scanf("%d%d", &n, &m);
    //单链表的初始化让头节点指向-1 → 如果有N个头节点让N个头节点全部指向-1
    memset(h, -1, sizeof h);
    //读入m条边 a和b之间可能有多条边 
    while(m -- )
    {
        int a, b, c;
        scanf("%d%d%d", &a, &b, &c);
        //读入m条边    
        add(a, b, n);
    }
    //输出1号点到n号点的最短距离
    int t = dijkstra();
    printf("%d\n", t);
    return 0;
}

为什么Dijkstra算法不能处理带负权边的图?

转载

为什么Dijkstra算法不能处理带负权边的图

Dijkstra 算法在运行过程中维持的关键信息是一组节点集合 S,从源节点s到该集合中每个节点之间的最短路径已经被找到。算法重复从节点集合 V-S 中选择最短路径估计最小的节点 u,将u加入到集合 S,然后对所有从u出发的边进行松弛操作。

当把一个节点选入集合 S 时,即意味着已经找到了从源点到这个点的最短路径,但若存在负权边,就与这个前提矛盾,可能会出现得出的距离加上负权后比已经得到 S 中的最短路径还短。(无法回溯)

Bellman-Ford 算法

迭代 n 次,每次循环所有边 a、b、w,a、b、w 表示存在一条从 a 走到 b 的边,权重是 w。边的存储方式不一定需要写成邻接表,可以用结构体,只要能让我们遍历到所有边就可以了,Bellman-Ford 算法存储边的方式是定义一个结构体,如果有 m 条边就开 m 个结构体数组就可以了,如果循环所有边的话,直接遍历这个结构体数组就可以了

遍历所有边的时候更新,与迪杰斯特拉算法的更新方式类似,看一下从 1 号点走到 a 这个点,再从 a 这个点走到 b 这个点这条路径是不是比从 1 号点走到 b 的路径要短。如果短的话,就把 b 这个点的路径更新

第一重循环循环 n 次,第二重循环循环所有边,每次循环的时候更新一下最短距离,循环 n 次之后,Bellman-Ford 算法证明了所有边的距离一定满足 dist[b] ≤  dist[a] + w 这个等式叫作三角不等式,更新的过程叫作松弛操作

Bellman-Ford 算法用于处理有负权边的图,求最短路的时候,如果有负权回路(一圈的长度 < 0),最短路不一定存在

例如下面这种图,题目给出的 2、3、4 是一个负权回路,想求从 1 号点到 5 号点的最短路径,由于 2、3、4 是一个负权回路,这一圈的长度小于 0 ,从 1 号点走到 2 号点,从 2 号点开始,转一圈,长度就会减 1,转两圈长度就会减 2,可以转无穷多圈,总的距离就会变成 -\infty,再走出去,距离还是 -\infty,所以如果图里面存在负权回路,从 1 号点走到 n 号点的距离就会变成 -\infty,就不存在了

需要注意,如果能求出来最短路的话,图里面一般情况下不能有负权回路,如果有负权回路,最短路可能是负无穷,同时 Bellman-Ford 算法可以判断图里面是不是存在负权回路,但是时间复杂度比较高

迭代 k 次,求的最短距离是什么含义呢?

迭代 k 次指的是从 1 号点经过不超过 k 条边走到每个点的最短路的距离。如果迭代第 n 次的时候,又更新了某些边的话,那么就说明存在一条最短路径,这个最短路径上有 n 条边

如果一个路径上有 n 条边意味着什么呢?

n 条边就意味着有 n + 1 个点,1 到 n 只有 n 个点,由抽屉原理,一定有两个点编号一样,那么这个路径上一定存在环,存在环一定是更新过之后,所以这个环一定是负环,所以第 n 次迭代有更新的话,就说明存在一条边数为 n 的最短路径,说明存在负环
Bellman-Ford 算法可以检测负环
如果第 n 次循环更新了边,说明一条最短路上有 n 个点,也就是有 n + 1 条边 根据抽屉原则 有两个点是同样的点
就形成了负环
打个比方说 3 个点 3条边 必定为环


但不是有负环的一定求不出最短路 参考上图

如果 2 的负环边不在1-n的最短路上,就没有影响,只有负环是在从 1 号点到 n 号点的某个路径当中的时候,才不存在最短路径,但是 Bellman-Ford 算法求负环的时间复杂度比较高,推荐用 SPFA 算法

时间复杂度分析:第一重循环 n,第二重循环 m,时间复杂度为 O(nm)

一般情况下 SPFA 算法优于 Bellman-Ford 算法,但是有些题只能用 Bellman-Ford 算法,例如下面这道题目,求最多经过不超过 k 条边的最短路径

有边数限制的最短路

如果要用 SPFA 算法就一定要求图里面不存在负环

题目说明边权可能是负数,不能使用迪杰斯特拉算法,迪杰斯特拉算法要求不能存在负权边,求从 1 号点到 n 号点最多经过 k 条边的最短距离,如果不存在输出 impossible,注意图中可能存在负权回路,由于可能存在负权回路,最短路径不一定存在,但是这一题有限制,题目给出经过的边数最多不超过 k 条,由于最多只能经过 k 条边,所以最多在负环里面最多只能转 k 次,如果限制了最短路经过边的个数的话,有负环也就无所谓了

k =  1,说明从 1 号点到 3 号点的边数不能超过一条

虽然有两条路径,但是只有第一条路径可以选择,因为题目要求只能经过一条边,不可以经过两条边,所以只能直接从 1 号点走到 3 号点,只经过一条边,最短路的长度是 3。最短路的距离不能是 2,因为边数有限制,只能经过一条边

在做 Bellman-Ford 算法的时候,需要特殊处理 每次进行一次新的迭代之前,需要先把 dist 数组备份一下。从 1 号点到 2 号点的距离是 1,从 2 号点 到 3 号点的距离是 1,从 1 号点到 3 号点的距离是 3,k 等于 1 说明从 1 号点到 3 号点的边数不能超过 1 条。如果不加备份,枚举所有边的时候,可能发生串联,第一次枚举的边是 1 → 2,第一次迭代的时候,会把 2 号点的距离更新成 1,然后我们再枚举第二条边,枚举第二条边的时候,2 的距离其实发生了变化,2 的距离变成了 1,1 + 1 = 2,由此得到 3 的最短距离就是 2

虽然只迭代了一次,但是在更新的过程中,可能发生串联,例如先更新了 b,再拿 b 更新其他点,再拿其他点更新其他点,会发生串联,不能保证 k = 1 这个限制了

如何保证不发生串联呢?


保证更新的时候,只用上一次迭代的结果,先备份一下,backup 里面存储的就是上一次迭代的结果,只用上一次迭代的结果就不会发生串联了

还是刚才的例子,先更新 2 号点,2 号点的距离就变成 1,然后更新 3 号点,更新 3 号点的时候用的是 backup 数组中的距离来更新,2 号点的距离还是 +\infty+\infty+ 1 >+\infty,3 号点的距离就不会更新成 2 

Bellman-Ford 算法图解

转载

带负权边的最短路径 – bellman-ford算法

 

参考

bellman-ford 算法 - AcWing

1.为什么要用0x3f3f3f3f / 2?
看下图

如果我们的判断最后dist[n] 是不是无解
之前用的语句是 if(dist[n] == 0x3f3f3f3f)
如果存在负权边
5 号点到 n 号点的边的长度是 -2,并且dist[5] = +\infty,1 号点到不了 5 号点,1 号点也到不了 n 号点,dist[n] = +\infty,但是 5 号点的 +\infty可能会把 n 号点的 +\infty 更新为+\infty- 2,由于存储的 +\infty 并不是真正意义上的 +\infty,而是 0x3f3f3f,虽然 n 最后可能到不了,但是 n 不是 +\infty,而是 +\infty减去一个数,初始的判断条件 if(dist[n] == 0x3f3f3f3f) 就不能判断是否是无解了
当然减也不会减太多,m 给定的范围是 10000,k 给定的范围是 500,也就是最多有 0x3f3f3f3f - 5x10^6 的值
这个值是大于0x3f3f3f3f / 2 的
所以我们把条件变为dist[n] < 0x3f3f3f3f / 2 作为dist[n] 有解,如果大于一个比较大的数也可以表示它到不了

2.为什么有负环是正无穷?

想求下面这个图的最短路,边的权值如图所示,看 1 号点到 5 号点的最短路径长度是多少,可以发现从 2 开始沿着这个环转一圈的话,最短路的长度就会减 6,可以转无穷圈,转无穷圈之后最短路的长度就会变成 -\infty,变成 -\infty之后再出去 -\infty + 10,还是 -\infty,所以我们想把从 1 号点到 5 号点的路径变成多小就可以变成多小,只要有负环存在,就可以在负环里转无限次,转无限次就会变成负无穷,所以只有 1 号点到 n 号点的路径中存在负环的话,从 1 号点到 n 号点的路径就是 -\infty,也就是不存在从 1 号点到 n 号点的路径

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

using namespace std;

//点数 边数
const int N = 510, M = 10010;

//k表示最多经过k条边
int m, n, k;
//距离 备份
int dist[N], backup[N];

//定义一个结构体来存储所有边
struct Edge
{
    //a表示边的起点 b表示边的终点 w表示权重
    int a, b, w;
}edgs[M];

int bellman-ford()
{
    //初始化
    meset()
    //求不超过k条边的最短路长度 迭代k次
    for(int i = 0; i < k; i++ )
    {
        //需要特殊处理 每次进行一次新的迭代之前 需要先把dist数组备份一下
        memcpy(backup, dist, sizeof dist);
        //遍历所有边
        for(int j = 0; j < m; j++ )
        {
            int a = edgs[j].a, b = edgs[j].b, w = edgs[j].w;  
            //只用上一次迭代的结果更新来当前的距离就不会发生串联了
            dist[b] = min(dist[b], backup[a] + w);
        }
    }
    //判断 路径不存在返回-1
    if(dist[n] > 0x3f3f3f / 2) return -1;
    //返回找到的路径
    return dist[n];
}

int main()
{
    //读入m、n、k
    scanf("%d%d%d", &n, &m, &k);
    //读入m条边
    for(int i = 0; i < m; i++ )
    {
        //起点终点 权重
        int a, b, w;
        scanf("%d%d%d", &a, &b, &w);
        //存储边
        edges[i] = {a, b, w};
    }
    //调用 Bellman-Ford 算法
    int t = bellman-ford()
    //如果t等于-1的话说明最短路长度不存在
    if(t == -1)
        //输出impossible
        puts("impossible");
    else 
        //输出最短距离
        printf("%d\n", t);
    return 0;
}

求负环

参考一位大佬的分享,写得很好,强推  求负环 - AcWing

给定一张有向图 ( 无向图的每条边可以看作两条方向相反的有向边,从而按照有向图处理 ),每条边都有一个权值(长度)。若一条边的权值是负数,则称它是负权边。若图中存在一个环,环上各边的权值之和是负数,则称这个环为 " 负环 " 。
多种求解单源最短路径问题的算法。这里回顾它们的适用条件

如果图中存在负环,那么直观表现为:无论经过多少轮迭代,总存在有向边 (x,y,z),使得 dist[y] > dist[x] + z,Bellman-Ford 与 SPFA 算法永远不能结束。

根据抽屉原理,若存在一个 dist[x],从起点 1 到节点 x 的最短路包含 ≥ n 条边,则这条路径必然重复经过了某个节点 p , 换言之,这条最短路径上存在一个环,环上各点都能更新下一个点的 dist 值。p 绕这个环一圈,最终能更新它自己。因此,这个环的总长度是负数。每绕一圈,最短路长度只会越来越小,不可能收敛到每条边都满足三角不等式。基于这个理论有以下判定法则:

Bellman-Ford 算法判断负环

若经过 n 轮迭代,算法仍未结束(仍有能力产生更新的边),则图中存在负环。

​若 n−1 轮迭代之内,算法结束(所有边满足三角不等式),则图中无环。

三角不等式,即在三角形中两边之和大于第三边

SPFA 判定负环

设 cnt[N] 表示从 1 到 x 的最短路径包含的边数, cnt[1]=0 。 当执行更新 dist[y]=dist[x]+z 时,同样更新 cnt[y]=cnt[x]+1 。若发现 cnt[y] ≥ n,则图中有负环。若算法正常结束,则图中没有负环。
统计每个点入队的次数,如果某个点入队 n 次,则存在负环。

在处理负环时, SPFA 一般是效率比较低的,时间复杂度在 O(nm) 左右,此时一般都会超时,经验之谈:可以在 当所有点的入队次数超过 2n 时,我们认为图中有很大可能存在负环。

使用 SPFA 判负环时的两个点:                

    所有节点在初始化全部入队
    dist[N]

数组可以初始化为 0。

假想一个虚拟源点,向图中每个点连一条边权值为 0 的边,原图中存在负环等价于加上虚拟源点后也存在负环,在新图中,负环一定是可以从 虚拟源点 走到的。在新图中一开始将 虚拟源点 插入队列,并且由于虚拟源点和所有点都是相连的,所以接下来会将 其他所有点加入队列中。那么在新图上的第一次迭代之后就等价于在原图上第一次就将所有点都加到队列中。

dist[N] 一开始可以被初始化 0 的原因是 如果图中存在负环,那么无论是 0 还是 0x3f3f3f3f,都会被这个负环最后减成 负无穷。并且次数是无限次,那么更新也就会超过 n 次,也就会达到求负环的目的。所以不管初值是多大的有限值,在经过无限次的负环后都会变成负无穷。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

qiuqiuyaq

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

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

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

打赏作者

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

抵扣说明:

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

余额充值