单源最短路
字数
1694 字
阅读时间
8 分钟
分类
边权非负
1. 朴素Dijstra算法
- 适用于稠密图,时间复杂度为
2. 堆优化版Dijstra算法
- 适用于稀疏图,时间复杂度为
,m 为询问次数,n 为建图的点个数
有负边权
大多数情况均使用SPFA算法,仅少数情况使用Bellman-ford算法,所以只需要了解SPFA算法(平均时间复杂度为
难点/重点
最难的是问题的转化与抽象,如何将问题转化为对应的模板问题,用于求解两个点之间的一条最短路径
建图
稠密图:边数 >= 顶点数的平方
稀疏图:边数 < 顶点数的平方
邻接表建图(稀疏图)
C++
void add(int a, int b, int c) // 建立一条由 a 指向 b 的边
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
} // e:当前边的终点(即 b),ne:下一条边的终点
假设我们有 1, 2, ..., n 个点,则用邻接表建图,一共有 n 个邻接表
若需要建立一条由 1 指向 2 的边,边权为 c,则 add(1, 2, c),使得在邻接表a 的表头 h[1] 后建立了一条指向 2 的边,即:
h[1]: 2 → -1
若再添加一条边,使得 1 指向 3,边权为 c,则 add(1, 3, c),得到邻接表:
h[1]: 3 → 2 → -1
邻接矩阵建图(稠密图)
C++
直接存储
int dist[N][N];
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
cin >> dist[i][j];
例题
热浪(SPFA)
C++
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2510, M = 6200 * 2 + 10;
int n, m, S, T; // 点数,边数,起点,终点
int h[N], e[M], w[M], ne[M], idx; // 邻接表存储
int dist[N], q[N];
bool st[N];
void add(int a, int b, int c) // 建立一条由 a 指向 b 的边
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[S] = 0; // 起点距离设置为 0
//因为是循环队列,所以要保证初始条件 hh != tt 能成立,我们正常使用模拟队列可能习惯了使 hh = 0, tt = 0,并用 while(hh <= tt)来作为循环条件,所以可能对此处的 tt = 1心存疑惑。其实很简单,此处需要保证循环队列成立,故 tt 指向的是下一空位,而非指向队尾元素;若 hh == tt,则说明循环队列中不存在
// tt 始终指向下一可插入的位置,hh 指向当前待处理元素的位置
int hh = 0, tt = 1; // 注意循环队列初始条件
q[0] = S, st[S] = true;
// 使用循环队列是因为每次更新最短距离路径都可能加入新的节点
while(hh != tt) // 队列不空的判断是 hh != tt, 也可以直接用queue<int> q[N], 这样就不用写 if(hh == N) h = 0 来模拟循环队列了
{
int t = q[hh ++];
if (hh == N) hh = 0;
st[t] = false; // 将 t 出队,清除 st
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];
if (!st[j]) // 判断该点是否已经在队列中,避免重复处理同一个点
{
q[tt ++] = j;
if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
}
int main()
{
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h); // 初始化表头不能忘
for (int i = 0; i < m; i ++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
spfa();
cout << dist[T] << endl;
return 0;
}
最小花费(Dijkstra)
C++
// 1126
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 2010;
int n, m, S, T;
double g[N][N];
double dist[N];
bool st[N];
void dijkstra()
{
// 这里是求百分比乘积最大,所以设置为1,若是求最短路,则将dist[S] = 0;
dist[S] = 1; // 1 乘任何数都是它本身
for (int i = 1; i <= n; i ++)
{
int t = -1;
for (int j = 1; j <= n; j ++)
{
if (!st[j] && (t == -1 || dist[t] < dist[j]))
t = j;
}
st[t] = true;
for (int j = 1; j <= n; j ++)
dist[j] = max(dist[j], dist[t] * g[t][j]);
// 若是求最短路,则使得dist[j] = min(dist[j],dist[t] + g[t][j]);
// 枚举每一个点,判断从 1 号点出发,经过中间点 t 的路线与直接到 j 号点的距离哪个更短
}
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i <= m; i ++)
{
int a, b, c;
cin >> a >> b >> c;
double z = (100.0 - c) / 100;
g[a][b] = g[b][a] = max(g[a][b], z);
}
cin >> S >> T;
dijkstra();
cout << (100 / dist[T]) << endl;
return 0;
}
堆优化版Dijkstra
C++
// Dijkstra算法求最短路
// 题目:给定一个 n 个点,m 条边的有向图,图中可能存在重边和自环,所有边权均为正数,求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 -1
// 范围:1 <= n <= 1e6; 1 <= m <= 1e6 ---------- 稀疏图 ----------- 邻接表
// 输入:第一行包括两个整数 n 和 m,接下来 m 行包含三个整数 x,y,z,表示点 x 和点 y 之间有一条有向边,其边权(边长)为 z
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
typedef pair<int,int> PII;
const int N = 100010;
int n,m;
int idx,e[N],ne[N],h[N],w[N]; // w 存权值
int dist[N]; // dist[i]: 1 号点到 i 号点的最短距离
bool st[N]; // st[i]:记录点 i 是否已经为最短距离
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx, idx ++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0; // 1 号点是起点
priority_queue<PII,vector<PII>, greater<PII> > heap;
heap.push({0,1}); // first 存权值(从 1 号点到该点的最短距离),second 存第 i 号点 ---------- 因为小根堆的排序是先将 first 从小到大排完之后再排second
while(heap.size())
{
auto t = heap.top(); // 每次找到一个点最短距离
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) 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()
{
cin >> n >> m;
memset(h, -1, sizeof h);
while(m --)
{
int a,b,c;
cin >> a >> b >> c;
add(a,b,c);
}
int t = dijkstra();
cout << t << endl;
return 0;
}
分类情况做法
- 边权均
,则可以用Dijstra算法 - 所有边的权值
,可以大于1,也可以小于1,相当于有负权边的最短路问题(因为 ,当 时, ,可视为边权 < 0),则只能用SPFA算法