3 条题解

  • 0
    @ 2026-6-5 11:53:07

    太棒了!自己编写数据生成器,是你从“答题者”蜕变为“出题人”的关键一步。

    对于这道题,我们面临和之前一样的情况:题目原定的数据范围 N,M1000N, M \le 1000 实在太温柔了!在这个范围内,O(N×M)O(N \times M) 的双重循环暴力搜索最多只需执行 10610^6 次,任何正常的评测机都会让它瞬间拿到 100 分。

    为了满足你**“让暴力解法能部分 AC 但不能全部 AC”的要求,我们在保证前几个测试点完全符合原题意的前提下,必须在最后几个测试点偷偷“超纲”**,把 NNMM 放大到 10510^5 级别。在这个级别下:

    • 新手暴力解法 O(N×M)O(N \times M) 需要查找 101010^{10} 次,绝对会因为超时(TLE)而挂掉。
    • 标答哈希解法 O(N+M)O(N + M) 只需要 2×1052 \times 10^5 次操作,依然 11 毫秒极速 AC

    同时,为了满足你对**文件大小(理想值 2MB 内)**的要求,10510^5 个整数以空格分隔,产生的文件大小约为 500 KB ~ 600 KB,完美符合极其轻量化的标准!


    核心设计思路(10 个测试点分布)

    1. Test 1 (N=3):样例 1。完全乱序但全员到齐。
    2. Test 2 (N=3):样例 2。极端重复报数,大量缺席。
    3. Test 3 (N=2)极小边界。班上只有两人,且只有一人报数。
    4. Test 4 (N=100):局部重复。前 50 次全报 0,后 50 次全报 1,测试去重逻辑。
    5. Test 5 (N=500):常规区间。所有人刚好都报了一次(全员到齐),打乱顺序。
    6. Test 6 (N=1000):符合原题最大数据限制。纯随机缺席测试。
    7. Test 7 (N=1000):符合原题最大数据限制。全员到齐的随机报数。
    8. Test 8 (N=50000)【区分度杀手数据 1】。开始卡时间!纯随机报数,包含大量缺席。暴力法需要执行 2.5×1092.5 \times 10^9 次对比,极大概率 TLE。
    9. Test 9 (N=100000)【区分度杀手数据 2】。十万级别全员到齐打乱顺序。暴力法需要执行 101010^{10} 次对比,必定 TLE!
    10. Test 10 (N=100000)【终极杀手数据】。十万级别,但所有人全都报了同一个人的编号 N-1!这是暴力法双重循环的最坏情况(每次内层遍历都要一直走到最后才能判断没找到),专门惩罚没写 break 或算法低效的代码。

    数据生成器完整 C++ 代码 (generator.cpp)

    请将以下代码保存为 generator.cpp,本地编译运行即可生成完美的测试数据。

    #include <iostream>
    #include <fstream>
    #include <vector>
    #include <string>
    #include <random>
    #include <algorithm>
    #include <numeric>
    
    using namespace std;
    
    // 标程:极速 O(N+M) 哈希桶算法
    // 返回一个 vector,如果全员到达,返回一个特殊的 {-1} 标志
    vector<int> solve(int n, int m, const vector<int>& reports) {
        vector<bool> present(n, false);
        int count = 0;
        for (int x : reports) {
            if (!present[x]) {
                present[x] = true;
                count++;
            }
        }
        
        if (count == n) {
            return {-1}; // 代表全员到齐
        }
        
        vector<int> missing;
        for (int i = 0; i < n; ++i) {
            if (!present[i]) {
                missing.push_back(i);
            }
        }
        return missing;
    }
    
    int main() {
        // 优化系统 IO 速度
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
    
        mt19937 rng(42); // 固定随机种子以保证每次生成的数据一致
    
        vector<int> Ns = {3, 3, 2, 100, 500, 1000, 1000, 50000, 100000, 100000};
        vector<int> Ms = {3, 5, 2, 100, 500, 1000, 1000, 50000, 100000, 100000};
        vector<vector<int>> reports(10);
    
        // Test 1: 样例 1 (0 2 1)
        reports[0] = {0, 2, 1};
    
        // Test 2: 样例 2 (0 0 0 0 0)
        reports[1] = {0, 0, 0, 0, 0};
    
        // Test 3: 极小边界
        reports[2] = {1, 1};
    
        // Test 4: 极端两极分化
        reports[3].assign(100, 0);
        for(int i = 50; i < 100; ++i) reports[3][i] = 1;
    
        // Test 5: 所有人刚好一次,打乱顺序
        reports[4].resize(500);
        iota(reports[4].begin(), reports[4].end(), 0); // 填充 0~499
        shuffle(reports[4].begin(), reports[4].end(), rng);
    
        // Test 6: 原题边界 1000 随机
        for(int i = 0; i < 1000; ++i) reports[5].push_back(rng() % 1000);
    
        // Test 7: 原题边界 1000 全员到达打乱
        reports[6].resize(1000);
        iota(reports[6].begin(), reports[6].end(), 0);
        shuffle(reports[6].begin(), reports[6].end(), rng);
    
        // Test 8: TLE杀手 - 50000 随机
        for(int i = 0; i < 50000; ++i) reports[7].push_back(rng() % 50000);
    
        // Test 9: TLE杀手 - 100000 全员到达打乱
        reports[8].resize(100000);
        iota(reports[8].begin(), reports[8].end(), 0);
        shuffle(reports[8].begin(), reports[8].end(), rng);
    
        // Test 10: 终极恶心数据 - 100000 次全都报最后一个人的编号
        reports[9].assign(100000, 99999);
    
        // 开始生成文件
        for (int i = 0; i < 10; ++i) {
            int N = Ns[i];
            int M = Ms[i];
            
            // --- 1. 生成 .in 文件 ---
            string in_filename = to_string(i + 1) + ".in";
            ofstream fout_in(in_filename);
            if (!fout_in.is_open()) continue;
            
            fout_in << N << " " << M << "\n";
            for (int j = 0; j < M; ++j) {
                fout_in << reports[i][j] << (j == M - 1 ? "" : " ");
            }
            fout_in << "\n";
            fout_in.close();
    
            // --- 2. 调用 O(N+M) 标程计算正确答案 ---
            vector<int> ans = solve(N, M, reports[i]);
    
            // --- 3. 生成 .out 文件 ---
            string out_filename = to_string(i + 1) + ".out";
            ofstream fout_out(out_filename);
            if (!fout_out.is_open()) continue;
            
            if (ans.size() == 1 && ans[0] == -1) {
                fout_out << N << "\n"; // 全员到齐
            } else {
                for (int j = 0; j < ans.size(); ++j) {
                    fout_out << ans[j] << (j == ans.size() - 1 ? "" : " ");
                }
                fout_out << "\n";
            }
            fout_out.close();
    
            cout << "成功生成 Test " << (i + 1) << "\t | N = " << N << "\t | M = " << M << "\n";
        }
    
        cout << "\n所有测试数据生成完毕!无递归防爆栈,10个测试点总文件大小约 1.5 MB。" << endl;
        return 0;
    }
    

    OJ 部署与防坑指南

    1. 头文件保障:代码中包含了 <numeric>,这是用来调用 iota 函数的(它可以快速给数组填入 0, 1, 2... N-1),它是极度优雅且避免写大量 for 循环赋值的高效算法函数。配合 <algorithm> 里的 shuffle 使用,让数据生成器兼具高性能。
    2. 避免文件体积爆炸(符合你的要求):虽然 100000100000 听起来是个巨大的数字,但在存储空间上,包含 10 万个整数加上空格文本文件,实际大小只有约 550 KB。所有的 .in.out 加在一起大约在 1.5 MB1.5 \text{ MB} 上下,读取与评测速度在 OJ 系统中只需几毫秒。
    3. 彻底区分复杂度:把这套数据放到 OJ,设置 Time Limit 为 1000 ms(默认值)。你会惊喜地看到,使用数组直接映射(桶思想)的学生瞬间满分;而使用两层 for 循环遍历的学生,会在 Test 8, 9, 10 遭遇红色的 Time Limit Exceeded。你作为教练就能直接抓住这批同学,给他们讲授“哈希映射”的真谛了!

    信息

    ID
    13925
    时间
    1000ms
    内存
    128MiB
    难度
    3
    标签
    递交数
    1
    已通过
    1
    上传者