\(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(n^2)\),本题运算上界是 \(6.26×10^6\) -
\(Dijkstra\) 堆优化版
时间复杂度 为 \(O(mlog_n)\),本题运算上界是 \(1.36×10^5\)
三、\(Dijkstra\)+堆优化 [\(PII\)+推荐]
#include <bits/stdc++.h>
using namespace std;
/*
(堆优化dijkstra) O((n+m)logm)
时间复杂度查了好久,说什么的也有,保险起见,这里就采用那个最高的吧!
*/
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 d[N]; //最短距离数组
//小顶堆
priority_queue<PII, vector<PII>, greater<PII>> q;
int n; // n个城镇
int m; // m条路径
int S; //起点
int T; //终点
//维护邻接表
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int dijkstra() {
memset(d, 0x3f, sizeof d); //初始化为无穷大
d[S] = 0; //出发点的距离初始化为0
q.push({0, S}); //源点入队列
// q里装的 first:距离出发点的距离 second:结点编号
while (q.size()) {
PII t = q.top();
q.pop();
//如果此结点已经被尝试过后,而且排在小顶堆的后面被尝试,说明不会更优秀
if (st[t.second]) continue;
//用这个点去尝试更新相关的点
st[t.second] = true;
for (int i = h[t.second]; ~i; i = ne[i]) {
int j = e[i];
if (d[j] > t.first + w[i]) {
d[j] = t.first + w[i];
q.push({d[j], j});
}
}
}
return d[T];
}
// 30 ms 还是推荐记忆这个,方便,代码短
int main() {
//加快读入
cin.tie(0), ios::sync_with_stdio(false);
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;
}
四、\(Dijkstra\)+堆优化[结构体]
#include <bits/stdc++.h>
using namespace std;
const int N = 2510;
const int M = 6200 * 2 + 10;
//邻接表
int h[N], w[M], e[M], ne[M], idx;
bool st[N]; //是否使用过
int d[N]; //最短距离数组
struct Node {
int id; //节点号
int dist; // id号节点,距离起点的最短距离
// 1、priority_queue默认是大顶堆,重载操作符<本质上是继承了less
// 2、重载结构体小于号,值大的小,值小的大。这样值小的就在堆顶
bool operator<(const Node &t) const {
//父节点 大于 孔洞节点,发生交换,则小元素上行,小顶堆
return dist > t.dist;
}
};
int n; // n个城镇
int m; // m条路径
int S; //起点
int T; //终点
//维护邻接表
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
// 30 ms
void dijkstra() {
priority_queue<Node> q; //优先级队列,大顶堆
memset(d, 0x3f, sizeof d); //初始化为无穷大
d[S] = 0; // S点距离自己的距离是0
q.push({S, 0}); //源点入队列
while (q.size()) {
auto t = q.top();
q.pop();
int u = t.id;
//如果此结点已经被尝试过后,而且排在小顶堆的后面被尝试,说明不会更优秀
if (st[u]) continue;
//用这个点去尝试更新相关的点
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (d[j] > t.dist + w[i]) {
d[j] = t.dist + w[i];
q.push({j, d[j]});
}
}
}
}
int main() {
//加快读入
cin.tie(0), ios::sync_with_stdio(false);
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);
}
dijkstra();
printf("%d\n", d[T]);
return 0;
}
五、\(Dijkstra\)+堆优化[结构体\(2\)]【废弃】
#include <bits/stdc++.h>
using namespace std;
const int N = 2510;
const int M = 6200 * 2 + 10;
//邻接表
int h[N], w[M], e[M], ne[M], idx;
bool st[N]; //是否使用过
int d[N]; //最短距离数组
// 优先队列的排序函数
// sort函数默认是从小到大排序,而优先队列默认是从大到小排序
struct cmp {
//自定义一个比较"类",重载括号,operator(),这种方式可以由 less 继承
//原理解析:https://www.cnblogs.com/littlehb/p/16806209.html
bool operator()(int &a, int &b) const {
//父节点 大于 孔洞节点,发生交换,则小元素上行,小顶堆
return d[a] > d[b]; //可以类比一下greater<>
//父节点 小于 孔洞节点,发生交换,则大元素上行,大顶堆
// return d[a]<d[b];
/*
疑惑关键就在于比较函数。
priority_queue 默认形成大根堆,而传入的comp默认为less。
为何传入一个可将序列顺序调为有小到大的函数,建成的堆反而是大顶堆呢?
不知你们有没有这种感觉?直觉上认为传入less,建成小顶堆,而传入greater,建成大顶堆。
前提: 判断条件==true 时,才会发生一次元素交换
场景1:父节点 小于(less) 孔洞节点 = true 发生交换,大元素向上走,大顶堆
场景2:父节点 大于(greater) 孔洞节点 = true 发生交换,大元素向下走,小顶堆
*/
}
};
int n; // n个城镇
int m; // m条路径
int S; //起点
int T; //终点
//维护邻接表
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void dijkstra() {
priority_queue<int, vector<int>, cmp> q; //优先级队列,引入cmp类进行排序,可以靠非优先队列中元素,而是外围数组等条件进行排序
memset(d, 0x3f, sizeof d); //初始化为无穷大
d[S] = 0; // S点距离自己的距离是0
q.push(S); //源点入队列
while (q.size()) {
auto u = q.top();
q.pop();
//如果此结点已经被尝试过后,而且排在小顶堆的后面被尝试,说明不会更优秀
if (st[u]) continue;
//用这个点去尝试更新相关的点
st[u] = true;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (d[j] > d[u] + w[i]) {
d[j] = d[u] + w[i];
q.push(j);
}
}
}
}
// 29 ms
int main() {
//加快读入
cin.tie(0), ios::sync_with_stdio(false);
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);
}
dijkstra();
printf("%d\n", d[T]);
return 0;
}
六、朴素版\(Dijkstra\) 【废弃】
#include <bits/stdc++.h>
using namespace std;
/*
(朴素dijkstra) O(n2)
*/
const int N = 2510;
const int M = 6200 * 2 + 10;
int n; // n个城镇
int m; // m条路径
int S; //起点
int T; //终点
//邻接表
int h[N], w[M], e[M], ne[M], idx;
bool st[N]; //是否使用过
int dist[N]; //最短距离数组
//维护邻接表
void add(int a, int b, int c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
int dijkstra() {
memset(dist, 0x3f, sizeof dist); //初始化为无穷大
dist[S] = 0; //出发点的距离初始化为0
for (int i = 0; i < n; i++) {
//找到距离出发点最近的点
int t = -1;
for (int j = 1; j <= n; j++)
if (!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
st[t] = true;
for (int j = h[t]; ~j; j = ne[j]) {
int k = e[j];
dist[k] = min(dist[k], dist[t] + w[j]);
}
}
return dist[T];
}
int main() {
cin >> n >> m >> S >> T;
memset(h, -1, sizeof h);
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c), add(b, a, c);
}
cout << dijkstra() << endl;
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 j = e[i];
if (d[j] > d[u] + w[i]) {
d[j] = d[u] + w[i];
if (!st[j]) {
st[j] = true;
q.push(j);
}
}
}
}
}
int main() {
//加快读入
cin.tie(0), ios::sync_with_stdio(false);
//输入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;
}