## [$AcWing$ $1129$. 热浪【单源最短路】](https://www.acwing.com/problem/content/description/1131/) ### 一、题目描述 德克萨斯纯朴的民众们这个夏天正在遭受巨大的热浪!!! 他们的德克萨斯长角牛吃起来不错,可是它们并不是很擅长生产富含奶油的乳制品。 农夫$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$+堆优化 ```cpp {.line-numbers} #include using namespace std; const int N = 2510; const int M = 6200 * 2 + 10; typedef pair 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, greater> 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)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$,只是某些图他跑的飞飞飞飞快** **实现代码** ```cpp {.line-numbers} #include 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 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; } ```