1. 题目解析与算法选择
这道PTA L3-040题目描述了一个典型的图论问题:在一个带权无向图中,我们需要找到从起点出发,在给定预算b内能够到达的所有城市,并在这些城市中找出能获得最大旅行体验值的那些城市。题目要求我们处理多个查询,每个查询给出不同的起点城市。
1.1 问题建模
首先我们需要将这个问题转化为图论模型:
- 城市作为图中的顶点
- 城市间的交通路线作为图中的边
- 每条边有两个权重属性:耗时/费用w和体验值x
问题的核心在于:在不超过预算b的前提下,找到所有可达城市,并在这些城市中找出能提供最大体验值的那些。
1.2 算法选择分析
对于这类多源最短路径问题,我们有几种常见算法选择:
- Dijkstra算法:适合单源最短路径,但需要为每个查询单独运行,时间复杂度O(k*n^2),当k较大时效率不高
- Bellman-Ford算法:能处理负权边,但时间复杂度O(n^3)
- Floyd-Warshall算法:直接计算所有顶点对的最短路径,时间复杂度固定为O(n^3)
考虑到题目中n的范围通常在几百以内,且查询次数k可能较多,Floyd-Warshall算法是最佳选择。它虽然时间复杂度较高,但只需要运行一次就能得到所有顶点对的最短路径,后续查询可以在O(n)时间内完成。
提示:Floyd算法特别适合顶点数不多但需要频繁查询不同起点的情况,这正是本题的特点。
2. Floyd-Warshall算法详解
2.1 基础Floyd算法
标准Floyd算法用于计算图中所有顶点对的最短路径。其核心思想是动态规划,通过逐步考虑更多的中间顶点来更新最短路径。
算法伪代码:
code复制for k from 1 to n:
for i from 1 to n:
for j from 1 to n:
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])
2.2 本题的扩展
本题需要在最短路径的基础上考虑第二关键字——体验值x。我们需要对标准Floyd算法进行扩展:
- 当找到更短的路径时,完全更新距离和体验值
- 当路径长度相同时,选择体验值更大的路径
这需要在状态转移时增加额外的判断条件:
code复制if 新路径更短:
完全更新w和x
else if 路径长度相同但体验值更大:
只更新x
2.3 剪枝优化
本题一个巧妙之处在于加入了预算限制b。我们可以在Floyd算法内部进行剪枝:
code复制tmp = dst[i][k].w + dst[k][j].w
if tmp > b: # 超过预算,跳过
continue
这个剪枝可以避免计算那些明显不符合条件的路径,虽然不影响最坏时间复杂度,但在实际运行中可以显著减少计算量。
3. 代码实现解析
3.1 数据结构设计
代码中使用结构体数组存储图的邻接矩阵:
cpp复制struct node {
int w = N, x = 0; // N表示无穷大
} dst[510][510];
这种设计可以同时存储两个属性:
- w:城市间的最短耗时/费用
- x:对应路径的最大体验值
3.2 初始化阶段
初始化包括两个部分:
- 对角线元素初始化为0(城市到自身的距离为0,体验值也为0)
- 读取输入边时,处理重边情况:
- 对于w取最小值
- 对于x取最大值
cpp复制// 初始化对角线
for (int i = 1; i <= n; i++) {
dst[i][i].w = 0;
dst[i][i].x = 0;
}
// 读取边
for (int i = 1; i <= m; i++) {
int u, v, w, x;
cin >> u >> v >> w >> x;
// 处理重边
dst[u][v].w = min(dst[u][v].w, w);
dst[u][v].x = max(dst[u][v].x, x);
dst[v][u] = dst[u][v]; // 无向图
}
3.3 Floyd算法实现
扩展的Floyd算法实现如下:
cpp复制void fld() {
for (int kk = 1; kk <= n; kk++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
int tmp = dst[i][kk].w + dst[kk][j].w;
if (tmp > b) continue; // 剪枝
if (dst[i][j].w > tmp) {
// 发现更短路径,完全更新
dst[i][j].w = tmp;
dst[i][j].x = dst[i][kk].x + dst[kk][j].x;
}
else if (dst[i][j].w == tmp) {
// 路径长度相同,取体验值更大者
if (dst[i][j].x < dst[i][kk].x + dst[kk][j].x) {
dst[i][j].x = dst[i][kk].x + dst[kk][j].x;
}
}
}
}
}
}
3.4 查询处理
对于每个查询,需要:
- 找出所有在预算b内可达的城市
- 在这些城市中找出体验值最大的那些
实现要点:
- 遍历所有城市,跳过不可达或超过预算的
- 维护当前最大体验值和对应城市列表
- 注意输出格式要求
cpp复制while (k--) {
int ask;
cin >> ask;
vector<int> ans;
bool fl = false;
int mx = -1;
// 第一轮输出所有可达城市
for (int i = 1; i <= n; i++) {
if (i == ask) continue;
if (dst[i][ask].w == N || dst[i][ask].w > b) continue;
if (!fl) fl = true;
else cout << " ";
cout << i;
// 维护最大体验值城市列表
if (dst[i][ask].x > mx) {
ans.clear();
mx = dst[i][ask].x;
ans.push_back(i);
}
else if (dst[i][ask].x == mx) {
ans.push_back(i);
}
}
if (!fl) {
cout << "T_T\n";
continue;
}
cout << "\n";
// 输出最大体验值城市
for (int i = 0; i < ans.size(); i++) {
if (i) cout << " ";
cout << ans[i];
}
cout << "\n";
}
4. 算法优化与注意事项
4.1 性能优化技巧
- 对称性利用:由于是无向图,可以只计算上三角或下三角矩阵,但实现复杂度会增加
- 提前终止:如果某次迭代没有更新任何距离,可以提前终止算法
- 内存局部性:调整循环顺序可能提高缓存命中率
4.2 常见错误与调试
- 无穷大值设置:确保足够大但不会溢出,本题使用1e9是合理的选择
- 初始化遗漏:忘记初始化对角线元素会导致错误
- 无向图处理:添加边时需要同时更新两个方向
- 重边处理:需要正确处理输入中的重边情况
注意:在竞赛编程中,使用
#define int long long可以避免很多整数溢出问题,但会稍微增加内存使用。
4.3 边界条件测试
编写测试用例时应考虑:
- 单个城市的情况
- 完全不连通图
- 所有边权重相同的情况
- 预算b为0或极大的情况
- 重边和自环边
5. 算法扩展与应用
5.1 类似问题变种
- 多关键字最短路径:可以扩展到更多关键字,如时间、费用、舒适度等
- 受限最短路径:除了预算限制,还可以加入其他限制条件
- 动态图问题:边权重可能随时间变化的情况
5.2 实际应用场景
- 旅游路线规划
- 物流配送路径优化
- 网络路由选择
- 交通导航系统
5.3 进一步学习建议
- 学习Dijkstra算法的堆优化实现
- 了解A*等启发式搜索算法
- 研究分层图、拆点等高级图论技巧
- 练习更多PTA/LeetCode上的图论题目
在实际编程竞赛中,图论问题非常常见。掌握Floyd算法及其变种是解决这类问题的基础。本题的特别之处在于需要在经典算法基础上进行适当扩展,处理多关键字和限制条件,这也是竞赛题目的常见套路。