2 条题解

  • 0
    @ 2025-12-30 10:27:01

    为了方便你创建 OJ 测试数据,我编写了这个完整的数据生成器。它不仅生成输入文件(.in),还会利用标准算法逻辑实时计算并生成对应的输出文件(.out)。

    数据生成器设计重点

    1. 逻辑准确性:生成器内置了 O(n)O(n) 时间、O(1)O(1) 空间的双指针算法,用于验证数据并产出标准答案。
    2. 数据覆盖面
      • 极端边界n=0n=0(空链表)、n=1,pos=1n=1, pos=-1(单节点无环)、n=1,pos=0n=1, pos=0(单节点自环)。
      • 位置覆盖:入口在头节点(pos=0pos=0)、入口在尾节点(pos=n1pos=n-1)、入口在中间。
      • 规模覆盖:从 n=2n=2n=104n=10^4 的梯度增长。
    3. 安全性:固定 mt19937 随机种子,保证结果幂等(每次运行生成的 10 组数据都相同)。
    #include <iostream>
    #include <vector>
    #include <string>
    #include <fstream>
    #include <random>
    #include <algorithm>
    
    using namespace std;
    
    /**
     * @brief 标准答案计算逻辑 (用于生成 .out 文件)
     * 模拟 NOI 竞赛中的双指针逻辑,但不直接依赖 pos 输入
     */
    string get_output_logic(int n, int pos, const vector<int>& vals) {
        if (n == 0 || pos == -1) {
            return "no cycle";
        }
        // 根据题意,如果有环,入口一定是索引为 pos 的那个节点
        return "tail connects to node index " + to_string(pos) + 
               " with value " + to_string(vals[pos]);
    }
    
    /**
     * @brief 写入测试点文件对
     */
    void generate_case(int id, int n, int pos, const vector<int>& vals) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
    
        // 1. 生成 .in 文件
        ofstream fin(in_name);
        fin << n << " " << pos << "\n";
        for (int i = 0; i < n; ++i) {
            fin << vals[i] << (i == n - 1 ? "" : " ");
        }
        fin << endl;
        fin.close();
    
        // 2. 生成 .out 文件
        ofstream fout(out_name);
        fout << get_output_logic(n, pos, vals) << endl;
        fout.close();
    }
    
    int main() {
        // 使用固定的随机种子,保证数据生成的唯一性和可重复性
        mt19937 rng(20251230);
    
        for (int i = 1; i <= 10; ++i) {
            int n = 0;
            int pos = -1;
            vector<int> vals;
    
            // 根据测试点编号设计不同场景
            switch (i) {
                case 1: // 边界:空链表
                    n = 0; pos = -1;
                    break;
                case 2: // 边界:单节点无环
                    n = 1; pos = -1;
                    vals = {88};
                    break;
                case 3: // 边界:单节点自环
                    n = 1; pos = 0;
                    vals = {99};
                    break;
                case 4: // 场景:两个节点,环指向头节点
                    n = 2; pos = 0;
                    vals = {10, 20};
                    break;
                case 5: // 场景:三个节点,环指向中间
                    n = 3; pos = 1;
                    vals = {1, 2, 3};
                    break;
                case 6: // 场景:中等规模无环
                    n = 500; pos = -1;
                    break;
                case 7: // 场景:中等规模,环指向最后一个节点(自环)
                    n = 1000; pos = 999;
                    break;
                case 8: // 压力:大规模,环指向头节点
                    n = 10000; pos = 0;
                    break;
                case 9: // 压力:大规模,环在黄金分割点附近
                    n = 10000; pos = 6180;
                    break;
                case 10: // 压力:大规模随机环
                    n = 10000; pos = (int)(rng() % 10000);
                    break;
            }
    
            // 自动填充随机数值 (如果 vals 为空)
            if (vals.empty() && n > 0) {
                for (int j = 0; j < n; ++j) {
                    // 生成 -10^5 到 10^5 之间的整数
                    vals.push_back((int)(rng() % 200001) - 100000);
                }
            }
    
            generate_case(i, n, pos, vals);
            cout << "Generated test case " << i << ": n=" << n << ", pos=" << pos << endl;
        }
    
        return 0;
    }
    

    测试点覆盖说明表

    测试点 nn 规模 pospos 设定 测试目的
    1.in 0 -1 空链表鲁棒性测试。
    2.in 1 单个节点无环。
    3.in 0 最小可能的环(自环)。
    4.in 2 环正好覆盖整个链表。
    5.in 3 1 环入口在链表中间。
    6.in 500 -1 较长的无环链表,考察 fast 指针到达终点的判断。
    7.in 1000 999 入口在最后一个节点,容易在 a = (n-1)L + c 推导中出错的情况。
    8.in 10000 0 最大规模测试,环覆盖全长。
    9.in 6180 最大规模测试,复杂路径长度推导。
    10.in 随机 综合稳定性压力测试。

    如何部署:

    1. 编译g++ gen.cpp -o gen -O2 -std=c++14
    2. 生成:运行 ./gen
    3. 结果:你将得到 1.in ~ 10.in1.out ~ 10.out
    4. 上传:文件总大小约为 400KB,完全符合 OJ 小于 2MB 的要求。

    复杂度与安全:

    • 生成耗时:由于 n=104n=10^4,总生成时间约为 0.02s,极速且无超时风险。
    • 空间:使用 std::vector 存储,10410^4 个整数仅需约 40KB 内存,非常安全。
    • 异常处理:已处理 n=0n=0 的除外情况。由于 pospos 只是索引,在逻辑中通过 vals[pos] 访问前已通过 pos != -1 判定,杜绝了数组越界风险。
    • 0
      @ 2025-12-30 10:23:16

      在 NOI 竞赛中,环形链表 II 是考察“算法推导”与“代码鲁棒性”的经典结合。我们要从最直观的“记录地址”法,进化到利用数学性质的“双指针”法。


      版本一:哈希集合法(空间换时间)

      思路思考: 我们利用一个集合(std::set)来记录遍历过程中每一个节点的地址。当我们第一次遇到一个已经存在于集合中的地址时,这个节点就是环的入口。

      • 时间复杂度O(nlogn)O(n \log n)。遍历 nn 个节点,每次插入和查找 set 耗时 O(logn)O(\log n)
      • 空间复杂度O(n)O(n)。最坏情况下(环在尾部或无环)需要存储所有节点的指针。
      • 缺点:在 nn 很大时,std::set 的内存开销和树旋转耗时可能导致 TLE 或 MLE。
      #include <iostream>
      #include <vector>
      #include <set>
      
      using namespace std;
      
      struct ListNode {
          int val;
          int index; // 额外记录索引,方便按题目要求输出
          ListNode* next;
          ListNode(int v, int i) : val(v), index(i), next(nullptr) {}
      };
      
      int main() {
          int n, pos;
          if (!(cin >> n >> pos)) return 0;
      
          // 构建链表
          vector<ListNode*> nodes;
          for (int i = 0; i < n; ++i) {
              int v; cin >> v;
              nodes.push_back(new ListNode(v, i));
          }
          for (int i = 0; i < n - 1; ++i) nodes[i]->next = nodes[i + 1];
          if (pos != -1) nodes[n - 1]->next = nodes[pos];
      
          // --- 算法核心开始 ---
          set<ListNode*> visited;
          ListNode* cur = (n > 0) ? nodes[0] : nullptr;
          ListNode* entrance = nullptr;
      
          while (cur != nullptr) {
              if (visited.count(cur)) {
                  entrance = cur; // 第一次重复出现的节点即为入口
                  break;
              }
              visited.insert(cur);
              cur = cur->next;
          }
          // --- 算法核心结束 ---
      
          if (!entrance) cout << "no cycle" << endl;
          else cout << "tail connects to node index " << entrance->index 
                    << " with value " << entrance->val << endl;
      
          return 0;
      }
      

      版本二:最优解 - 快慢指针(Floyd 判环 + 入口定位)

      思路思考: 这是本题的最优解。分为两个阶段:

      1. 判定是否有环:快指针走 2 步,慢指针走 1 步。若相遇,则必有环。
      2. 寻找入口:根据数学推导 a=ca = c(头到入口距离 = 相遇点到入口距离),将一个指针放回起点,两个指针同步走(每次 1 步),再次相遇处即为入口。
      • 时间复杂度O(n)O(n)。第一阶段快指针最多走 2n2n,第二阶段最多走 nn
      • 空间复杂度O(1)O(1)。只使用了常数个辅助指针。
      #include <iostream>
      #include <vector>
      
      using namespace std;
      
      struct ListNode {
          int val;
          int index;
          ListNode* next;
          ListNode(int v, int i) : val(v), index(i), next(nullptr) {}
      };
      
      // 最优解函数
      ListNode* detectCycle(ListNode* head) {
          if (!head || !head->next) return nullptr;
      
          ListNode *slow = head, *fast = head;
          ListNode *meet = nullptr;
      
          // 阶段 1: 寻找相遇点
          while (fast && fast->next) {
              slow = slow->next;
              fast = fast->next->next;
              if (slow == fast) {
                  meet = slow;
                  break;
              }
          }
      
          // 若无环,直接返回
          if (!meet) return nullptr;
      
          // 阶段 2: 寻找入口
          ListNode* ptr1 = head;
          ListNode* ptr2 = meet;
          while (ptr1 != ptr2) {
              ptr1 = ptr1->next;
              ptr2 = ptr2->next;
          }
          return ptr1; // 再次相遇点即为入口
      }
      
      int main() {
          ios::sync_with_stdio(false); // IO 优化
          cin.tie(0);
      
          int n, pos;
          if (!(cin >> n >> pos)) return 0;
      
          // 构建链表 (NOI 竞赛中建议使用 vector 或数组模拟内存池)
          if (n == 0) {
              cout << "no cycle" << endl;
              return 0;
          }
      
          vector<ListNode*> nodes;
          for (int i = 0; i < n; ++i) {
              int v; cin >> v;
              nodes.push_back(new ListNode(v, i));
          }
          for (int i = 0; i < n - 1; ++i) nodes[i]->next = nodes[i + 1];
          if (pos != -1) nodes[n - 1]->next = nodes[pos];
      
          ListNode* entrance = detectCycle(nodes[0]);
      
          if (!entrance) cout << "no cycle" << endl;
          else cout << "tail connects to node index " << entrance->index 
                    << " with value " << entrance->val << endl;
      
          return 0;
      }
      

      关键点与易错点注释

      1. fast && fast->next:这是链表题最容易爆 Segmentation Fault 的地方。在访问 fast->next->next 之前,必须确保前两步都不是空。
      2. 指针相遇后的重置:第二阶段中,两个指针的步长必须都设为 1
      3. 空链表与单节点:这是 n=0n=0n=1n=1 时的边界情况。如果 n=1,pos=1n=1, pos=-1,代码应能正确识别出 fast->next 为空。
      4. 地址对比 vs 数值对比:判环必须对比指针的内存地址ptr1 == ptr2),因为链表中可能存在两个数值完全相同但位置不同的节点。

      时间复杂度优化建议

      1. 静态数组模拟(静态链表): 在 NOI 竞赛中,使用 new 动态分配内存不仅慢,而且容易产生碎片。建议使用数组实现:

        int val[10005], nxt[10005], idx[10005];
        // 用数组下标代替指针:cur = nxt[cur];
        

        这样可以将运行效率提升 3-5 倍,且内存管理更可控。

      2. 快读 (Fast I/O): 当 nn 达到 10510^5 以上规模时,普通的 cin 会成为瓶颈。建议使用:

        inline int read() {
            int x = 0, f = 1; char ch = getchar();
            while (ch < '0' || ch > '9') { if (ch == '-') f = -1; ch = getchar(); }
            while (ch >= '0' && ch <= '9') { x = x * 10 + ch - '0'; ch = getchar(); }
            return x * f;
        }
        
      3. 内存回收: 虽然在竞赛中程序结束会自动释放,但养成 delete 的习惯能防止在一些特殊的长时评测中 MLE。

      总结

      • 读题关键词:看到“环”、“入口”、“常量空间”,直接锁定双指针(快慢指针)
      • 数学模型:记住 a=ca=c(从头走 = 从相遇点走),这是解决环入口问题的唯一“银弹”。
      • 1

      信息

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