1. 题目背景与核心需求
洛谷P3371是一道经典的图论练习题,要求实现单源最短路径算法。题目给定一个包含n个顶点、m条边的有向图,可能存在重边和自环,边权可为负数。需要从源点s出发,计算出到所有其他顶点的最短路径长度。
这个"弱化版"的特殊之处在于:
- 数据规模较小(n≤10^4, m≤5×10^5)
- 测试用例不包含负权回路
- 时间限制相对宽松(C++标准为1秒)
注意:虽然题目说明"可能存在负权边",但实际测试数据保证不含负权回路。这是选择SPFA算法的重要前提。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 算法选型分析
2.1 Dijkstra算法的局限性
传统Dijkstra算法采用贪心策略,每次扩展距离源点最近的未访问节点。其核心缺陷在于:
cpp复制// 伪代码示例
while (!pq.empty()) {
int u = pq.top().second;
pq.pop();
if (visited[u]) continue;
visited[u] = true;
for (auto &[v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({-dist[v], v}); // 小根堆实现
}
}
}
当图中存在负权边时,已标记为"visited"的节点可能通过负权边获得更短路径,导致算法失效。实测在洛谷提交纯Dijkstra会WA(Wrong Answer)。
2.2 SPFA的适用性证明
SPFA(Shortest Path Faster Algorithm)是Bellman-Ford的队列优化版本,特别适合本题场景:
- 时间复杂度平均O(m),最坏O(nm)(但本题数据保证无负环)
- 能正确处理负权边
- 代码实现比Dijkstra+堆优化更简单
关键优势在于通过动态松弛操作处理负权边:
cpp复制while (!q.empty()) {
int u = q.front();
q.pop();
inqueue[u] = false;
for (auto &[v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
if (!inqueue[v]) {
q.push(v);
inqueue[v] = true;
}
}
}
}
3. 完整AC代码实现
3.1 输入处理优化
洛谷的Java时限较紧,需要特别处理输入:
java复制import java.io.*;
import java.util.*;
public class Main {
static class Edge {
int to, weight;
Edge(int t, int w) { to = t; weight = w; }
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
String[] firstLine = br.readLine().split(" ");
int n = Integer.parseInt(firstLine[0]);
int m = Integer.parseInt(firstLine[1]);
int s = Integer.parseInt(firstLine[2]);
List<Edge>[] adj = new ArrayList[n + 1];
for (int i = 1; i <= n; i++) adj[i] = new ArrayList<>();
for (int i = 0; i < m; i++) {
String[] line = br.readLine().split(" ");
int u = Integer.parseInt(line[0]);
int v = Integer.parseInt(line[1]);
int w = Integer.parseInt(line[2]);
adj[u].add(new Edge(v, w));
}
// ... SPFA实现部分
}
}
3.2 SPFA核心实现
java复制int[] dist = new int[n + 1];
Arrays.fill(dist, Integer.MAX_VALUE);
dist[s] = 0;
Queue<Integer> q = new LinkedList<>();
boolean[] inqueue = new boolean[n + 1];
q.add(s);
inqueue[s] = true;
while (!q.isEmpty()) {
int u = q.poll();
inqueue[u] = false;
for (Edge e : adj[u]) {
if (dist[u] != Integer.MAX_VALUE && dist[e.to] > dist[u] + e.weight) {
dist[e.to] = dist[u] + e.weight;
if (!inqueue[e.to]) {
q.add(e.to);
inqueue[e.to] = true;
}
}
}
}
3.3 输出处理
注意题目要求无法到达时输出2^31-1:
java复制StringBuilder sb = new StringBuilder();
for (int i = 1; i <= n; i++) {
sb.append(dist[i] == Integer.MAX_VALUE ? 2147483647 : dist[i]).append(" ");
}
System.out.println(sb.toString().trim());
4. 性能优化技巧
4.1 队列选择策略
实测发现不同队列实现影响显著:
- LinkedList:最通用,但节点创建开销大
- ArrayDeque:性能最优(推荐)
- PriorityQueue:反而更慢,不符合SPFA特性
4.2 负权边检测(虽然本题不需要)
实际工程中应添加负环检测:
java复制int[] count = new int[n + 1];
// 在松弛操作后添加:
count[e.to] = count[u] + 1;
if (count[e.to] >= n) {
System.out.println("存在负权回路");
return;
}
4.3 内存分配优化
对于Java选手,提前分配足够内存:
java复制List<Edge>[] adj = new ArrayList[n + 1];
for (int i = 1; i <= n; i++) {
adj[i] = new ArrayList<>(2); // 预设初始容量
}
5. 不同语言的实现差异
5.1 C++版本优势
cpp复制vector<vector<pair<int, int>>> adj(n+1);
// 输入处理更简洁
queue<int> q;
q.push(s);
vector<int> dist(n+1, INT_MAX);
C++版本通常比Java快2-3倍,但代码逻辑相同。
5.2 Python的注意事项
Python由于性能限制,建议:
- 使用deque而非list实现队列
- 用sys.stdin读取输入
- 优先考虑PyPy3提交
6. 常见错误排查
-
WA问题:
- 检查dist数组初始值是否为最大值
- 确认源点dist[s]初始化为0
- 验证输出格式(空格分隔,行末无空格)
-
TLE问题:
- Java使用Scanner改为BufferedReader
- 检查是否误用Dijkstra
- 避免不必要的对象创建
-
RE问题:
- 数组大小是否为n+1
- 检查顶点编号是否从1开始
- 处理自环边的情况
实测坑点:某些测试用例会故意给出u==v的自环边,虽然题目说"可能有自环",但实际需要正确处理。
7. 算法扩展思考
虽然本题用SPFA能AC,但在工程实践中:
- 若无负权边,Dijkstra+优先队列更稳定(O(mlogn))
- 若需检测负环,SPFA或Bellman-Ford更合适
- 对于大规模图,考虑A*等启发式算法
一个有趣的测试用例:
code复制4 4 1
1 2 2
2 3 -1
3 4 -1
4 2 1
这个图虽然没有负权回路,但SPFA会执行较多松弛操作,正好测试队列实现的效率。
