1. 项目概述
"天梯赛L2-001 紧急救援"是一道典型的图论算法题目,主要考察参赛者对Dijkstra算法的掌握程度及其在实际场景中的应用能力。题目设定在一个城市发生灾害的情境下,参赛者需要编写程序计算从起点到终点的最短路径,同时考虑路径上的救援队伍数量最大化这一附加条件。
这道题目源自国内知名程序设计竞赛平台,属于中等难度级别。它不仅测试基础算法实现能力,更考验选手对经典算法的灵活变通和优化技巧。在实际编程竞赛中,这类题目往往是区分选手水平的关键分水岭。
2. 核心需求解析
2.1 问题建模
题目可以抽象为带权有向图的最短路径问题,其中:
- 节点代表城市
- 边代表城市之间的道路
- 边权代表道路长度/通行时间
- 节点权代表该城市的救援队伍数量
需要同时满足两个优化目标:
- 从起点到终点的路径长度最短
- 在满足最短路径的前提下,选择救援队伍数量最多的路径
2.2 输入输出规范
典型输入格式示例:
code复制4 5 0 3 // 城市数 道路数 起点 终点
20 30 40 10 // 各城市救援队伍数量
0 1 1 // 道路信息:城市0到城市1,距离1
1 3 2
0 3 3
0 2 2
2 3 2
对应输出要求:
code复制2 60 // 最短路径数量 最多救援队伍数
0->2->3 // 具体路径
3. 算法设计与实现
3.1 基础Dijkstra算法改造
标准Dijkstra算法只能求解单源最短路径,需要针对题目需求进行三项关键改造:
- 路径数量统计:维护一个数组记录到每个节点的最短路径数量
- 救援队伍累计:当发现相同长度的路径时,选择救援队伍更多的路径
- 路径回溯:需要记录具体路径而非仅距离
核心数据结构:
cpp复制struct Node {
int id;
int dist;
bool operator<(const Node& rhs) const {
return dist > rhs.dist; // 小顶堆
}
};
vector<int> teams; // 各城市救援队伍数
vector<int> dist; // 到各城市的最短距离
vector<int> pathCount; // 到各城市的最短路径数
vector<int> totalTeams; // 到各城市可集结的最大队伍数
vector<int> pre; // 前驱节点用于路径回溯
3.2 算法实现步骤
- 初始化:
cpp复制void init(int n, int start) {
dist.assign(n, INT_MAX);
pathCount.assign(n, 0);
totalTeams.assign(n, 0);
pre.assign(n, -1);
dist[start] = 0;
pathCount[start] = 1;
totalTeams[start] = teams[start];
}
- 优先队列处理:
cpp复制priority_queue<Node> pq;
pq.push({start, 0});
while (!pq.empty()) {
Node curr = pq.top();
pq.pop();
if (curr.dist > dist[curr.id]) continue;
for (auto& edge : graph[curr.id]) {
int next = edge.to;
int newDist = curr.dist + edge.dist;
if (newDist < dist[next]) {
// 发现更短路径,更新所有信息
dist[next] = newDist;
pathCount[next] = pathCount[curr.id];
totalTeams[next] = totalTeams[curr.id] + teams[next];
pre[next] = curr.id;
pq.push({next, newDist});
}
else if (newDist == dist[next]) {
// 相同长度路径,合并信息
pathCount[next] += pathCount[curr.id];
if (totalTeams[curr.id] + teams[next] > totalTeams[next]) {
totalTeams[next] = totalTeams[curr.id] + teams[next];
pre[next] = curr.id;
}
}
}
}
3.3 路径回溯实现
通过前驱数组反向构建路径:
cpp复制vector<int> getPath(int dest) {
vector<int> path;
while (dest != -1) {
path.push_back(dest);
dest = pre[dest];
}
reverse(path.begin(), path.end());
return path;
}
4. 关键优化与注意事项
4.1 堆优化的重要性
使用优先队列(堆)实现的Dijkstra算法时间复杂度为O(E + VlogV),相比邻接矩阵实现的O(V²)版本更适合稀疏图。这是解决大规模数据的关键优化点。
注意:STL的priority_queue默认是大顶堆,需要通过重载运算符或使用greater实现小顶堆
4.2 相同距离处理逻辑
当发现相同距离的路径时,需要:
- 累加路径计数
- 比较救援队伍数量
- 必要时更新前驱节点
这个分支逻辑是题目最容易出错的地方,需要仔细处理所有可能性。
4.3 测试边界条件
特别注意以下边界情况:
- 起点和终点相同
- 存在重边(相同城市对的多条道路)
- 不连通图(无解情况)
- 最大数据量测试(如500个节点的完全图)
5. 完整代码实现
cpp复制#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
using namespace std;
struct Edge {
int to, dist;
Edge(int t, int d) : to(t), dist(d) {}
};
struct Node {
int id, dist;
bool operator<(const Node& rhs) const {
return dist > rhs.dist;
}
};
vector<vector<Edge>> graph;
vector<int> teams, dist, pathCount, totalTeams, pre;
void dijkstra(int start) {
priority_queue<Node> pq;
pq.push({start, 0});
dist[start] = 0;
pathCount[start] = 1;
totalTeams[start] = teams[start];
while (!pq.empty()) {
Node curr = pq.top();
pq.pop();
if (curr.dist > dist[curr.id]) continue;
for (auto& edge : graph[curr.id]) {
int next = edge.to;
int newDist = curr.dist + edge.dist;
if (newDist < dist[next]) {
dist[next] = newDist;
pathCount[next] = pathCount[curr.id];
totalTeams[next] = totalTeams[curr.id] + teams[next];
pre[next] = curr.id;
pq.push({next, newDist});
}
else if (newDist == dist[next]) {
pathCount[next] += pathCount[curr.id];
if (totalTeams[curr.id] + teams[next] > totalTeams[next]) {
totalTeams[next] = totalTeams[curr.id] + teams[next];
pre[next] = curr.id;
}
}
}
}
}
vector<int> getPath(int dest) {
vector<int> path;
while (dest != -1) {
path.push_back(dest);
dest = pre[dest];
}
reverse(path.begin(), path.end());
return path;
}
int main() {
int N, M, S, D;
cin >> N >> M >> S >> D;
teams.resize(N);
for (int i = 0; i < N; ++i) {
cin >> teams[i];
}
graph.resize(N);
for (int i = 0; i < M; ++i) {
int c1, c2, d;
cin >> c1 >> c2 >> d;
graph[c1].emplace_back(c2, d);
graph[c2].emplace_back(c1, d);
}
dist.assign(N, INT_MAX);
pathCount.assign(N, 0);
totalTeams.assign(N, 0);
pre.assign(N, -1);
dijkstra(S);
vector<int> path = getPath(D);
cout << pathCount[D] << " " << totalTeams[D] << endl;
for (int i = 0; i < path.size(); ++i) {
if (i != 0) cout << "->";
cout << path[i];
}
return 0;
}
6. 常见问题与解决方案
6.1 内存超限问题
当城市数量较多(如N=500)时:
- 避免使用邻接矩阵(空间O(N²))
- 使用邻接表存储图结构(空间O(N+E))
- 预分配vector大小减少动态扩容开销
6.2 时间效率优化
对于大规模数据:
- 确保使用堆优化的Dijkstra
- 使用scanf/printf代替cin/cout加速IO
- 减少不必要的容器拷贝操作
- 使用reserve预分配容器空间
6.3 特殊用例处理
常见陷阱用例:
- 起点终点相同:应输出1和该城市队伍数
- 存在更短路径但救援队伍较少:必须优先保证路径最短
- 多条等长路径:正确统计路径数量
7. 算法扩展思考
7.1 多目标优化变种
实际问题中可能需要考虑更多优化目标:
- 路径安全性评估
- 道路通行能力限制
- 多救援队伍协调
这类问题可考虑:
- 多维度Dijkstra(维护多维距离)
- 分层图技术
- 启发式搜索算法
7.2 动态场景处理
如果道路状况会随时间变化:
- 引入时间维度的状态表示
- 使用A*等启发式算法
- 考虑动态规划思想
7.3 并行计算优化
对于超大规模图:
- 使用GPU加速(如CUDA实现)
- 图分割并行计算
- 分布式Dijkstra算法
在实际竞赛中,这类题目往往需要选手在有限时间内快速完成算法设计、实现和调试。建议平时练习时注意:
- 熟练掌握Dijkstra的标准实现
- 理解各种优化变种的适用场景
- 建立完善的测试用例库
- 培养快速debug能力
我个人的经验是,这类题目在比赛中最容易因为边界条件考虑不周而出错。建议实现后立即测试以下用例:
- 单节点图
- 完全连通图
- 存在多条等长路径的图
- 最大规模极限数据
最后分享一个调试技巧:当遇到WA(Wrong Answer)时,可以输出中间变量(如每个节点的dist、pathCount等)与手工计算的结果对比,往往能快速定位逻辑错误。
