图的最短路算法
图的存储
图:点集、边集。
我们程序中所有存储的信息是边的信息,点的信息没有必要存。
存储方式:
- 邻接矩阵(缺点:浪费储存空间。优点:可以快速索引两个点之间边的信息)
- 邻接表(c++用动态数组实现,c用链表实现)邻接表的思想是,对于图中的每一个顶点,用一个数组来记录这个点和哪些点相连。优点:节省存储空间,有几条边的信息就用几个存储空间。缺点:不好查找边的信息O(n)。
- 链式前向星(本质是邻接表)。当新增加一条边时,数据变动的方式其实就是链表的头插法。
最短路问题
- 单源最短路问题。(单一源点到其他点的最短路径问题)。
- 多源最短路径,任意两点之间的最短路径。
单源最短路算法
Dijkstra算法
解决单源最短路问题。但是它处理不了带负权值的图。因为带负权值的边可能会更新已经被扩展过的点,从而导致那时从该点往外扩展的权值都是错误的。
算法流程:
我们定义图中所有的顶点集合为V,已经确定从源点出发的最短路径的顶点集合为U,初始集合U为空。初始每个顶点到源点的距离为INF,源点到自身的距离为0。
-
从v-u中找出一个距离源点最近(路径最短)的顶点v,将v加入集合U。
-
然后根据这个最短路径来更新与顶点v相邻的、不在集合U中的顶点最短路径,这一步叫做松弛操作。
- 重复步骤1和2,知道所有的点都归入集合U。
我们乍一看像bfs,但是我们可以发现bfs每次是扩展一层,Dijkstra算法是每次扩展一个点。实质是广度优先搜索加贪心。
核心思想:就是一直维护一个还没有确定最短路径的点的集合,然后每次从这个集合中选出一个最小的点去更新其他的点。
堆优化
如果每次暴力枚举选取距离最小的元素,则总的时间复杂度是 O ( V 2 ) O(V^2) O(V2)。所以我们可以考虑用堆优化,用一个set来维护点的集合,这样时间复杂度就优化到了 O ( ( V + E ) l o g V ) O((V+E)logV) O((V+E)logV),常用来解决稠密图问题。
算法需要两个数组,一个数组用来存储源点到图中所有点的距离值,另一个数组用来存储该点是否已经被用来扩展过了。
算法的改进:当某个点的最短距离被更新,那么标记值就置为0。这样就能适用于带负权值的图。
在时间复杂度上的改进:因为原始的算法,在每一次寻找新一轮的扩展点时,都要扫描一遍数组,所以时间复杂度为O(n^2)。但是我们可以拿优先队列来优化。在优先队列中存储一个pair对,存储边的编号和距离源点的距离,距离小的在队列头部。优化之后的时间复杂度为O(nlogn)。
最短路的代码都是差不多的形式
最简单版本的 D i j s k t r a Dijsktra Dijsktra算法:
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
#define MAX_M 500000
#define MAX_N 10000
#define INF 0x3f3f3f3f
//图的存储一般采用链式前向星的方式储存
struct Edge {
int to, next, c;
}g[MAX_M + 5]; //开的内存大小要跟边的数量相关
//head数组的值表示以当前下标为起始点的第一条边的结构体数组编号
int head[MAX_N + 5], cnt = 0;
int dis[MAX_N + 5], vis[MAX_N + 5];
//0编号是不存边的,因为我们用0表示结束位,从1开始存边
inline void add(int a, int b, int c) {
//头插法
g[++cnt].to = b;
g[cnt].next = head[a];
g[cnt].c = c;
head[a] = cnt;
}
void dij(int s, int n) {
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
//扫描n-1轮
for (int i = 1; i < n; i++) {
int ind = -1;
//找到未被标记的最短路径
for (int j = 1; j <= n; j++) {
if (vis[j]) continue;
if (ind == -1 || dis[j] < dis[ind]) ind = j;
}
//结束是以该点为起点的所有边都查过了,标记位为0
for (int j = head[ind]; j; j = g[j].next) {
//检查从ind出发所能扩展的边的路径与原始值相比能否更新
if (dis[g[j].to] > dis[ind] + g[j].c) {
dis[g[j].to] = dis[ind] + g[j].c;
}
}
vis[ind] = 1;
}
return;
}
int main() {
int n, m, s;
cin >> n >> m >> s;
for (int i = 0;i < m; i++) {
int a, b, c;
cin >> a >> b >> c;
//注意这里处理的是有向边,若为无向图,则要加一个add(b,a,c)
add(a, b, c);
}
dij(s, n);
for (int i = 1; i <= n; i++) {
i == 1 || cout << " ";
if (dis[i] == INF) {
cout << 2147483647;
} else {
cout << dis[i];
}
}
return 0;
}
使用优先队列改进和重标记的Dijkstra算法(可适用于负权边图)
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <queue>
#include <iostream>
using namespace std;
#define MAX_N 10000
#define MAX_M 500000
#define INF 0x3f3f3f3f
struct Edge{
int to, next, c;
}g[MAX_M + 5];
//head数组储存的是以当前下标(或加1)为起点的第一条边对应的结束点
int head[MAX_N + 5];
int dis[MAX_N + 5], vis[MAX_N + 5];
struct Data {