算法笔记1——链式前向星+SPFA+Dijkstra
题目链接:[P3371 【模板】单源最短路径(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)]
1、题目描述(入门)
给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
输入格式
第一行包含三个整数 n, m, s分别表示点的个数、有向边的个数、出发点的编号。
接下来 m 行每行包含三个整数 u, v, w表示一条 u→v 的,长度为 w 的边。
输出格式
输出一行 n 个整数,第 i 个表示 s 到第 i 个点的最短路径,若不能到达则输出 2^31−1。
输入输出样例
输入
1 | 4 6 1 |
输出
1 | 0 2 4 3 |
数据范围:对于 100% 的数据:1 ≤ n ≤104,1 ≤ m ≤ 5×105,保证数据随机。
2、链式前向星
本题根据数据边数m<=500000,邻接矩阵存不下,只能使用静态邻接表存储。
如果说邻接表是不好写但效率好,邻接矩阵是好写但效率低的话,前向星就是一个相对中庸的数据结构。前向星固然好写,但效率并不高。而在优化为链式前向星后,效率也得到了较大的提升。虽然说,世界上对链式前向星的使用并不是很广泛,但在不愿意写复杂的邻接表的情况下,链式前向星也是一个很优秀的数据结构。 ——摘自《百度百科》
链式前向星其实就是静态建立的邻接表,时间效率为O(m),空间效率也为O(m)。遍历效率也为O(m)。
对于下面的数据,第一行5个顶点,7条边。接下来是边的起点,终点和权值。也就是边1 -> 2 权值为1。
1 | 5 7 |
链式前向星存的是以【1,n】为起点的边的集合,对于上面的数据输出就是:
1 | 1 //以1为起点的边的集合 |
我们先对上面的7条边进行编号第一条边是0以此类推编号【0~6】,然后我们要知道两个变量的含义:
- Next,表示与这个边起点相同的上一条边的编号。
- head[ i ]数组,表示以 i 为起点的最后一条边的编号。
head数组一般初始化为-1,为什么是 -1后面会讲到。加边函数是这样的:
1 | void add_edge(int u, int v, int w)//加边,u起点,v终点,w边权 |
我们只要知道next,head数组表示的含义,根据上面的数据就可以写出下面的过程:
- 对于1 2 1这条边:edge[0].to = 2; edge[0].next = -1; head[1] = 0;
- 对于2 3 2这条边:edge[1].to = 3; edge[1].next = -1; head[2] = 1;
- 对于3 4 3这条边:edge[2].to = 4; edge[2],next = -1; head[3] = 2;
- 对于1 3 4这条边:edge[3].to = 3; edge[3].next = 0; head[1] = 3;
- 对于4 1 5这条边:edge[4].to = 1; edge[4].next = -1; head[4] = 4;
- 对于1 5 6这条边:edge[5].to = 5; edge[5].next = 3; head[1] = 5;
- 对于4 5 7这条边:edge[6].to = 5; edge[6].next = 4; head[4] = 6;
遍历函数是这样的:
1 | for(int i = 1; i <= n; i++)//n个起点 |
第一层for循环是找每一个点,依次遍历以【1,n】为起点的边的集合。第二层for循环是遍历以 i 为起点的所有边,k首先等于head[ i ],注意head[ i ]中存的是以 i 为起点的最后一条边的编号。然后通过edge[ j ].next来找下一条边的编号。我们初始化head为-1,所以找到你最后一个边(也就是以 i 为起点的第一条边)时,你的edge[ j ].next为 -1做为终止条件。
3、SPFA(Shortest Path Faster Algorithm)
1. spfa的算法思想(动态逼近法):
- 设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。
- 松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式。所谓对结点i,j进行松弛,就是判定是否dis[j]>dis[i]+w[i,j],如果该式成立则将dis[j]减小到dis[i]+w[i,j],否则不动。
和广搜bfs的区别:
SPFA 在形式上和广度(宽度)优先搜索非常类似,不同的是bfs中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进(重新入队),于是再次用来改进其它的点,这样反复迭代下去。
2. 伪代码
1 | void spfa(s); //求单源点s到其它各顶点的最短距离 |
3. 最短路径的输出?
- 在一个图中,我们仅仅知道结点A到结点E的最短路径长度,有时候意义不大。这个图如果是地图的模型的话,在算出最短路径长度后,我们总要说明“怎么走”才算真正解决了问题。如何在计算过程中记录下来最短路径是怎么走的,并在最后将它输出呢?
- 我们定义一个path[]数组,path[i]表示源点s到i的最短路程中,结点i之前的结点的编号(父结点),我们在借助结点u对结点v松弛的同时,标记下path[v]=u,记录的工作就完成了。
- 如何输出呢?我们记录的是每个点前面的点是什么,输出却要从最前面到后面输出,这很好办,递归就可以了:
1 | void printpath(int k){ |
4. 完整题解代码
1 |
|
5、题目描述(进阶)
题目描述
给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。数据保证你能从 s 出发到任意点。
输入格式
第一行为三个正整数 n, m, s。 第二行起 m 行,每行三个非负整数 ui, vi, wi , 表示从 ui->vi 有一条权值为 wi 的有向边。
输出格式
输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。
输入 #1
1 | 4 6 1 |
输出 #1
1 | 0 2 4 3 |
SPFA算法由于它上限 O(NM) = O(VE)的时间复杂度,被卡掉的几率很大.在算法竞赛中,我们需要一个更稳定的算法:dijkstra.
6、dijkstra算法
- dijkstra是一种单源最短路径算法, 时间复杂度上限为O(n2)(朴素), 在实际应用中较为稳定; 加上堆优化之后更是具有**O((n+m)log2n)**的时间复杂度, 在稠密图中有不俗的表现.
dijkstra的原理/流程?
- dijkstra本质上的思想是贪心,它只适用于不含负权边的图.
- 我们把点分成两类,一类是已经确定最短路径的点,称为”白点”,另一类是未确定最短路径的点,称为”蓝点”
- dijkstra的流程如下:
- 初始化dis[start] = 0, 其余节点的disdis值为无穷大.
- 找一个dis值最小的蓝点 x , 把节点 x 变成白点.
- 遍历 x 的所有出边(x,y,z), 若dis[y] > dis[x] + z, 则令dis[y] = dis[x] + z
- 重复2,3两步,直到所有点都成为白点..
- 时间复杂度为O(n2)
dijkstra的正确性
- 当所有边长都是非负数的时候,全局最小值不可能再被其他节点更新.所以在第2步中找出的蓝点 x 必然满足:dis[x]:已经是起点到 x 的最短路径.我们不断选择全局最小值进行标记和拓展,最终可以得到起点到每个节点的最短路径的长度
- 缺点:不能处理有负权边的情况!
dijkstra的堆优化?
观察dijkstra的流程,发现步骤2可以优化:
我们可以用堆对dis数组进行维护, 用O(log2n)的时间取出堆顶元素并删除, 用O(log2n)遍历每条边, 总复杂度O((n+m)log2n)
7、完整代码
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。
1 |
|