7.6 KiB
AcWing
1129
. 热浪【单源最短路】
一、题目描述
德克萨斯纯朴的民众们这个夏天正在遭受巨大的热浪!!!
他们的德克萨斯长角牛吃起来不错,可是它们并不是很擅长生产富含奶油的乳制品。
农夫John
此时身先士卒地承担起向德克萨斯运送大量的营养冰凉的牛奶的重任,以减轻德克萨斯人忍受酷暑的痛苦。
John
已经研究过可以把牛奶从威斯康星运送到德克萨斯州的路线。
这些路线包括 起始点 和 终点 一共有 T
个城镇,为了方便标号为 1
到 T
。
除了起点和终点外的每个城镇都由 双向道路 连向至少两个其它的城镇。
每条道路有一个通过费用(包括油费,过路费等等)。
给定一个地图,包含 C
条直接连接 2
个城镇的道路。
每条道路由道路的起点 R_s
,终点 R_e
和花费 C_i
组成。
求从起始的城镇 T_s
到终点的城镇 T_e
最小的总费用。
输入格式
第一行: 4
个由空格隔开的整数: T,C,T_s,T_e
;
第 2
到第 C+1
行: 第 i+1
行描述第 i
条道路,包含 3
个由空格隔开的整数: R_s,R_e,C_i
。
输出格式
一个单独的整数表示从 T_s
到 T_e
的最小总费用。
数据保证至少存在一条道路。
二、题目分析
单源点,正权图,求到终点的 最短路径
经典的 单源最短路 裸题,点数的范围是 n=2500
,边数的范围是 m=12400
Dijkstra
堆优化版 时间复杂度 为O(mlog_n)
,本题运算上界是1.36×10^5
三、Dijkstra
+堆优化
#include <bits/stdc++.h>
using namespace std;
const int N = 2510;
const int M = 6200 * 2 + 10;
typedef pair<int, int> PII;
int h[N], w[M], e[M], ne[M], idx;
bool st[N];
int dis[N];
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int n, m, S, T;
int dijkstra() {
memset(dis, 0x3f, sizeof dis);
dis[S] = 0;
priority_queue<PII, vector<PII>, greater<PII>> q;
q.push({0, S});
while (q.size()) {
PII t = q.top();
q.pop();
int u = t.second;
if (st[u]) continue;
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (dis[v] > dis[u] + w[i]) {
dis[v] = dis[u] + w[i];
q.push({dis[v], v});
}
}
}
return dis[T];
}
int main() {
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
while (m--) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
printf("%d\n", dijkstra());
return 0;
}
七、SPFA
【已死,有事烧纸~】
SPFA
算法是Bellman\_Ford
的一种 队列改进,减少了不必要的冗余计算;相比于Dijkstra
算法的 优点 是可以用来 在负权图上求最短路,且平均情况下复杂度也 较优;
算法思想
用一个队列来从源点开始维护,使得队列中的每个点都与它相连的点进行松弛操作;若松弛成功,则入队;否则开始下一个点的松弛;直到队列为空;
从上不难看出,其本质就是bfs
的过程,核心部分就是 松弛;
和bfs
不同的是,bfs
时每个点都最多只入队一次,而SPFA
算法中 每个点可以多次入队;也就是说,一个点在改进其它的点后,本身有可能再次被其它的点改进,然后加入队列再次准备改进其它的点,如此反复迭代......直到所有的点都无法再改进;此时,队列为空。
SPFA
算法是一种求解单源最短路径的算法,也就是说,它可以求解某个点 S
到图中其它所有的点的距离;所以我们用dist[i]
来表示 S
到 i
的最短距离。另外,它还可以检查出 负环,判断有无负环:若某个点的入队次数超过 V
(代表顶点数)次,则存在负环,所以我们用count[i]
来表示 i
点进入队列的次数;
SPFA
算法有两个优化算法 SLF
和 LLL
。
-
SLF
:Small
Label
First
策略,设要加入的节点是j
,队首元素为i
,若dist(j)<dist(i)
,则将j
插入队首,否则插入队尾。 -
LLL
:Large
Label
Last
策略,设队首元素为i
,队列中所有dist
值的平均值为x
,若dist(i)>x
则将i
插入到队尾,查找下一元素,直到找到某一i
使得dist(i)<=x
,则将i
出队进行松弛操作。
SLF
可使速度提高 15 \sim 20\%
;SLF + LLL
可提高约 50\%
。 在实际的应用中SPFA
的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra
算法。
如何看待 SPFA
算法已死这种说法?
在非负边权的图中,随手卡 SPFA
已是业界常识。在负边权的图中,不把 SPFA
卡到最慢就设定时限是非常不负责任的行为,而卡到最慢就意味着 SPFA
和传统 Bellman
Ford
算法的时间效率类似,而后者的实现难度远低于前者。SPFA
的受到怀疑和最终消亡,是 OI
界水平普遍提高、命题规范完善和出题人的使命感和责任心增强的最好见证。
观点
- 没有负权:
dijkstra
- 有负权 卡
spfa
:写spfa
最坏也是一个bellman-ford
- 随机图:
spfa
飞快
所以,可以写spfa
,不要故意写spfa
就可以了吧
坚定相信: 我写的这个是bellman-ford
,只是某些图他跑的飞飞飞飞快
实现代码
#include <bits/stdc++.h>
using namespace std;
/*
(spfa) O(m)
平均O(m),最坏O(nm) 一般会被卡到O(n*m)会被卡死
*/
const int N = 2510; // 端点数量
const int M = 6200 * 2 + 10; // 边的数量,记得别开太小,和N一样就等着挂吧
int n; // n个城镇
int m; // m条路径
int S; // 起点
int T; // 终点
int h[N], e[M], w[M], ne[M], idx; // 邻接表
int d[N]; // 最短距离数组
queue<int> q; // 队列
bool st[N]; // 是否使用过
// 邻接表模板
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// spfa模板
void spfa() {
// 将所有距离初始化为无穷大
memset(d, 0x3f, sizeof d);
// 出发点的距离清零
d[S] = 0;
q.push(S); // 出发点入队列
st[S] = true; // 出发点标识已使用
while (q.size()) {
int u = q.front();
q.pop();
st[u] = false;
for (int i = h[u]; ~i; i = ne[i]) {
int v = e[i];
if (d[v] > d[u] + w[i]) {
d[v] = d[u] + w[i];
if (!st[v]) {
st[v] = true;
q.push(v);
}
}
}
}
}
int main() {
// 输入n个城镇,m条路径,S:起点,T:终点
cin >> n >> m >> S >> T;
// 初始化邻接表
memset(h, -1, sizeof h);
// 读入m条路径
for (int i = 0; i < m; i++) {
// 每条路径的起点,终点,花费
int a, b, c;
cin >> a >> b >> c;
// 无向边
add(a, b, c), add(b, a, c);
}
spfa();
// 输出终点T的单源最短距离
printf("%d\n", d[T]);
return 0;
}