2 条题解

  • 0
    @ 2026-1-9 14:21:37

    为了方便你创建 OJ(Online Judge)测试数据,我编写了这个 C++ 数据生成器。它包含了标准 Bellman-Ford 算法作为标程(用于生成 .out 文件)以及针对性构造逻辑(用于生成 .in 文件)。

    数据生成器设计逻辑

    1. Case 1-3:题目官方样例,确保基础逻辑正确。
    2. Case 4:最小边界测试(N=2N=2)。
    3. Case 5 (长链图):构造一条长度为 50 的链,但 KK 设置为 10,测试程序是否能正确输出 -1(不可达)。
    4. Case 6 (稠密图)N=100N=100 的完全图,测试算法在边数较多时的稳定性。
    5. Case 7 (价格路径权衡):构造一条直达航线(贵)和一条中转多次航线(便宜),通过调整 KK 测试决策逻辑。
    6. Case 8-10:大规模随机图,包含各种 KK 值,确保没有漏掉的连通分支。

    数据生成器 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;
    }
    

    数据生成器设计要点说明:

    1. 文件大小优化
      • N100N \le 100M4950M \le 4950。即使在最大规模下,每个 .in 文件的大小约为 50KB。10 个测试点总大小约 0.5MB,远低于 2MB 限制,加载极快。
    2. 避免超前松弛
      • solve_std 中使用了 vector<int> last = dist;。这是 Bellman-Ford 处理限步最短路的关键。如果直接更新 dist,会在一轮迭代内走多步,导致 .out 答案错误。
    3. 无重边与自环逻辑
      • 通过 std::set<pair<int, int>> seen 保证随机生成的边不重复,且 u != v 排除自环。
    4. 异常处理
      • 使用了 mt19937 随机引擎代替 rand(),保证了跨平台的数据一致性和随机质量。
      • 航班价格从 11 开始,避免了 00 权边可能导致的某些算法逻辑歧义。
    5. NOI 风格输入输出
      • 输入格式修正为:n m k 紧跟 mm 条边数据,最后是 src dst。这是典型的图论题目输入习惯。

    使用方法:

    1. 将上述代码保存为 gen.cpp
    2. 使用 g++ -O3 gen.cpp -o gen 编译并运行。
    3. 它会自动生成 1.in, 1.out, ..., 10.in, 10.out
    4. 将这些文件直接上传到你的 OJ 系统即可。
    • 0
      @ 2026-1-9 14:19:25

      在 NOI 竞赛中,处理“带步数限制的最短路”有多种建模方式。从最直观的搜索,到经典的动态规划(Bellman-Ford),再到堆优化的状态空间搜索,体现了算法思维的不断进阶。


      版本一:深度优先搜索 (DFS) —— 暴力与剪枝

      思路: 递归地探索从 src 开始的所有路径。记录当前花费和已走步数。 关键剪枝: 如果当前花费已经超过了目前找到的最小花费,或者步数已经超过 K+1K+1,则停止搜索。

      #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;
      }
      
      • 时间复杂度O(NK)O(N^K),指数级。在稠密图中会非常慢。
      • 空间复杂度O(K)O(K),递归栈深度。

      版本二:Bellman-Ford 算法 (迭代 DP) —— 标准正解

      思路分析: Bellman-Ford 的本质是进行 tt 轮松弛,每轮松弛代表路径增加一条边。 易错点: 在第 tt 次松弛时,必须基于第 t1t-1 次的快照进行。如果直接在原数组更新,会导致在一轮迭代中连续松弛多条边,从而违反了“中转 KK 次”的限制。

      #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;
      }
      
      • 时间复杂度O(KM)O(K \cdot M)
      • 空间复杂度O(N)O(N)

      版本三:堆优化 Dijkstra (分层图思想) —— 最优扩展

      思路分析: 传统的 Dijkstra 状态是 (u),这里需要扩展为 (u, steps)。我们可以理解为有 K+1K+1 层图,层与层之间通过航班连接。 优化: 在堆中,我们按价格排序。如果到达某个点时,价格比之前更贵且步数也更多,这个状态就可以舍弃。

      #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;
      }
      
      • 时间复杂度O(MlogNK)O(M \log N \cdot K)(状态数增加到 N×KN \times K)。
      • 空间复杂度O(NK)O(N \cdot K)

      复杂度分析思考过程

      1. 为什么不直接用普通 Dijkstra? 普通 Dijkstra 找到的是全局最短路径(可能经过 10 站)。当你因为步数限制舍弃了这条路时,你可能已经把一条虽然贵一点但步数少(比如 1 站)的路径给“丢弃”了(标记为 visited)。所以必须在状态里加上步数。

      2. Bellman-Ford 的天然优势: Bellman-Ford 的第 ii 次循环结果就是“经过不超过 ii 条边的最短路”。这与题目要求的“最多 kk 站中转(即 k+1k+1 条边)”完全契合。

      时间复杂度优化建议

      • 剪枝:在 Bellman-Ford 循环中,如果一轮松弛下来 dist 数组没有变化,说明已经找到所有可能的最优解,可以 break
      • SPFA 变体:虽然 SPFA 在 NOI 容易被卡,但在带步数限制的题目中,将步数作为状态层,SPFA 的效率往往很高。

      读题关键词总结

      • “最多中转 k 站”:核心限制,意味着路径边数 k+1\le k+1
      • “最便宜”:权值最小化。
      • “不存在输出 -1”:边界处理,初始化时使用一个安全的较大值(INF)。
      • N=100N=100:数据规模较小,说明 O(N3)O(N^3)O(KM)O(K \cdot M) 的算法是绝对安全的。
      • 1

      信息

      ID
      19467
      时间
      1000ms
      内存
      128MiB
      难度
      10
      标签
      (无)
      递交数
      2
      已通过
      1
      上传者