2 条题解

  • 0
    @ 2025-12-24 17:35:28

    作为金牌教练,制作数据不仅要追求随机性,更要针对算法的边界条件进行“定点爆破”。

    本题的核心在于哈希表的效率。我们需要构造能够覆盖“极长序列”、“极碎序列”、“大规模重复”以及“大数值差”的测试点。由于 n=105n=10^5 且涉及哈希,生成器必须保证在 O(n)O(n)O(nlogn)O(n \log n) 时间内完成计算,否则生成 1010 组数据会非常缓慢。

    以下是为你准备的自动化数据生成器。它集成了 O(n)O(n) 标程逻辑,采用非递归写法,确保生成的每一组数据都有准确的 .out 结果。

    一、 数据生成器代码 (C++14 标准)

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <unordered_set>
    #include <random>
    #include <ctime>
    #include <algorithm>
    
    using namespace std;
    
    // ================= 标准标程逻辑 (用于生成 .out) =================
    // 复杂度 O(n),用于计算标准答案
    int solve_standard(int n, const vector<int>& nums) {
        if (n == 0) return 0;
        unordered_set<int> st;
        for (int x : nums) st.insert(x);
    
        int longest_streak = 0;
        for (int num : st) {
            // 只有当 num 是序列起点时才开始计数
            if (st.find(num - 1) == st.end()) {
                int current_num = num;
                int current_streak = 1;
                while (st.find(current_num + 1) != st.end()) {
                    current_num += 1;
                    current_streak += 1;
                }
                longest_streak = max(longest_streak, current_streak);
            }
        }
        return longest_streak;
    }
    
    // ================= 数据生成辅助函数 =================
    void write_test_case(int id, int n, vector<int>& nums) {
        string in_name = to_string(id) + ".in";
        string out_name = to_string(id) + ".out";
    
        // 1. 写入 .in 文件
        ofstream fout(in_name);
        fout << n << "\n";
        for (int i = 0; i < n; ++i) {
            fout << nums[i] << (i == n - 1 ? "" : " ");
        }
        fout << endl;
        fout.close();
    
        // 2. 计算并写入 .out 文件
        int ans = solve_standard(n, nums);
        ofstream fans(out_name);
        fans << ans << endl;
        fans.close();
    
        cout << "Testcase " << id << " generated: n=" << n << ", ans=" << ans << endl;
    }
    
    // ================= 主生成逻辑 =================
    int main() {
        // 使用 mt19937 提供高质量随机数
        mt19937 rng(time(0));
        uniform_int_distribution<int> dist_large(-1000000000, 1000000000);
    
        for (int i = 1; i <= 10; ++i) {
            int n;
            vector<int> nums;
    
            if (i == 1) { // 样例 1
                n = 6; nums = {100, 4, 200, 1, 3, 2};
            } 
            else if (i == 2) { // 边界:n=0 和 n=1
                n = 1; nums = {0};
            }
            else if (i == 3) { // 边界:全同元素
                n = 1000; nums.assign(n, 777);
            }
            else if (i == 4) { // 正序连续序列 (测试最简单 O(n))
                n = 5000;
                for (int j = 1; j <= n; ++j) nums.push_back(j);
            }
            else if (i == 5) { // 倒序连续序列
                n = 5000;
                for (int j = n; j >= 1; --j) nums.push_back(j);
            }
            else if (i == 6) { // 多个碎片化序列 (测试起点判定)
                n = 10000;
                for (int j = 0; j < n; ++j) {
                    nums.push_back((j / 10) * 100 + (j % 10)); 
                }
                // 生成如 0-9, 100-109, 200-209...
            }
            else if (i == 7) { // 极大值的跨度 (测试哈希表性能)
                n = 20000;
                for (int j = 0; j < n / 2; ++j) {
                    nums.push_back(j); // 序列 1
                    nums.push_back(j + 500000000); // 序列 2
                }
            }
            else if (i == 8) { // 极限负值与正值
                n = 50000;
                for (int j = 0; j < n; ++j) {
                    if (j % 2 == 0) nums.push_back(-1000000000 + j);
                    else nums.push_back(1000000000 - j);
                }
            }
            else if (i == 9) { // 最大规模 n=10^5, 一个超长序列 + 干扰项
                n = 100000;
                int start_val = dist_large(rng) / 2;
                for (int j = 0; j < 80000; ++j) nums.push_back(start_val + j);
                for (int j = 0; j < 20000; ++j) nums.push_back(dist_large(rng));
                shuffle(nums.begin(), nums.end(), rng);
            }
            else { // 最大规模 n=10^5, 稀疏随机分布
                n = 100000;
                for (int j = 0; j < n; ++j) nums.push_back(dist_large(rng));
            }
    
            write_test_case(i, n, nums);
        }
        return 0;
    }
    

    二、 测试点设计思路(创建 OJ 必读)

    作为教练,我设计的这 10 个测试点覆盖了选手可能出错的每一个角落:

    测试点 数据特征 考察重点
    1 官方样例 确保基本逻辑正确。
    2-3 n1n \le 1 或重复值 考察对边界和去重的处理。
    4-5 连续递增/递减 考察 O(n)O(n) 算法在最理想/最简单情况下的表现。
    6 碎片化序列 核心考查点:考察是否正确过滤了非起点数字。如果逻辑不对(对每个数都往后搜),会退化到 O(n2)O(n^2)
    7-8 大数值/大跨度 考察哈希表处理极大整数的稳健性(避免数组桶排序的“偷鸡”做法)。
    9 80% 连续 + 20% 随机 压力测试n=105n=10^5。测试 while 循环在长序列下的效率。
    10 极限稀疏随机 压力测试n=105n=10^5。测试 unordered_set 的查找效率和抗哈希冲突能力。

    三、 考场避坑建议 (针对本题)

    1. 关于 std::unordered_set 的稳定性: 在 NOI 竞赛中,如果 nn 很大且时间限制紧(如 0.5s),要注意哈希表的常数。如果发现程序变慢,检查是否是因为频繁的 Rehash

      • 技巧:在生成器和选手的代码中,建议使用 num_set.reserve(n)。这可以大幅减少动态分配内存的时间。
    2. 避免使用 std::set: 如果选手使用了 std::set(红黑树,O(nlogn)O(n \log n)),在测试点 9 和 10 可能会因为对 10510^5 个元素进行 logn\log n 次查找而超时。

    3. 数据溢出: 虽然 numsinums_iint 范围内,但 num + 1num 等于 23112^{31}-1 时会发生溢出。

      • 对策:在本题的逻辑中,建议使用 long long current_num = num; 来进行 current_num + 1 的运算。
    4. 非递归安全: 本生成器和标程均不涉及递归,所有序列查询均通过 while 迭代完成。即使序列长度达到 10510^5,也不会触发系统栈溢出。

    你可以直接运行此生成器,它会自动在当前目录下生成一套高质量的练习数据。加油!

    • 0
      @ 2025-12-24 17:31:35

      你好,同学。这道题是考查哈希表优化搜索的经典案例。在 NOI 竞赛中,如果题目要求 O(n)O(n) 复杂度处理数值关系,通常意味着我们必须放弃排序,转而利用哈希表的 O(1)O(1) 查找特性。

      以下是基于 C++14 标准 的题解标准代码。


      一、 最长连续序列 标准题解 (C++14)

      #include <iostream>
      #include <vector>
      #include <unordered_set>
      #include <algorithm>
      
      using namespace std;
      
      /**
       * 核心逻辑:哈希集合起点探测法
       * 1. 利用 unordered_set 将所有数字存入哈希表,实现 O(1) 的查询。
       * 2. 遍历集合中的每个数,通过判断 "num - 1 是否存在" 来确定该数是否为序列起点。
       * 3. 只有从起点出发,才开始向后累加计数。
       */
      
      int solve() {
          int n;
          // NOI 竞赛中处理多组输入或读取 n
          if (!(cin >> n)) return 0;
          if (n == 0) return 0;
      
          vector<int> nums(n);
          unordered_set<int> num_set;
          
          // 关键优化点:预分配内存,减少 unordered_set 频繁扩容导致的 Rehash 耗时
          num_set.reserve(n); 
      
          for (int i = 0; i < n; ++i) {
              cin >> nums[i];
              num_set.insert(nums[i]); // 自动去重
          }
      
          int longest_streak = 0;
      
          // 注意:遍历哈希表而不是原数组,可以减少重复元素的无效判断
          for (int num : num_set) {
              // 关键逻辑:寻找序列的“绝对起点”
              // 如果 num - 1 在集合中,说明 num 肯定不是起点,跳过它。
              // 只有 num 是起点时,才进入下面的 while 循环。
              if (num_set.find(num - 1) == num_set.end()) {
                  int current_num = num;
                  int current_streak = 1;
      
                  // 只有作为起点的数才会触发后续查找
                  while (num_set.find(current_num + 1) != num_set.end()) {
                      current_num += 1;
                      current_streak += 1;
                  }
      
                  // 更新全局最大长度
                  longest_streak = max(longest_streak, current_streak);
              }
          }
      
          return longest_streak;
      }
      
      int main() {
          // NOI 竞赛提速必备:关闭 I/O 同步流
          ios::sync_with_stdio(false);
          cin.tie(nullptr);
      
          cout << solve() << endl;
      
          return 0;
      }
      

      二、 复杂度分析思考过程

      1. 时间复杂度:均摊 O(n)O(n)

      • 第一步(插入):将 nn 个数字插入 unordered_set,期望时间为 O(n)O(n)
      • 第二步(遍历与计数)
        • 外部 for 循环遍历了集合中的每个元素,共执行 nn 次。
        • 内部 while 循环看起来很吓人,但由于我们加了 !num_set.count(num - 1) 的判断,每个数字只会在作为序列的一员时被 while 循环访问一次
        • 例如:对于序列 1, 2, 3, 4,只有在处理 1 的时候会进入 while 遍历 2, 3, 4。当处理到 2, 3, 4 时,因为它们前面都有数字,会直接被 if 过滤掉。
      • 结论:总的操作次数是 n+n=2nn + n = 2n,时间复杂度为 O(n)O(n)

      2. 空间复杂度:O(n)O(n)

      • 推导:需要使用哈希表存储所有的数字。在 n=105n=10^5 的量级下,约占用几兆字节内存。
      • 结论:在 NOI 256MB 的限制下非常安全。

      三、 考场优化建议与易错点

      1. 哈希冲突风险(防 Hack): 在某些极端测试点下,unordered_set 的哈希函数可能被恶意构造的数据触发大量冲突,导致复杂度退化到 O(n2)O(n^2)

        • 建议:在真正的 NOI 考场上,若担心被卡,可以使用 std::unordered_set<int, custom_hash> 自定义哈希函数(加入随机扰动时间戳)。
      2. 避免 std::setstd::set 底层是红黑树,查询是 O(logn)O(\log n)。如果使用它,总时间复杂度会变成 O(nlogn)O(n \log n)。在 10510^5 规模且时限紧的情况下,可能会丢掉 10-20 分。

      3. 使用 reserve 预热: 哈希表在元素增多时会自动扩容。通过 num_set.reserve(n) 提前申请足够的桶(bucket),可以显著减少程序运行常数,这是抢时限的小窍门。

      4. I/O 性能: 如果 nn 很大,输出一定要用 \n 而不是 endl,因为 endl 会强制刷新输出缓冲区,极其耗时。


      四、 读题关键词总结

      在 NOI 考场上,看到以下特征要迅速反应:

      • “未排序” + “线性时间 / O(n)”:这是最明显的信号,提示你不能排序,必须用哈希表。
      • “连续”:在数值空间中连续,意味着 xx 后面必跟着 x+1x+1
      • “不要求序列元素在原数组中连续”:这告诉我们原数组的顺序是没用的信息,进一步锁定了“数值集合”处理法。

      教练寄语: 这道题的精髓在于**“起点的判定”**。通过一个简单的 num-1 检查,把一个看似 O(n2)O(n^2) 的暴力搜索剪枝成了均摊 O(n)O(n) 的神级算法。这种对计算冗余的剔除,就是算法竞赛的魅力所在。加油!

      • 1

      信息

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