1. 杭电OJ 2066题解析:单源最短路径算法实战
杭电OJ(Hangzhou Dianzi University Online Judge)作为国内知名的编程题库平台,2066题是一道经典的图论题目,主要考察单源最短路径算法的应用。这道题在ACM训练和算法竞赛准备中具有典型意义,能够帮助编程者深入理解Dijkstra算法的实现细节和优化技巧。
1.1 题目核心需求分析
题目描述一个旅行者希望从城市S出发,到达若干个目标城市中的一个。已知各城市之间的道路连接情况和通行时间,要求找出从S出发到达任意目标城市的最短时间。
关键数据特征:
- 城市数量不超过1000个(中等规模图)
- 可能存在重边(两个城市间多条不同权值的边)
- 需要处理多个目标终点的情况
- 时间限制严格(C/C++通常要求500ms内)
1.2 算法选择依据
对于单源最短路径问题,常见有三种算法备选:
- Dijkstra算法 - 时间复杂度O((V+E)logV),适合无负权边
- Bellman-Ford算法 - 时间复杂度O(VE),可处理负权边
- SPFA算法 - 最坏O(VE),实际效率不稳定
基于题目特性:
- 城市间通行时间均为正数(无负权边)
- 城市规模中等(1000节点级别)
- 需要高效求解
Dijkstra算法+堆优化是最佳选择,其时间复杂度可以很好地处理题目规模,且实现相对稳定。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Dijkstra算法实现详解
2.1 基础数据结构准备
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <climits>
using namespace std;
const int MAXN = 1005;
vector<pair<int, int>> adj[MAXN]; // 邻接表存储图
int dist[MAXN]; // 存储最短距离
bool visited[MAXN]; // 标记是否访问
邻接表是存储稀疏图最高效的方式,pair<int,int>中first存储相邻节点,second存储边权值。dist数组初始化为INF,visited数组初始化为false。
2.2 优先队列优化实现
传统Dijkstra使用普通队列会导致O(V²)时间复杂度,采用优先队列(堆)优化可降为O((V+E)logV):
cpp复制void dijkstra(int start) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
fill(dist, dist+MAXN, INT_MAX);
fill(visited, visited+MAXN, false);
dist[start] = 0;
pq.push({0, start});
while(!pq.empty()) {
int u = pq.top().second;
pq.pop();
if(visited[u]) continue;
visited[u] = true;
for(auto &edge : adj[u]) {
int v = edge.first;
int weight = edge.second;
if(dist[v] > dist[u] + weight) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
}
关键点说明:
- 使用小顶堆(greater)确保每次取出当前距离最小的节点
- visited数组避免重复处理
- 松弛操作(relaxation)时更新距离并加入队列
2.3 多终点处理技巧
题目要求计算到多个目标城市的最短时间,只需:
- 运行一次Dijkstra算法
- 遍历所有目标城市,取dist中的最小值
cpp复制int min_time = INT_MAX;
for(int target : destinations) {
if(dist[target] < min_time) {
min_time = dist[target];
}
}
cout << min_time << endl;
这种处理方式避免了多次运行算法,显著提高效率。
3. 性能优化与边界处理
3.1 输入输出加速
杭电OJ对IO时间有严格要求:
cpp复制ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
关闭同步流可以大幅提升cin/cout速度,对于大规模数据输入至关重要。
3.2 内存与初始化优化
- 使用全局数组而非vector可以减少动态分配开销
- 每次测试用例重置时,只需重置相关部分内存:
cpp复制for(int i=0; i<=max_city; i++) {
adj[i].clear();
}
3.3 重边处理
题目可能存在重边,需要在建图时保留最小权值的边:
cpp复制while(m--) {
int a, b, t;
cin >> a >> b >> t;
// 检查是否已有更短的边
bool exists = false;
for(auto &edge : adj[a]) {
if(edge.first == b && edge.second <= t) {
exists = true;
break;
}
}
if(!exists) {
adj[a].push_back({b, t});
adj[b].push_back({a, t});
}
}
4. 完整AC代码实现
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
using namespace std;
const int MAXN = 1005;
vector<pair<int, int>> adj[MAXN];
int dist[MAXN];
bool visited[MAXN];
void dijkstra(int start) {
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
fill(dist, dist+MAXN, INT_MAX);
fill(visited, visited+MAXN, false);
dist[start] = 0;
pq.push({0, start});
while(!pq.empty()) {
int u = pq.top().second;
pq.pop();
if(visited[u]) continue;
visited[u] = true;
for(auto &edge : adj[u]) {
int v = edge.first;
int weight = edge.second;
if(dist[v] > dist[u] + weight) {
dist[v] = dist[u] + weight;
pq.push({dist[v], v});
}
}
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T, S, D;
while(cin >> T >> S >> D) {
// 清空图
for(int i=0; i<MAXN; i++) adj[i].clear();
// 建图
int max_city = 0;
while(T--) {
int a, b, t;
cin >> a >> b >> t;
adj[a].push_back({b, t});
adj[b].push_back({a, t});
max_city = max(max_city, max(a, b));
}
// 邻近城市
vector<int> starts(S);
for(int i=0; i<S; i++) cin >> starts[i];
// 目标城市
vector<int> ends(D);
for(int i=0; i<D; i++) cin >> ends[i];
// 计算最短时间
int min_time = INT_MAX;
for(int start : starts) {
dijkstra(start);
for(int end : ends) {
if(dist[end] < min_time) {
min_time = dist[end];
}
}
}
cout << min_time << endl;
}
return 0;
}
5. 常见问题与调试技巧
5.1 超时问题排查
- 检查是否使用了IO加速
- 确认使用堆优化而非朴素Dijkstra
- 避免不必要的内存重置
- 检查重边处理是否高效
5.2 错误答案分析
- 初始化问题:确保dist数组每次运行前正确重置
- 无穷大值处理:INT_MAX做加法可能溢出,题目中保证结果在int范围内
- 多起点处理:需要对每个起点单独运行Dijkstra
5.3 测试用例设计
建议自测用例:
code复制// 简单直线型
1 1 1
1 2 10
1
2
// 带重边
2 1 1
1 2 10
1 2 5
1
2
// 多起点多终点
3 2 2
1 2 1
2 3 1
3 4 1
1 3
2 4
6. 算法扩展与变种思考
6.1 双向Dijkstra优化
对于固定起点和终点的场景,可以同时从起点和终点开始搜索,当两边的搜索相遇时终止。这种优化可以显著减少搜索空间。
6.2 A*算法应用
如果有启发式函数(如地理坐标),可以使用A*算法进一步加速。但在OJ题目中通常无法获取额外信息。
6.3 多层级图处理
如果题目扩展为可以乘坐不同交通工具(每种工具速度不同),可以构建多层图,每层代表一种交通工具状态,层间通过换乘边连接。
