三类最短路径算法简单介绍(Dijkstra、SPFA、Floyd)
1. D i j k s t r a Dijkstra Dijkstra算法
解决单源最短路径问题常用 Dijkstra 算法,用于计算一个顶点到其他所有顶点的最短路径。Dijkstra 算法的主要特点是以起点为中心,逐层向外扩展(这一点类似于 bfs,但是不同的是,bfs 每次扩展一个层,但是 Dijkstra 每次只会扩展一个点),每次都会取一个最近点继续扩展,直到取完所有点为止。
注意:Dijkstra 算法要求图中不能出现负权边。
①、 D i j k s t r a Dijkstra Dijkstra算法流程
我们定义带权图 G G G所有顶点的集合为 V V V,接着我们再定义已确定从源点出发的最短路径的顶点集合为 U U U,初始集合 U U U为空,记从源点 s s s出发到每个顶点 v v v的距离为 d i s t v dist_v distv,初始 d i s t s dist_s dists=0。接着执行以下操作:
-
从 V − U V−U V−U中找出一个距离源点最近的顶点 v v v,将 v v v加入集合 U U U。
-
并用 d i s t v dist_v distv和顶点 $v $连出的边来更新和 v v v相邻的、不在集合 U U U中的顶点的 d i s t dist dist,这一步称为松弛操作。
-
重复步骤 1 和 2,直到 V = U V=U V=U或找不出一个从 s s s 出发有路径到达的顶点,算法结束。
如果最后 V ≠ U V \neq U V=U,说明有顶点无法从源点到达;否则每个 d i s t i dist_i disti表示从 s s s 出发到顶点$ i $的最短距离。
Dijkstra 算法的时间复杂度为 O ( V 2 ) \mathcal{O}(V^2) O(V2),其中 V V V 表示顶点的数量。
Dijkstra 是解决无负边权的图的单源最短路问题,经常使用邻接表存储。
不优化的时间复杂度是 O ( V 2 + E ) O(V^2 + E) O(V2+E)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
const int inf = 0x3f3f3f3f;
struct edge {
int v, w, fail;
edge() {}
edge(int _v, int _w, int _fail) {
v = _v;
w = _w;
fail = _fail;
}
} e[M << 1];
int head[N], len;
void init() {
memset(head, -1, sizeof(head));
len = 0;
}
void add(int u, int v, int w) {
e[len] = edge(v, w, head[u]);
head[u] = len++;
}
void add2(int u, int v, int w) {
add(u, v, w);
add(v, u, w);
}
int n, m;
int dis[N];
bool vis[N];
void dijkstra(int u) {
memset(vis, false, sizeof(vis));
memset(dis, 0x3f, sizeof(dis));
dis[u] = 0;
for (int i = 0; i < n; ++i) {
int mi = inf;
for (int j = 1; j <= n; ++j) {
if (!vis[j] && dis[j] < mi) {
mi = dis[u = j];
}
}
if (mi == inf) {
return;
}
vis[u] = true;
for (int j = head[u]; ~j; j = e[j].fail) {
int v = e[j].v;
int w = e[j].w;
if (!vis[v] && dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
}
}
}
}
int main() {
init();
int u, v, w;
cin >> n >> m;
while (m--) {
cin >> u >> v >> w;
add2(u, v, w);
}
dijkstra(1);
cout << dis[n] << endl;
return 0;
}
②、基于小根堆优化的 D i j k s t r a Dijkstra Dijkstra算法
用一个set来维护点的集合,这样的时间复杂度就优化到了 O ( ( V + E ) log V ) \mathcal{O}((V+E)\log V) O((V+E)logV),对于稀疏图的优化效果非常好
const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v, int w) { // 插入带权有向边
e[eid].v = v;
e[eid].w = w;
e[eid].next = p[u];
p[u] = eid++;
}
void insert2(int u, int v, int w) { // 插入带权双向边
insert(u, v, w);
insert(v, u, w);
}
typedef pair<int, int> PII;
set<PII, less<PII> > min_heap;
int dist[MAX_N]; // 存储单源最短路的结果
bool vst[MAX_N]; // 标记每个顶点是否在集合 U 中
bool dijkstra(int s) {
// 初始化 dist、小根堆和集合 U
memset(vst, 0, sizeof(vst));
memset(dist, 0x3f, sizeof(dist));
min_heap.insert(make_pair(0, s));
dist[s] = 0;
for (int i = 0; i < n; ++i) {
if (min_heap.size() == 0) { // 如果小根堆中没有可用顶点,说明有顶点无法从源点到达,算法结束
return false;
}
// 获取堆顶元素,并将堆顶元素从堆中删除
set<PII, less<PII> >::iterator iter = min_heap.begin();
int v = iter->second;
min_heap.erase(*iter);
vst[v] = true;
// 进行和普通 dijkstra 算法类似的松弛操作
for (int j = p[v]; j != -1; j = e[j].next) {
int x = e[j].v;
if (!vst[x] && dist[v] + e[j].w < dist[x]) {
// 先将对应的 pair 从堆中删除,再将更新后的 pair 插入堆
min_heap.erase(make_pair(dist[x], x));
dist[x] = dist[v] + e[j].w;
min_heap.insert(make_pair(dist[x], x));
}
}
}
return true; // 存储单源最短路的结果
}
③、基于优先队列优化的 D i j k s t r a Dijkstra Dijkstra算法
我们在 n o d e node node节点里面记录对应的点的最短路,然后每次更新一个点的最短路后都把这个点压入到优先队列里面(不管之前有没有被压入到队列里面),这样就一定能够保证优先队列对的性质不会改变
这个代码的时间复杂度实际上会比用真正的堆要慢一点,因为有的点可能会入队多次,但是每一条边最多导致一次入队,所以这个算法的时间复杂度为 O ( E log E ) \mathcal{O}(E\log E) O(ElogE)。其中$ E $为边的数量。
const int MAX_N = 10000;
const int MAX_M = 100000;
const int inf = 0x3f3f3f3f;
struct edge {
int v, w, next;
} e[MAX_M];
int p[MAX_N], eid, n;
void mapinit() {
memset(p, -1, sizeof(p));
eid = 0;
}
void insert(int u, int v, int w) { // 插入带权有向边
e[eid].v = v;
e[eid].w = w;
e[eid].next = p[u];
p[u] = eid++;
}
void insert2(int u, int v, int w) { // 插入带权双向边
insert(u, v, w);
insert(v, u, w);
}
int dist[MAX_N]; // 存储单源最短路的结果
bool vst[MAX_N]; // 标记每个顶点是否在集合 U 中
struct node {
int u;
int dist;
node(int _u, int _dist) : u(_u), dist(_dist) {}
bool operator < (const node &x) const {
return dist > x.dist;
}
}; // 记录点的结构体
bool dijkstra(int s) {
// 初始化 dist、小根堆和集合 U
memset(vst, 0, sizeof(vst));
memset(dist, 0x3f, sizeof(dist));
priority_queue<node> min_heap;
dist[s] = 0;
min_heap.push(node(s, 0));
while (!min_heap.empty())
// 获取堆顶元素,并将堆顶元素从堆中删除
int v = min_heap.top().u;
min_heap.pop();
if (vst[v]) {
continue;
}
vst[v] = true;
// 进行和普通 dijkstra 算法类似的松弛操作
for (int j = p[v]; j != -1; j = e[j].next) {
int x = e[j].v;
if (!vst[x] && dist[v] + e[j].w < dist[x]) {
dist[x] = dist[v] + e[j].w;
min_heap.push(node(x, dist[x]));
}
}
}
return true;
}
2. S P F A SPFA SPFA算法
SPFA(Shortest Path Faster Algorithm)算法是单源最短路径的一种算法,通常被认为是 Bellman-ford 算法的队列优化,在代码形式上接近于宽度优先搜索 BFS,是一个在实践中非常高效的单源最短路算法。
①、 S P F A SPFA SPFA算法流程
在 S P F A SPFA SPFA 算法中,使用 d i d_i di表示从源点到顶点 i i i的最短路,额外用一个队列来保存即将进行拓展的顶点列表,并用 i n q i inq_i inqi来标识顶点 i i i是不是在队列中。
1.初始队列中仅包含源点,且源点 s s s 的 d s = 0 d_s=0 ds=0。
2.取出队列头顶点 u u u,扫描从顶点 u u u 出发的每条边,设每条边的另一端为 v v v,边 < u , v > <u,v> <u,v> 权值为 w w w,若 d u + w < d v d_u+w<d_v du+w<dv,则
-
将 d v d_v dv修改为 d u + w d_u+w du+w
-
若 v v v 不在队列中,则
-
将 $v $入队
3.重复步骤 2 直到队列为空
最终$ d$ 数组就是从源点出发到每个顶点的最短路距离。如果一个顶点从没有入队,则说明没有从源点到该顶点的路径。
S P F A SPFA SPFA 的空间复杂度为 O ( V ) \mathcal{O}(V) O(V)。如果顶点的平均入队次数为 k k k,则 S P F A SPFA SPFA的时间复杂度为 O ( k E ) \mathcal{O}(kE) O(kE),对于较为随机的稀疏图,根据经验 k k k 一般不超过 4。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1e3 + 9;
const int M = 1e4 + 9;
struct edge{
int v, w, fail;
edge(){}
edge(int _v, int _w, int _fail){
v = _v;
w = _w;
fail = _fail;
}
}e[M << 1];
int head[N], len;
void init(){
memset(head, -1, sizeof(head));
len = 0;
}
void add(int u, int v, int w){
e[len] = edge(v, w, head[u]);
head[u] = len++;
}
void add2(int u, int v, int w){
add(u, v, w);
add(v, u, w);
}
int n, m;
int dis[N];
bool vis[N];
void spfa(int u){
memset(vis, false, sizeof(vis));
vis[u] = true;
memset(dis, 0x3f, sizeof(dis));
dis[u] = 0;
queue<int> q;
q.push(u);
while(!q.empty()){
u = q.front();
q.pop();
vis[u] = false;
for(int j = head[u];~j;j = e[j].fail){
int v = e[j].v;
int w = e[j].w;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
if(!vis[v]){
q.push(v);
vis[v] = true;
}
}
}
}
}
int main() {
init();
int u, v, w;
cin>>n>>m;
while(m--){
cin>>u>>v>>w;
add2(u, v, w);
}
spfa(1);
cout<<dis[n]<<endl;
return 0;
}
②、 S P F A SPFA SPFA判断负环
D i j k s t r a Dijkstra Dijkstra不能处理有负权的图,而 S P F A SPFA SPFA 可以处理任意不含负环(负环是指总边权和为负数的环)的图的最短路,并能判断图中是否存在负环
但是 S P F A SPFA SPFA可以用来判断负环,在进行 S P F A SPFA SPFA时,用一个数组 c n t i cnt_i cnti来标记每个顶点入队次数。如果一个顶点入队次数 c n t i cnt_i cnti大于顶点总数 n,则表示该图中包含负环。一般情况下, S P F A SPFA SPFA 判负环都只用在有向图上,因为在无向图上,一条负边权的边就是一个负环了
memset(in, 0, sizeof in);
in[u] = 1;
// 修改入队部分的操作
if(!vis[v]){
q.push(v);
vis[v] = true;
++in[v];
if(in[v] > n){
return true;
}
}
3、 F l o y d Floyd Floyd多源最短路算法
∀ 1 ≤ k ≤ n , d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i ] [ k ] + d p [ k ] [ j ] ) ∀1≤k≤n,dp [i] [j] = min(dp[i] [j],dp[i][k]+ dp [k][j]) ∀1≤k≤n,dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j])
int g[N][N];
void floyd(int n) {
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]);
}
}
}
}