2 条题解
-
0
你好!这是为你准备的完整数据生成器。这份代码集成了标准AC解法和10种针对性测试数据生成策略,能够自动生成符合OJ要求的
1.in/1.out到10.in/10.out文件。生成器特点
- 全自动流程:一键编译运行,自动生成所有输入输出文件。
- 覆盖全面:包含极小值边界、全重复、全不重复、负数、最大数据量、高频重复等多种场景。
- 标准格式:生成的输入输出完全符合NOIP竞赛及题目描述的格式要求。
- 高效稳健:使用 的核心算法生成答案,数据生成部分使用 排序,对于 的数据规模瞬间完成。
C++ 数据生成器代码
请保存为
generator.cpp,编译并运行。/** * 题目:删除有序数组中的重复项 II - 数据生成器 * 功能:生成 1.in/1.out ~ 10.in/10.out * 环境:C++14 Standard * 作者:OI Gold Medal Coach */ #include <iostream> #include <fstream> #include <vector> #include <algorithm> #include <random> #include <ctime> #include <string> using namespace std; // ========================================== // Part 1: 标准 AC 解法 (Solver) // ========================================== // 用于生成 .out 标准答案 class Solver { public: // 返回处理后的数组 vector<int> solve(int n, vector<int> nums) { if (n <= 2) return nums; int slow = 2; for (int fast = 2; fast < n; ++fast) { // 核心逻辑:允许保留的条件是与 slow-2 位置不同 if (nums[fast] != nums[slow - 2]) { nums[slow] = nums[fast]; slow++; } } // 截断数组,只保留有效部分 nums.resize(slow); return nums; } }; // ========================================== // Part 2: 数据生成工具箱 // ========================================== mt19937 rng(time(0)); // 生成指定长度、指定数值范围的随机有序数组 vector<int> gen_random_sorted(int n, int min_val, int max_val) { vector<int> res(n); uniform_int_distribution<int> dist(min_val, max_val); for(int i = 0; i < n; ++i) { res[i] = dist(rng); } // 题目要求输入必须是有序的 sort(res.begin(), res.end()); return res; } // 生成全重复数组 vector<int> gen_all_same(int n, int val) { return vector<int>(n, val); } // 生成全不重复数组 (连续序列) vector<int> gen_unique_seq(int n, int start_val) { vector<int> res(n); for(int i = 0; i < n; ++i) res[i] = start_val + i; return res; } // 生成特定模式:每个数重复 k 次 (e.g., 1,1,1, 2,2,2...) vector<int> gen_pattern_k_reps(int n, int k) { vector<int> res; int val = -1000; // 起始值 while (res.size() < n) { for (int j = 0; j < k && res.size() < n; ++j) { res.push_back(val); } val++; } return res; } // 生成高密度重复数据 (只有很少的 unique values) vector<int> gen_dense_reps(int n, int unique_count) { vector<int> res(n); uniform_int_distribution<int> dist(0, unique_count - 1); for(int i=0; i<n; ++i) { res[i] = dist(rng); // 数值范围很小,必然大量重复 } sort(res.begin(), res.end()); return res; } // ========================================== // Part 3: 主程序 // ========================================== int main() { Solver solver; cout << "Generating data..." << endl; for (int i = 1; i <= 10; ++i) { string in_file_name = to_string(i) + ".in"; string out_file_name = to_string(i) + ".out"; ofstream fin(in_file_name); ofstream fout(out_file_name); int N; vector<int> input_nums; string desc; // ---- 针对不同测试点设计数据 ---- switch(i) { case 1: // 极小边界:N=1 N = 1; input_nums = {10}; desc = "Min N=1"; break; case 2: // 极小边界:N=2 (无需删除) N = 2; input_nums = {10, 10}; desc = "Min N=2"; break; case 3: // 极小边界:N=3 (需要删除) N = 3; input_nums = {5, 5, 5}; desc = "Min N=3 All Same"; break; case 4: // 特例:全不重复 N = 100; input_nums = gen_unique_seq(N, -50); desc = "All Unique"; break; case 5: // 特例:全重复 (由于 N 较大,删除量大) N = 100; input_nums = gen_all_same(N, 7); desc = "All Same Large"; break; case 6: // 特例:每个数重复 3 次 (测试边界条件 >2) N = 1000; input_nums = gen_pattern_k_reps(N, 3); desc = "Pattern: 3 Reps each"; break; case 7: // 正常随机:含负数 N = 2000; input_nums = gen_random_sorted(N, -2000, 2000); desc = "Random with Negatives"; break; case 8: // 大数据:最大 N,稀疏重复 (值域大) N = 30000; input_nums = gen_random_sorted(N, -10000, 10000); desc = "Max N, Sparse"; break; case 9: // 大数据:最大 N,高频重复 (值域极小) // 只有 10 种不同的数,分布在 30000 个位置上 N = 30000; input_nums = gen_dense_reps(N, 10); desc = "Max N, Very Dense"; break; case 10: // 大数据:随机混合 N = 30000; input_nums = gen_random_sorted(N, -5000, 5000); desc = "Max N, General Random"; break; } // ---- 写入 .in 文件 ---- // 格式: // N // num1 num2 ... numN fin << N << endl; for (int k = 0; k < N; ++k) { fin << input_nums[k] << (k == N - 1 ? "" : " "); } fin << endl; // 保持文件末尾换行习惯 // ---- 计算并写入 .out 文件 ---- vector<int> result = solver.solve(N, input_nums); // 格式: // len // res1 res2 ... resLen fout << result.size() << endl; for (size_t k = 0; k < result.size(); ++k) { fout << result[k] << (k == result.size() - 1 ? "" : " "); } fout << endl; // ---- 关闭文件 ---- fin.close(); fout.close(); cout << "Generated Case " << i << ": " << desc << " [N=" << N << "]" << endl; } cout << "\nAll 10 test cases generated successfully!" << endl; return 0; }使用说明
- 运行环境:任何支持 C++11 及以上的编译器(g++, clang++, Visual Studio 等)。
- 生成结果:运行后当前目录下会出现
1.in~10.in和1.out~10.out。 - OJ配置建议:
- 时间限制:1000ms (C++ 对于 的处理是微秒级的,时间极度宽裕)。
- 内存限制:128MB。
- 判题模式:这是 Special Judge 类型的题目(因为通常 OJ 比较的是文本),但由于题目要求有序且稳定保留前两个,所以结果是唯一确定的。你可以直接使用标准的文本比对(Ignore trailing spaces/newlines)进行判题,不需要写额外的 SPJ。
测试点覆盖详情
- 测试点 1-3: 考察 的极小值处理,防止数组越界。
- 测试点 4: 全不重复,验证算法是否会错误删除。
- 测试点 5: 全重复,验证算法是否能正确压缩到只剩2个。
- 测试点 6: 固定重复3次,精准打击边界判断逻辑(
slow-2是否生效)。 - 测试点 7: 包含负数,考察数据类型处理。
- 测试点 8: 且值域分散,模拟一般情况。
- 测试点 9: 且值域极小(只有10种数),模拟极端稠密情况,考察频繁写入。
- 测试点 10: 综合随机,压测性能。
-
0
你好!我是你的OI竞赛金牌教练。
基于上一条我们改写的题目描述,下面是完全符合 NOIP/CSP 竞赛标准的 C++14 题解代码。代码中包含了详细的注释,解释了双指针的运作机制和易错点。
NOIP 标准题解代码 (C++14)
/** * 题目:删除有序数组中的重复项 II * 算法:双指针 (快慢指针) / 原地算法 * 时间复杂度:O(N) * 空间复杂度:O(1) * 风格:NOIP/CSP 竞赛标准 */ #include <iostream> #include <vector> #include <algorithm> using namespace std; // 为了应对可能的 IO 瓶颈,在 main 开头进行加速 void fast_io() { ios::sync_with_stdio(false); cin.tie(nullptr); } int main() { fast_io(); // 1. 读入数据 int n; if (!(cin >> n)) return 0; // 鲁棒性检查 // 使用 vector 存储,方便动态管理,但在逻辑上我们视为定长数组处理 vector<int> nums(n); for (int i = 0; i < n; ++i) { cin >> nums[i]; } // 2. 算法核心逻辑:双指针法 // 题目允许每个元素最多出现 2 次。 // 如果数组长度 <= 2,无论如何都不需要删除,直接输出原数组即可。 if (n <= 2) { cout << n << endl; for (int i = 0; i < n; ++i) { cout << nums[i] << (i == n - 1 ? "" : " "); } cout << endl; return 0; } // 定义慢指针 slow: // slow 指向的是“当前处理好的有效数组”的【下一个写入位置】。 // 因为前两个元素 (下标0, 1) 必然保留(最多允许2个重复),所以 slow 从 2 开始。 int slow = 2; // 定义快指针 fast: // fast 用于遍历数组,寻找有资格放入有效数组的新元素。 // fast 也从 2 开始遍历。 for (int fast = 2; fast < n; ++fast) { // 【关键逻辑 / 易错点】 // 我们要检查 nums[fast] 是否可以写入 nums[slow] 的位置。 // 允许保留的条件是:当前考察的元素 nums[fast] 不等于 有效区倒数第二个元素 nums[slow-2]。 // // 为什么是 slow-2? // 因为数组是有序的。如果 nums[fast] == nums[slow-2], // 那么意味着 nums[slow-2] == nums[slow-1] == nums[fast]。 // 这时 nums[fast] 就是该数值的第 3 个副本,属于多余元素,需要丢弃。 // 反之,如果不相等,说明 nums[fast] 是该数值的第 1 个或第 2 个副本,可以保留。 if (nums[fast] != nums[slow - 2]) { nums[slow] = nums[fast]; // 将符合条件的元素“搬运”过来 slow++; // 有效区长度 +1,指针后移 } } // 3. 输出结果 // 此时 slow 的值即为处理后的新长度 cout << slow << endl; for (int i = 0; i < slow; ++i) { // 题目要求输出前 len 个元素,以空格分隔 cout << nums[i] << (i == slow - 1 ? "" : " "); } cout << endl; return 0; }
复杂度分析与思考过程
1. 时间复杂度分析
- 思考过程:
- 我们使用了一个
for循环,快指针fast从下标 2 遍历到 。 - 在循环内部,只进行了简单的比较
if和赋值操作nums[slow] = nums[fast],这些都是 的常数时间操作。 - 每个元素最多被访问和搬运一次。
- 我们使用了一个
- 结论:总时间复杂度为 ,其中 是数组的长度。
- 优化建议:对于本题, 已经是理论下界(必须遍历一次数组才能知道哪些要删),无法在算法层级进一步降低量级。唯一的优化是 IO 优化(如代码中的
ios::sync_with_stdio(false)),防止输入输出数据量巨大时 TLE。
2. 空间复杂度分析
- 思考过程:
- 题目明确要求 原地 (In-place) 修改,空间复杂度限制为 。
- 我们定义了两个整数变量
slow和fast,以及少量的辅助变量。 - 我们直接在原数组
nums上进行覆盖操作,没有开辟新的数组(比如vector<int> result)。 - 注意:输入存储本身的 空间通常不计入算法的额外空间复杂度。
- 结论:额外空间复杂度为 。
核心逻辑图解 (草稿纸推演)
假设输入:
1 1 1 2 2 3-
初始状态:
slow = 2(指向第3个位置)- 前两个
1 1默认保留。 - 数组现状:
[1, 1, 1, 2, 2, 3]
-
Fast = 2, Value = 1:
- 比较:
nums[fast] (1)vsnums[slow-2] (即nums[0]=1)。 1 == 1,相等!说明这是第3个1。跳过。slow保持 2。
- 比较:
-
Fast = 3, Value = 2:
- 比较:
nums[fast] (2)vsnums[slow-2] (即nums[0]=1)。 2 != 1,不等!合法。- 搬运:
nums[slow] = 2。数组变为[1, 1, 2, 2, 2, 3]。 slow变为 3。
- 比较:
-
Fast = 4, Value = 2:
- 比较:
nums[fast] (2)vsnums[slow-2] (即nums[1]=1)。 2 != 1,不等!合法。- 搬运:
nums[slow] = 2。数组变为[1, 1, 2, 2, 2, 3](看似没变,其实覆盖了)。 slow变为 4。
- 比较:
-
Fast = 5, Value = 3:
- 比较:
nums[fast] (3)vsnums[slow-2] (即nums[2]=2)。 3 != 2,不等!合法。- 搬运:
nums[slow] = 3。数组变为[1, 1, 2, 2, 3, 3]。 slow变为 5。
- 比较:
-
结束:
- 返回
slow = 5。 - 输出前5个:
1 1 2 2 3。
- 返回
总结关键点 (Coach's Note)
- 通用扩展性:这道题的逻辑其实是通用的。如果题目改成“允许最多出现 次”,你只需要把代码里的
slow - 2改成slow - K,把初始的slow = 2改成slow = K即可。这是面试和竞赛中常考的变种。 - 覆盖而非交换:注意我们做的是
nums[slow] = nums[fast](覆盖),而不是 swap。因为被覆盖掉的那个旧垃圾值我们并不需要保留。 - 边界保护:代码中
if (n <= 2)的判断虽然删掉也能跑通(如果逻辑写得足够好),但在竞赛中显式处理边界情况能避免很多越界错误,也是良好的编程习惯。
- 思考过程:
- 1
信息
- ID
- 19349
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者