题解:洛谷 P4779 单源最短路径(标准版)
【题目来源】
【题目描述】
给定一个 \(n\) 个点,\(m\) 条有向边的带非负权图,请你计算从 \(s\) 出发,到每个点的距离。
数据保证你能从 \(s\) 出发到任意点。
【输入】
第一行为三个正整数 \(n,m,s\)。第二行起 \(m\) 行,每行三个非负整数 \(u_i,v_i,w_i\),表示从 \(u_i\) 到 \(v_i\) 有一条权值为 \(w_i\) 的有向边。
【输出】
输出一行 \(n\) 个空格分隔的非负整数,表示 \(s\) 到每个点的距离。
【输入样例】
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
【输出样例】
0 2 4 3
【核心思想】
-
问题分析:给定一个 \(n\) 个点、\(m\) 条有向边的非负权图,以及一个起点 \(s\),保证从 \(s\) 能到达所有点。要求输出 \(s\) 到每个点的最短距离。这是单源最短路的标准模板题,边权非负,\(n\) 和 \(m\) 均可达到 \(10^5\) 级别,需要使用 \(O(m \log n)\) 的算法。
-
算法选择:
- 堆优化的 Dijkstra:适用于所有边权非负的单源最短路问题。利用优先队列(小根堆)维护当前未确定最短距离的节点,每次取出距离最小的节点进行松弛,每个节点最多入堆一次(实际可能多次,但总复杂度 \(O(m \log n)\))。
- 链式前向星/邻接表:采用邻接表存储图,以支持高效遍历出边。
-
关键步骤:
- 初始化:距离数组
dist[1..n]初始化为无穷大,dist[s] = 0;标记数组vis[1..n]初始为false;优先队列pq压入(0, s)。 - 主循环:当
pq非空时:- 取出堆顶
(d, u),若d > dist[u]则跳过(表示该记录已过时)。 - 若
vis[u]为真,跳过;否则标记vis[u] = true。 - 遍历
u的每条出边(v, w):- 若
dist[v] > dist[u] + w,则更新dist[v],并将(dist[v], v)压入pq。
- 若
- 取出堆顶
- 输出:按顺序输出
dist[1..n]。
- 初始化:距离数组
-
时间/空间复杂度:
- 时间复杂度:\(O(m \log n)\),堆操作每次 \(O(\log n)\),每条边最多被松弛一次。
- 空间复杂度:\(O(n + m)\),存储邻接表和距离数组。
-
Dijkstra 算法的核心思想:
- 贪心策略:每次从未确定最短路的点中选取距离最小的点,因为边权非负,该点的最短距离已经确定。
- 松弛操作:通过当前点更新其所有邻接点的距离,是动态规划思想的体现。
- 堆优化:将朴素 Dijkstra 中找最小距离的 \(O(n)\) 步骤优化为 \(O(\log n)\),使得算法能处理大规模数据。
- 适用场景:所有边权非负的单源最短路问题,是图论中最基础且最重要的算法之一。
【算法标签】
普及 #SPFA
【代码详解】
//堆优化Dijkstra
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010; // 最大顶点数
int n, m, s; // n: 顶点数, m: 边数, s: 起点
int a, b, c; // 临时变量: 起点, 终点, 权重
struct edge
{
int v; // 邻接顶点
int w; // 边权重
};
vector<edge> e[N]; // 邻接表存储图
int d[N]; // 最短距离数组
int vis[N]; // 标记顶点是否已出队
/**
* Dijkstra算法实现(堆优化)
* 求从起点s到所有其他顶点的最短路径
* 注意:这里使用大根堆,通过存入负距离实现小根堆功能
* @param s 起始顶点
*/
void dijkstra(int s)
{
// 初始化距离数组
memset(d, 0x3f, sizeof d); // 初始化为无穷大
d[s] = 0; // 起点到自身距离为0
// 使用大根堆存储(-距离, 顶点)对
// 通过存储负距离,模拟小根堆的效果
priority_queue<pair<int, int>> q; // 默认是大根堆
// 起点入队
q.push({0, s}); // 距离为0,顶点为s
// 主循环
while (q.size())
{
// 取出堆顶元素
auto t = q.top();
q.pop();
int u = t.second; // 获取顶点编号
// 如果顶点已出队,跳过
if (vis[u])
{
continue; // 再出队跳过
}
vis[u] = 1; // 标记u已出队
// 遍历u的所有邻接边
for (auto ed : e[u])
{
int v = ed.v; // 邻接顶点
int w = ed.w; // 边权重
// 松弛操作
if (d[v] > d[u] + w)
{
d[v] = d[u] + w; // 更新最短距离
// 入队,存储负距离以实现小根堆
q.push({-d[v], v}); // 大根堆,负数使距离小的在堆顶
}
}
}
}
int main()
{
// 输入顶点数、边数、起点
cin >> n >> m >> s;
// 构建邻接表
for (int i = 0; i < m; i++)
{
cin >> a >> b >> c;
e[a].push_back({b, c}); // 添加有向边
}
// 执行Dijkstra算法
dijkstra(s);
// 输出结果
for (int i = 1; i <= n; i++)
{
printf("%d ", d[i]);
}
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 100005; // 最大顶点数
const int M = 200005; // 最大边数
int n, m, s; // n: 顶点数, m: 边数, s: 起点
int h[N]; // 邻接表头指针
int e[M], ne[M], w[M]; // 链式前向星: 终点, 下一条边, 权重
int idx; // 边的索引
int dist[N]; // 起点到各点的最短距离
bool st[N]; // 标记顶点是否已确定最短路径
typedef pair<int, int> PII; // 存储(距离, 顶点)
priority_queue<PII, vector<PII>, greater<PII> > heap; // 小根堆
/**
* 添加有向边
* @param a 起点
* @param b 终点
* @param c 权重
*/
void add(int a, int b, int c)
{
e[idx] = b; // 边指向的顶点
w[idx] = c; // 边的权重
ne[idx] = h[a]; // 指向原链表头
h[a] = idx++; // 更新头指针
}
/**
* 堆优化的Dijkstra算法
* 计算从起点s到所有顶点的最短路径
* 注意:第22行代码有错误,应为dist[s] = 0
* @param s 起始顶点
*/
void dijkstra(int s)
{
// 初始化所有距离为无穷大
memset(dist, 0x3f, sizeof(dist));
// 错误:这里应该设置dist[s] = 0,而不是dist[1] = 0
dist[1] = 0; // 错误!应该是dist[s] = 0
// 起点入堆
heap.push({0, s});
while (!heap.empty())
{
// 取出堆顶(距离最小的顶点)
auto t = heap.top();
heap.pop();
int veid = t.second; // 顶点编号
int distance = t.first; // 当前距离
// 如果顶点已确定最短路径,跳过
if (st[veid] == true)
{
continue;
}
st[veid] = true; // 标记为已确定
// 遍历veid的所有邻接边
for (int i = h[veid]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接顶点
// 松弛操作:如果通过veid到j的路径更短
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i]; // 更新最短距离
heap.push({dist[j], j}); // 新距离入堆
}
}
}
}
int main()
{
// 输入顶点数、边数、起点
cin >> n >> m >> s;
// 初始化邻接表
memset(h, -1, sizeof(h));
// 读入边
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); // 添加有向边
}
// 执行Dijkstra算法
dijkstra(s);
// 输出结果
for (int i = 1; i <= n; i++)
{
cout << dist[i] << " ";
}
cout << endl;
return 0;
}
【运行结果】
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
0 2 4 3
浙公网安备 33010602011771号