3.2 搜索与图论 | Dijkstra、Bellman-Ford、SPFA、Floyd
这是我的一个算法网课学习记录,道阻且长,好好努力
最短路
重在建图 侧重实现的思路
a —w—> b (a、b为两个点,w为权重)
数据结构中对于稀疏图的定义为:有很少条边或弧(边的条数 |E| 远小于 |V|² )的图称为稀疏图(sparse graph),反之边的条数 |E| 接近 |V|² ,称为稠密图(dense graph)。
单从算法效率上讲,稀疏图与稠密图的分界点大概就在 m = n² / log n 处,但是实际上复杂度是有系数的,所以单从式子上计算也是不太科学的,可以作为一个参考。
朴素的 Dijkstra 算法
最朴素的思想就是按长度递增的次序产生最短路径。即每次对所有可见点的路径长度进行排序后,选择一条最短的路径,这条路径就是对应顶点到源点的最短路径。
步骤:
-
不断运行广度优先算法找可见点,计算可见点到源点的距离长度
-
从当前已知的路径中选择长度最短的将其顶点加入S作为确定找到的最短路径的顶点。
Dijkstra不允许存在负权边
时间复杂度O(n^2)
例题:AcWing 849. Dijkstra求最短路 I
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 -1。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N]; // g[a][b]存储a到b的距离
int dist[N]; // 从1号点走到n号点当前最短距离是多少
bool st[N]; // 最短路是否确定
int dijkstra()
{
memset(dist, 0x3f, sizeof dist); // 将距离初始化为正无穷
dist[1] = 0;
// 循环n-1次(n个节点,n-1次距离)
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])) // 如果j点距离未定 且 未更新或这个距离不是最短的
t = j;
// if (t == n) break;
st[t] = true; // t的距离确定
// 用t更新其他点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
if (dist[n] == 0x3f3f3f3f) return -1; // 如果距离无穷大,说明不存在这条路径,返回-1
return dist[n]; // 返回最短距离
}
int main()
{
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
g[a][b] = min(g[a][b], c); // 处理重边
}
int t = dijkstra();
printf("%d\n", t);
return 0;
}
堆优化版朴素的 Dijkstra 算法
- 堆有两种实现方式:手写堆和优先队列
时间复杂度O(m log n)
priority_queue<Type, Container, Functional>
Type为数据类型, Container为保存数据的容器,Functional为元素比较方式。
如果不写后两个参数,那么容器默认用的是vector,比较方式默认用operator,也就是优先队列是大顶堆,队头元素最大。
例题:AcWing 850. Dijkstra求最短路 II
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 -1。
#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; // 邻接表储存图 w表示权重
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); // 所有距离初始化为INFINITE
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; // ver表示点的编号
if (st[ver]) continue; // 判断该点是否为冗余备份 如果是 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];
}
int main()
{
memset(h, -1, sizeof h); // 初始化成空节点
cin >> n >> m;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
Bellman-Ford 算法
时间复杂度O(nm)
Bellman-Ford算法适用于含有负权边但不含负环的情况
dijkstra 不能解决负权边是因为 dijkstra要求每个点被确定后 st[j] = true,dist[j]就是最短距离了,之后就不能再被更新了(一锤子买卖),而如果有负权边的话,那已经确定的点的dist[j]不一定是最短了。
步骤:
-
初始化源点s到各个点v的路径dis[v] = ∞,dis[s] = 0。
-
进行n - 1次遍历,每次遍历对所有边进行松弛操作,满足则将权值更新。
松弛操作
以a为起点,b为终点,ab边长度为w为例。dis[a]代表源点s到a点的路径长度,dis[b]代表源点s到b点的路径长度。
如果满足dist[b] <= dist[a] + w
(三角不等式) 则将dis[b]更新为dis[a] + w。
一般是没有负权回路的
为了不发生串联,更新时候只用上一次的结果,需要备份
例题:AcWing 850. Dijkstra求最短路 II
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
int n, m, k;
int dist[N], backup[N];
struct Edge
{
int a, b, w;
}edges[M];
int bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 题目要求不超过n条边
for (int i = 0; i < k; i ++ )
{
memcpy(backup, dist, sizeof dist); // 备份
for (int j = 0; j < m; j ++ )
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f / 2) return -1; // 判断是否存在负权
return dist[n];
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ )
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
int t = bellman_ford();
if (t == -1) puts("imposible");
else printf("%d\n", t);
return 0;
}
SPFA 算法
时间复杂度一般O(m)
最坏O(nm)`
其实是队列优化的 bellman-ford 算法。
迭代的时候用一个队列
实现方法:
建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点作为起始点去刷新到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。
判断有无负环:
如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)
这个算法也适用于解决部分使用 Dijkstra 算法的题目;可以解决有负权的问题,也可以解决没有负权的问题,时间可能是优于Dijkstra算法的,适用性和实用性都很好,但是可能会被卡。
例题1:AcWing 851.spfa求最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
请你求出1号点到n号点的最短距离,如果无法从1号点走到n号点,则输出impossible。
数据保证不存在负权回路。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
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 spfa()
{
memset(dist, 0x3f, sizeof dist); // 初始化
dist[1] = 0;
queue<int> q; // 定义一个队列来存储所有待更新的点
q.push(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]) // 用t这个点来更新
{
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;
}
}
}
}
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);
}
int t = spfa();
if (t == 0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
return 0;
}
例题2:AcWing 851.spfa判断负环
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。请你判断图中是否存在负权回路。
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], 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 ++ ;
}
int 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; // 超过n 则存在负环
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;
}
Floyd 算法
“一个一个多点的小涟漪,最后小涟漪铺满整个水面。”
一个一个的点更新邻接矩阵中的距离。
时间复杂度是O(n^3)
例题:AcWing 854.Floyd求最短路
给定一个n个点m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定k个询问,每个询问包含两个整数x和y,表示查询从点x到点y的最短距离,如果路径不存在,则输"impossible”。
数据保证图中不存在负权回路。
#include <iostream>
using namespace std;
const int N = 110, INF = 1e9;
int n, m, k;
int d[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 ++ )
d[i][j] = min(d[i][j], d[i][j] + d[k][j]);
}
int main()
{
cin >> n >> m >> k;
// 初始化邻接矩阵
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;
while (m -- )
{
int a, b, c;
cin >> a >> b >> c;
d[a][b] = min (d[a][b], c); // 保留最小的边
}
floyd();
while (k -- )
{
int a, b;
cin >> a >> b;
if (d[a][b] > INF / 2) cout << "imposible" << endl;
else cout << d[a][b];
}
return 0;
}