2 条题解

  • 0
    @ 2026-1-8 16:56:46

    为了方便你快速构建 OJ(Online Judge)测试数据,我编写了一个基于 C++14 标准的自动化数据生成器。它采用了**迭代(非递归)**的 BFS 逻辑来产生标准答案,并针对 N=5×104N=5 \times 10^4 的规模设计了多种复杂的图论模型。

    1. 数据生成器设计思路

    • 测试点场景分布
      • T1 (最小边界)N=1N=1,权值为 0。
      • T2-T3 (随机小规模):覆盖基础逻辑。
      • T4 (长链结构):构造 O(N)O(N) 的深度路径,测试遍历完整性。
      • T5 (全 0 数组):测试多起点目标。
      • T6 (不可达陷阱):将数组分为两个孤岛,起点所在的岛没有 0。
      • T7 (大循环):构造相互指向的循环跳跃。
      • T8 (之字形跳跃):构造跨度极大的左右反复跳跃。
      • T9-T10 (最大规模随机):测试 N=5×104N=5 \times 10^4 下的性能。
    • 安全性:求解器使用 std::queue 实现 BFS,绝对不会爆栈。
    • 文件大小控制:单个 .in 文件约 200300 KB200 \sim 300\text{ KB},总压缩包非常小。

    2. 数据生成器源码 (C++14)

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <queue>
    #include <random>
    #include <string>
    #include <algorithm>
    
    using namespace std;
    
    // ---------------- 核心算法:求解器用于生成标准答案 (.out) ----------------
    bool solve(int n, int start, const vector<int>& arr) {
        if (arr[start] == 0) return true;
        
        queue<int> q;
        vector<bool> vis(n, false);
        
        q.push(start);
        vis[start] = true;
        
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            
            if (arr[u] == 0) return true;
            
            int next_pos[2] = {u + arr[u], u - arr[u]};
            for (int i = 0; i < 2; ++i) {
                int v = next_pos[i];
                if (v >= 0 && v < n && !vis[v]) {
                    vis[v] = true;
                    q.push(v);
                }
            }
        }
        return false;
    }
    
    // ---------------- 随机数工具 ----------------
    mt19937 rng(1337);
    int randInt(int l, int r) {
        return uniform_int_distribution<int>(l, r)(rng);
    }
    
    // ---------------- 构造逻辑 ----------------
    void generate_case(int t) {
        string in_name = to_string(t) + ".in";
        string out_name = to_string(t) + ".out";
        ofstream fin(in_name);
        
        int n, start;
        vector<int> arr;
        
        if (t == 1) { // 最小边界
            n = 1; start = 0; arr = {0};
        } else if (t == 2) { // 小规模随机
            n = 10; start = randInt(0, 9);
            for(int i=0; i<n; ++i) arr.push_back(randInt(0, 5));
        } else if (t == 3) { // 随机不可达
            n = 20; start = 0;
            for(int i=0; i<n; ++i) arr.push_back(randInt(5, 10)); // 跳跃很大,难以踩到0
            arr[19] = 0; // 0 在最后
        } else if (t == 4) { // 极限长链 (True)
            n = 50000; start = 0;
            arr.assign(n, 1); // 每次跳 1 步
            arr[n-1] = 0;
        } else if (t == 5) { // 高密度 0
            n = 50000; start = randInt(0, n-1);
            for(int i=0; i<n; ++i) arr.push_back(randInt(0, 2) == 0 ? 0 : randInt(1, 10));
        } else if (t == 6) { // 孤岛陷阱 (False)
            n = 50000; start = 0;
            arr.assign(n, 0);
            for(int i=0; i < n/2; ++i) arr[i] = randInt(1, 10); // 前半段没有0,且跳不出前半段
            for(int i=n/2; i < n; ++i) arr[i] = 0; // 0 全部在后半段
        } else if (t == 7) { // 循环跳跃 (False)
            n = 50000; start = 0;
            arr.assign(n, 2); // 0->2, 2->4... 永远踩不到奇数位的0
            for(int i=1; i<n; i+=2) arr[i] = 0;
        } else if (t == 8) { // 之字形强力跳跃
            n = 50000; start = n/2;
            for(int i=0; i<n; ++i) arr.push_back(randInt(n/4, n/2));
            arr[0] = 0; arr[n-1] = 0;
        } else { // 最大规模随机
            n = 50000; start = randInt(0, n-1);
            for(int i=0; i<n; ++i) arr.push_back(randInt(0, n-1));
            if (t == 10) arr[randInt(0, n-1)] = 0; // 确保至少有一个0
        }
    
        // 写入输入文件
        fin << n << " " << start << "\n";
        for (int i = 0; i < n; ++i) {
            fin << arr[i] << (i == n - 1 ? "" : " ");
        }
        fin << "\n";
        fin.close();
    
        // 生成并写入答案
        ofstream fout(out_name);
        fout << (solve(n, start, arr) ? "true" : "false") << endl;
        fout.close();
        
        cout << "Testcase " << t << " [N=" << n << "] finished." << endl;
    }
    
    int main() {
        for (int i = 1; i <= 10; ++i) generate_case(i);
        return 0;
    }
    

    3. 测试点详细说明

    测试点 NN 规模 特征描述 考查点
    1 只有 1 个元素且为 0 边界特判
    2 10 小规模随机 基础 BFS 路径搜索
    3 20 稀疏 0 位 跨度较大的跳跃处理
    4 50,000 全为 1 的跳跃链 考查 O(N)O(N) 遍历的完整性
    5 极其密集的 0 考查一旦发现目标立即退出的效率
    6 分裂的组件 (Unreachable) 考查不可达情况下的 vis 数组防死循环能力
    7 步长为 2 的偶数环 考查逻辑严密性(踩不到奇数位的 0)
    8 大跨度左右震荡 考查边界检查 if (v >= 0 && v < n)
    9 完全随机数据 综合压力测试
    10 随机 + 强制 0 存在

    4. 教练的优化建议与使用说明

    1. 内存与效率:对于 N=50,000N=50,000 的数据,使用 vector<bool>bool vis[MAXN] 仅占用约 50 KB50\text{ KB} 内存,效率极高。
    2. 避免死循环:在生成不连通图(如 T6, T7)时,学生代码如果没有 vis 标记,会因在两个下标间无限反复跳跃而导致 TLEMLE
    3. OJ 部署
      • .in.out 文件配对上传。
      • 设置时间限制为 1.0s(实际 O(N)O(N) 运行只需约 0.01s0.01s)。
      • 设置空间限制为 128MB(实际只需约 2MB2MB)。

    这个生成器产生的输入格式完全符合竞赛要求的 N start 后接数组模式,且逻辑严密,适合作为标准的练习题库数据。

    • 0
      @ 2026-1-8 16:54:01

      在处理“可达性”问题时,搜索算法是核心。对于本题,N=5×104N = 5 \times 10^4 的数据规模要求我们必须达到 O(N)O(N) 的时间复杂度。

      以下提供三个版本的 C++ 代码:从基础的递归 DFS 到标准 BFS,再到针对竞赛常数优化的版本。


      版本一:深度优先搜索 (DFS) 递归版

      思路:从 start 开始,递归探测两个跳跃方向。使用 vis 数组记录已访问位置。 易错点:在 NOI 环境下,5×1045 \times 10^4 的递归深度可能接近系统默认栈限制,需注意。

      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      const int MAXN = 50005;
      int arr[MAXN];
      bool vis[MAXN];
      int n;
      
      bool dfs(int u) {
          // 越界检查或已访问检查
          if (u < 0 || u >= n || vis[u]) return false;
          
          // 目标检查:找到 0 则成功
          if (arr[u] == 0) return true;
          
          // 标记当前节点已访问,防止死循环
          vis[u] = true;
          
          // 递归向左和向右跳跃
          // 利用逻辑或的短路特性:只要一边找到即可返回 true
          return dfs(u + arr[u]) || dfs(u - arr[u]);
      }
      
      int main() {
          int start;
          if (!(cin >> n >> start)) return 0;
          for (int i = 0; i < n; i++) cin >> arr[i];
      
          if (dfs(start)) cout << "true" << endl;
          else cout << "false" << endl;
      
          return 0;
      }
      

      版本二:广度优先搜索 (BFS) 标准版

      思路:使用队列按层级扩散。BFS 在判定“是否存在路径”时非常稳健,且没有递归爆栈的风险,是竞赛中最推荐的写法。

      #include <iostream>
      #include <vector>
      #include <queue>
      
      using namespace std;
      
      const int MAXN = 50005;
      int arr[MAXN];
      bool vis[MAXN];
      
      int main() {
          int n, start;
          // 竞赛建议:使用快速输入输出或 scanf/printf
          if (!(cin >> n >> start)) return 0;
          for (int i = 0; i < n; i++) cin >> arr[i];
      
          queue<int> q;
          q.push(start);
          vis[start] = true;
      
          bool found = false;
          while (!q.empty()) {
              int u = q.front();
              q.pop();
      
              // 判定是否达到目标
              if (arr[u] == 0) {
                  found = true;
                  break;
              }
      
              // 尝试两个方向
              int directions[2] = {u + arr[u], u - arr[u]};
              for (int i = 0; i < 2; i++) {
                  int v = directions[i];
                  // 易错点:必须先判断越界,再访问 vis 数组
                  if (v >= 0 && v < n && !vis[v]) {
                      vis[v] = true;
                      q.push(v);
                  }
              }
          }
      
          if (found) cout << "true" << endl;
          else cout << "false" << endl;
      
          return 0;
      }
      

      版本三:竞赛极致优化版 (数组队列 + 快读)

      思路:在 NN 较大时,std::queue 的内存分配和 cin 的效率较低。通过数组模拟队列和手写快读,可以获得最优的常数表现。

      #include <cstdio>
      
      // 手写快速读入
      inline int read() {
          int x = 0; char ch = getchar();
          while (ch < '0' || ch > '9') ch = getchar();
          while (ch >= '0' && ch <= '9') { x = x * 10 + (ch - '0'); ch = getchar(); }
          return x;
      }
      
      const int MAXN = 50005;
      int arr[MAXN];
      bool vis[MAXN];
      int q[MAXN]; // 手写静态队列,大小为 N 即可
      
      int main() {
          int n, start;
          n = read();
          start = read();
          for (int i = 0; i < n; i++) arr[i] = read();
      
          int head = 0, tail = 0;
          q[tail++] = start;
          vis[start] = true;
      
          bool found = false;
          while (head < tail) {
              int u = q[head++];
              
              if (arr[u] == 0) {
                  found = true;
                  break;
              }
      
              // 向右跳
              int v1 = u + arr[u];
              if (v1 < n && !vis[v1]) {
                  vis[v1] = true;
                  q[tail++] = v1;
              }
              
              // 向左跳
              int v2 = u - arr[u];
              if (v2 >= 0 && !vis[v2]) {
                  vis[v2] = true;
                  q[tail++] = v2;
              }
          }
      
          if (found) printf("true\n");
          else printf("false\n");
      
          return 0;
      }
      

      复杂度分析思考过程

      1. 时间复杂度分析

        • 遍历逻辑:在搜索过程中,每个下标(节点)最多只会被标记为 vis 一次,每个节点最多产生 2 个分支。
        • 计算:总计访问的节点数为 O(N)O(N),总边数为 O(2N)O(2N)
        • 结论:时间复杂度为 O(N)O(N)。对于 5×1045 \times 10^4 的规模,运行时间在 1~5ms 之间,远快于竞赛限制。
      2. 空间复杂度分析

        • 存储空间arr 数组 O(N)O(N)vis 数组 O(N)O(N)
        • 辅助空间:BFS 队列或 DFS 递归栈最坏情况下存储 NN 个元素,空间 O(N)O(N)
        • 结论:总空间复杂度为 O(N)O(N)。约占用 1MB 内存,远低于 128MB 的限制。

      时间复杂度优化建议

      • 及早退出:在将节点推入队列(或进入递归)的一瞬间就判断 arr[v] == 0,可以比弹出队列时再判断少运行一轮。
      • 内存优化:如果内存限制极其严格(例如只有 2MB),可以利用 arr[i] 的正负号来标记 visited(例如将 arr[i] 变为负数),从而省去 vis 数组。但这种做法在原数组有 0 的情况下需要特殊处理。
      • IO 优化:在 NOI 竞赛中,若 N105N \ge 10^5,请务必使用 scanf 或快读,cin 即使加了 sync_with_stdio(false) 有时依然不够快。

      总结

      作为教练,我推荐你在正式考场上优先使用 版本二 (BFS)。它结构清晰,且能有效规避 DFS 可能带来的栈溢出风险,同时通过 O(N)O(N) 的效率稳拿满分。

      • 1

      信息

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