题解:洛谷 P3371 单源最短路径(弱化版)
【题目来源】
【题目描述】
如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
【输入】
第一行包含三个整数 \(n,m,s\),分别表示点的个数、有向边的个数、出发点的编号。
接下来 \(m\) 行每行包含三个整数 \(u,v,w\),表示一条 \(u→v\) 的,长度为 \(w\) 的边。
【输出】
输出一行 \(n\) 个整数,第 \(i\) 个表示 \(s\) 到第 \(i\) 个点的最短路径,若不能到达则输出 \(2^{31}−1\)。
【输入样例】
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\) 到所有点的最短路径长度。若某点不可达,则输出 \(2^{31}-1\)。这是最经典的单源最短路问题,由于边权均为正(题目未明确说明负权,但模板题通常为非负),使用堆优化的 Dijkstra 算法可在 \(O(m \log n)\) 时间内解决。
-
算法选择:
- Dijkstra 算法(堆优化):适用于所有边权非负的单源最短路。使用优先队列(小根堆)维护当前未确定最短距离的节点,每次取出距离最小的节点进行松弛,保证每个节点只被处理一次。
- SPFA(备选):虽然也能处理正权图,但在无负权时 Dijkstra 效率更稳定,不易被卡。SPFA 通常用于含负权边的情况。
-
关键步骤(以堆优化 Dijkstra 为例):
- 初始化:
dist[1..n] = INF,dist[s] = 0,将(0, s)入小根堆。 - 主循环:当堆非空时:
- 取出堆顶
(d, u),若d > dist[u]则跳过(过时记录)。 - 标记
u已确定(vis[u] = true)。 - 遍历
u的所有出边(v, w):- 若
dist[v] > dist[u] + w,则更新dist[v],并将(dist[v], v)入堆。
- 若
- 取出堆顶
- 输出:按顺序输出
dist[1..n],若为INF则输出2^{31}-1。
- 初始化:
-
时间/空间复杂度:
- 时间复杂度:\(O(m \log n)\),堆优化 Dijkstra 的标准复杂度。
- 空间复杂度:\(O(n + m)\),邻接表存储图。
-
单源最短路的核心思想:
- Dijkstra 的贪心性质:每次从所有未确定最短路的点中选择距离最小的点,因为所有边权非负,该点的最短距离不会通过其他未确定的点变得更小。
- 松弛操作:通过当前点更新其邻接点的距离,是动态规划思想的体现。
- 堆优化的作用:朴素 Dijkstra 每次找最小距离需要 \(O(n)\),堆优化将此过程降为 \(O(\log n)\),适用于 \(n, m\) 较大的情况(如 \(10^5\) 级别)。
- 适用场景:所有边权非负的单源最短路问题,是图论中最基础且最重要的算法之一。
【算法标签】
普及 #Dijkstra
【代码详解】
// Dijkstra算法实现
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
#define inf 2147483647 // 定义无穷大
using namespace std;
int n, m, s; // n: 顶点数, m: 边数, s: 起点
int a, b, c; // 临时变量: 起点, 终点, 权重
const int N = 100010; // 最大顶点数
struct edge
{
int v; // 邻接顶点
int w; // 边权重
};
vector<edge> e[N]; // 邻接表存储图
int d[N]; // 最短距离数组
int vis[N]; // 标记顶点是否已确定最短路径
/**
* Dijkstra算法实现
* 求从起点s到所有其他顶点的最短路径
* @param s 起始顶点
*/
void dijkstra(int s)
{
// 初始化所有距离为无穷大
for (int i = 0; i <= n; i++)
{
d[i] = inf;
}
// 起点到自己的距离为0
d[s] = 0;
// 最多需要n-1次松弛
for (int i = 1; i < n; i++) // 枚举次数
{
int u = 0; // 当前未访问顶点中距离最小的顶点
// 在未标记的顶点中寻找距离最小的顶点
for (int j = 1; j <= n; j++) // 枚举所有顶点
{
if (!vis[j] && d[j] < d[u])
{
u = j;
}
}
vis[u] = 1; // 标记u已找到最短路径
// 更新u的所有邻接顶点的距离
for (auto ed : e[u]) // 枚举u的所有邻边
{
int v = ed.v; // 邻接顶点
int w = ed.w; // 边权重
// 松弛操作:如果通过u到v的路径更短
if (d[v] > d[u] + w)
{
d[v] = d[u] + w; // 更新最短距离
}
}
}
}
int main()
{
// 输入顶点数、边数、起点
cin >> n >> m >> s;
// 构建邻接表
for (int i = 0; i < m; i++)
{
cin >> a >> b >> c;
e[a].push_back({b, c}); // 添加有向边 a->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 = 10005; // 最大顶点数
const int M = 500005; // 最大边数
int n, m, s; // n: 顶点数, m: 边数, s: 起点
int h[N]; // 邻接表头指针
int e[M], w[M], ne[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到所有顶点的最短路径
* @param s 起始顶点
*/
void dijkstra(int s)
{
// 初始化所有距离为无穷大
// memset(dist, 0x3f, sizeof(dist));
for (int i = 1; i <= n; i++)
{
dist[i] = 2147483647; // INT_MAX
}
dist[s] = 0; // 起点到自身距离为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;
}
// 使用spfa再写一遍
#include <bits/stdc++.h>
using namespace std;
const int N = 10005; // 最大顶点数
const int M = 500005; // 最大边数
const int INF = (1 << 31) - 1; // 定义无穷大(INT_MAX)
int n, m, s; // n: 顶点数, m: 边数, s: 起点
int h[N]; // 邻接表头指针
int e[M], ne[M], w[M]; // 链式前向星: 终点, 下一条边, 权重
int idx; // 边的索引
int dist[N]; // 起点到各点的最短距离
int st[N]; // 标记顶点是否在队列中(SPFA使用)
/**
* 添加有向边
* @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++; // 更新头指针
}
/**
* SPFA算法 (Shortest Path Faster Algorithm)
* 求从起点s到所有顶点的最短路径
* 可以处理负权边,但不能处理负权环
*/
void spfa()
{
// 初始化所有距离为无穷大
// memset(dist, 0x3f, sizeof(st)); // 注释掉的错误代码
for (int i = 1; i <= n; i++)
{
dist[i] = INF; // 初始化为INT_MAX
}
dist[s] = 0; // 起点到自身距离为0
queue<int> q; // SPFA队列
q.push(s); // 起点入队
st[s] = true; // 标记起点在队列中
while (!q.empty())
{
int t = q.front(); // 取出队首顶点
q.pop();
st[t] = false; // 标记t不在队列中
// 遍历t的所有邻接边
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接顶点
// 松弛操作:如果通过t到j的路径更短
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i]; // 更新最短距离
// 如果j不在队列中,入队
if (!st[j])
{
q.push(j);
st[j] = true; // 标记j在队列中
}
}
}
}
}
int main()
{
// 初始化邻接表
memset(h, -1, sizeof(h));
// 输入顶点数、边数、起点
cin >> n >> m >> s;
// 读入边
for (int i = 1; i <= m; i++)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); // 添加有向边
}
// 执行SPFA算法
spfa();
// 输出结果
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号