2 条题解
-
0
为了方便你创建 OJ(Online Judge)测试数据,我编写了这个 C++ 数据生成器。它包含了标准 Bellman-Ford 算法作为标程(用于生成
.out文件)以及针对性构造逻辑(用于生成.in文件)。数据生成器设计逻辑
- Case 1-3:题目官方样例,确保基础逻辑正确。
- Case 4:最小边界测试()。
- Case 5 (长链图):构造一条长度为 50 的链,但 设置为 10,测试程序是否能正确输出 -1(不可达)。
- Case 6 (稠密图): 的完全图,测试算法在边数较多时的稳定性。
- Case 7 (价格路径权衡):构造一条直达航线(贵)和一条中转多次航线(便宜),通过调整 测试决策逻辑。
- Case 8-10:大规模随机图,包含各种 值,确保没有漏掉的连通分支。
数据生成器 C++ 代码
#include <iostream> #include <fstream> #include <vector> #include <queue> #include <string> #include <random> #include <ctime> #include <algorithm> #include <set> using namespace std; // 使用 0x3f3f3f3f 作为无穷大,防止加法溢出 (约 10^9) const int INF = 0x3f3f3f3f; struct Edge { int u, v, w; }; // --- 标准 Bellman-Ford 算法用于生成 .out 文件 --- // 使用 const 引用解决编译错误 int solve_std(int n, int k, const vector<Edge>& edges, int src, int dst) { vector<int> dist(n, INF); dist[src] = 0; // 最多中转 k 次,即最多走 k+1 条边,进行 k+1 轮松弛 for (int i = 0; i <= k; i++) { vector<int> last = dist; // 必须使用上一轮的副本,防止超前松弛 for (const auto& e : edges) { if (last[e.u] != INF) { if (last[e.u] + e.w < dist[e.v]) { dist[e.v] = last[e.u] + e.w; } } } } return (dist[dst] >= INF ? -1 : dist[dst]); } // --- 数据写入函数 --- // 将 vector<Edge>& 改为 const vector<Edge>& void write_case(int id, int n, int k, const vector<Edge>& edges, int src, int dst) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; ofstream fout(in_name); fout << n << " " << edges.size() << " " << k << "\n"; for (const auto& e : edges) { fout << e.u << " " << e.v << " " << e.w << "\n"; } fout << src << " " << dst << endl; fout.close(); ofstream fans(out_name); fans << solve_std(n, k, edges, src, dst) << endl; fans.close(); cout << "Generated Case " << id << " (N=" << n << ", M=" << edges.size() << ")" << endl; } int main() { mt19937 rng(time(0)); auto get_rand = [&](int l, int r) { return uniform_int_distribution<int>(l, r)(rng); }; // Case 1: 样例 1 write_case(1, 4, 1, {{0,1,100}, {1,2,100}, {2,0,100}, {1,3,600}, {2,3,200}}, 0, 3); // Case 2: 样例 2 write_case(2, 3, 1, {{0,1,100}, {1,2,100}, {0,2,500}}, 0, 2); // Case 3: 样例 3 write_case(3, 3, 0, {{0,1,100}, {1,2,100}, {0,2,500}}, 0, 2); // Case 4: 最小边界 N=2 write_case(4, 2, 0, {{0, 1, 1000}}, 0, 1); // Case 5: 长链图不可达 (K太小) { vector<Edge> e; for (int i = 0; i < 49; i++) e.push_back({i, i + 1, 10}); write_case(5, 50, 5, e, 0, 49); } // Case 6: 稠密图随机 (N=100) { int n = 100; vector<Edge> e; set<pair<int, int>> seen; for (int i = 0; i < 2000; i++) { int u = get_rand(0, n - 1), v = get_rand(0, n - 1); if (u != v && !seen.count({u, v})) { e.push_back({u, v, get_rand(1, 1000)}); seen.insert({u, v}); } } write_case(6, n, 50, e, 0, 99); } // Case 7: 价格与步数的博弈 (构造性数据) { vector<Edge> e; e.push_back({0, 99, 10000}); // 直达:贵 e.push_back({0, 1, 1000}); // 1站转:中 e.push_back({1, 99, 1000}); for (int i = 1; i < 98; i++) e.push_back({i, i + 1, 1}); // 多站转:便宜 e.push_back({98, 99, 1}); write_case(7, 100, 1, e, 0, 99); } // Case 8 & 9: 随机生成大图 for (int id = 8; id <= 9; id++) { int n = 100; vector<Edge> e; set<pair<int, int>> seen; int m_target = get_rand(500, 1500); for (int i = 0; i < m_target; i++) { int u = get_rand(0, n - 1), v = get_rand(0, n - 1); if (u != v && !seen.count({u, v})) { e.push_back({u, v, get_rand(1, 5000)}); seen.insert({u, v}); } } write_case(id, n, get_rand(0, 30), e, get_rand(0, 40), get_rand(60, 99)); } // Case 10: 极限数据 (无解测试) { int n = 100; vector<Edge> e; // 构造两个互不连通的子图 for (int i = 0; i < 40; i++) e.push_back({i, (i + 1) % 40, 5}); for (int i = 50; i < 90; i++) e.push_back({i, (i + 1) % 40 + 50, 5}); write_case(10, n, 99, e, 0, 99); } return 0; }数据生成器设计要点说明:
- 文件大小优化:
- 且 。即使在最大规模下,每个
.in文件的大小约为 50KB。10 个测试点总大小约 0.5MB,远低于 2MB 限制,加载极快。
- 且 。即使在最大规模下,每个
- 避免超前松弛:
- 在
solve_std中使用了vector<int> last = dist;。这是 Bellman-Ford 处理限步最短路的关键。如果直接更新dist,会在一轮迭代内走多步,导致.out答案错误。
- 在
- 无重边与自环逻辑:
- 通过
std::set<pair<int, int>> seen保证随机生成的边不重复,且u != v排除自环。
- 通过
- 异常处理:
- 使用了
mt19937随机引擎代替rand(),保证了跨平台的数据一致性和随机质量。 - 航班价格从 开始,避免了 权边可能导致的某些算法逻辑歧义。
- 使用了
- NOI 风格输入输出:
- 输入格式修正为:
n m k紧跟 条边数据,最后是src dst。这是典型的图论题目输入习惯。
- 输入格式修正为:
使用方法:
- 将上述代码保存为
gen.cpp。 - 使用
g++ -O3 gen.cpp -o gen编译并运行。 - 它会自动生成
1.in, 1.out, ..., 10.in, 10.out。 - 将这些文件直接上传到你的 OJ 系统即可。
-
0
在 NOI 竞赛中,处理“带步数限制的最短路”有多种建模方式。从最直观的搜索,到经典的动态规划(Bellman-Ford),再到堆优化的状态空间搜索,体现了算法思维的不断进阶。
版本一:深度优先搜索 (DFS) —— 暴力与剪枝
思路: 递归地探索从
src开始的所有路径。记录当前花费和已走步数。 关键剪枝: 如果当前花费已经超过了目前找到的最小花费,或者步数已经超过 ,则停止搜索。#include <iostream> #include <vector> #include <algorithm> using namespace std; const int INF = 0x3f3f3f3f; int n, m, k, src, dst; struct Edge { int to, price; }; vector<Edge> adj[105]; int min_price = INF; // curr: 当前城市, steps: 已走边数, cost: 当前花费 void dfs(int curr, int steps, int cost) { // 剪枝:当前花费已超过已知最优解,或者步数超过限制 if (cost >= min_price) return; if (steps > k + 1) return; // 到达终点 if (curr == dst) { min_price = cost; return; } // 限制步数后,不能继续递归 if (steps == k + 1) return; for (auto &e : adj[curr]) { dfs(e.to, steps + 1, cost + e.price); } } int main() { if (!(cin >> n >> m >> k)) return 0; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; adj[u].push_back({v, w}); } cin >> src >> dst; dfs(src, 0, 0); if (min_price == INF) cout << -1 << endl; else cout << min_price << endl; return 0; }- 时间复杂度:,指数级。在稠密图中会非常慢。
- 空间复杂度:,递归栈深度。
版本二:Bellman-Ford 算法 (迭代 DP) —— 标准正解
思路分析: Bellman-Ford 的本质是进行 轮松弛,每轮松弛代表路径增加一条边。 易错点: 在第 次松弛时,必须基于第 次的快照进行。如果直接在原数组更新,会导致在一轮迭代中连续松弛多条边,从而违反了“中转 次”的限制。
#include <iostream> #include <vector> #include <cstring> #include <algorithm> using namespace std; const int INF = 0x3f3f3f3f; struct Edge { int u, v, w; }; int main() { int n, m, k, src, dst; if (!(cin >> n >> m >> k)) return 0; vector<Edge> edges; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; edges.push_back({u, v, w}); } cin >> src >> dst; // dist[i] 表示到达 i 点的最少花费 vector<int> dist(n, INF); dist[src] = 0; // 最多中转 k 次,即最多走 k+1 条边,进行 k+1 轮松弛 for (int i = 0; i <= k; i++) { // 关键:必须拷贝一份上一轮的结果,防止在本轮中产生链式更新(步数作弊) vector<int> last = dist; for (auto &e : edges) { if (last[e.u] != INF) { dist[e.v] = min(dist[e.v], last[e.u] + e.w); } } } if (dist[dst] == INF) cout << -1 << endl; else cout << dist[dst] << endl; return 0; }- 时间复杂度:。
- 空间复杂度:。
版本三:堆优化 Dijkstra (分层图思想) —— 最优扩展
思路分析: 传统的 Dijkstra 状态是
(u),这里需要扩展为(u, steps)。我们可以理解为有 层图,层与层之间通过航班连接。 优化: 在堆中,我们按价格排序。如果到达某个点时,价格比之前更贵且步数也更多,这个状态就可以舍弃。#include <iostream> #include <vector> #include <queue> #include <cstring> using namespace std; const int INF = 0x3f3f3f3f; struct Node { int u, cost, steps; // 小根堆,按价格排序 bool operator>(const Node& other) const { return cost > other.cost; } }; struct Edge { int to, price; }; int main() { int n, m, k, src, dst; if (!(cin >> n >> m >> k)) return 0; vector<Edge> adj[105]; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; adj[u].push_back({v, w}); } cin >> src >> dst; // best_steps[i] 记录到达 i 点时,在花费最小的前提下,最少的步数 // 或者简单使用 dist[u][steps] 数组 priority_queue<Node, vector<Node>, greater<Node>> pq; pq.push({src, 0, 0}); // 记录到达节点 u 且步数为 s 时的最小花费 int dist[105][105]; memset(dist, 0x3f, sizeof(dist)); while (!pq.empty()) { Node curr = pq.top(); pq.pop(); if (curr.u == dst) { cout << curr.cost << endl; return 0; } // 如果步数已达到限制,不能再飞了 if (curr.steps > k) continue; for (auto &e : adj[curr.u]) { if (curr.cost + e.price < dist[e.to][curr.steps + 1]) { dist[e.to][curr.steps + 1] = curr.cost + e.price; pq.push({e.to, dist[e.to][curr.steps + 1], curr.steps + 1}); } } } cout << -1 << endl; return 0; }- 时间复杂度:(状态数增加到 )。
- 空间复杂度:。
复杂度分析思考过程
-
为什么不直接用普通 Dijkstra? 普通 Dijkstra 找到的是全局最短路径(可能经过 10 站)。当你因为步数限制舍弃了这条路时,你可能已经把一条虽然贵一点但步数少(比如 1 站)的路径给“丢弃”了(标记为 visited)。所以必须在状态里加上步数。
-
Bellman-Ford 的天然优势: Bellman-Ford 的第 次循环结果就是“经过不超过 条边的最短路”。这与题目要求的“最多 站中转(即 条边)”完全契合。
时间复杂度优化建议
- 剪枝:在 Bellman-Ford 循环中,如果一轮松弛下来
dist数组没有变化,说明已经找到所有可能的最优解,可以break。 - SPFA 变体:虽然 SPFA 在 NOI 容易被卡,但在带步数限制的题目中,将步数作为状态层,SPFA 的效率往往很高。
读题关键词总结
- “最多中转 k 站”:核心限制,意味着路径边数 。
- “最便宜”:权值最小化。
- “不存在输出 -1”:边界处理,初始化时使用一个安全的较大值(INF)。
- :数据规模较小,说明 或 的算法是绝对安全的。
- 1
信息
- ID
- 19467
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 2
- 已通过
- 1
- 上传者