2 条题解
-
0
你好!这是为你准备的 NOI 风格数据生成器。这个程序会一次性生成
1.in/1.out到10.in/10.out共 20 个文件。数据生成策略说明
为了确保测试数据的完备性,我设计了如下 10 个测试点,覆盖了题目要求的 小数据和 大数据,以及各种边界情况:
- 测试点 1 (Sample): 极小数据,类似题目样例,用于基础验证。
- 测试点 2 (No Zeros): ,完全没有 0,测试不移动的情况。
- 测试点 3 (All Zeros): ,全是 0,测试全 0 逻辑。
- 测试点 4 (Front Zeros): ,前半段全是 0,后半段非 0。
- 测试点 5 (Back Zeros): ,前半段非 0,后半段全是 0。
- 测试点 6 (Sparse Zeros): (满数据),0 很少(约 5%),测试大量非 0 元素的移动。
- 测试点 7 (Dense Zeros): (满数据),0 很多(约 90%),测试大量 0 的堆积。
- 测试点 8 (Random): ,完全随机,0 和非 0 各占一半,数值覆盖 int32 负数到正数范围。
- 测试点 9 (Alternating): ,0 和非 0 交替出现 (0, x, 0, x...),测试频繁交换。
- 测试点 10 (Worst/Edge): ,只有一个非 0 元素在最后,或者只有一个 0 在最前,极端边界测试。
数据生成器代码 (C++14)
请将以下代码保存为
generator.cpp,编译运行即可在当前目录下生成所有测试数据文件。/** * Title: Move Zeroes Data Generator * Standard: C++14 * Author: OI Coach * Description: Generates 1.in/out to 10.in/out for OJ testing. */ #include <iostream> #include <vector> #include <string> #include <fstream> #include <algorithm> #include <random> #include <chrono> using namespace std; // ========================================== // 1. 标准题解算法 (用于生成 .out 文件) // ========================================== vector<int> solve(vector<int> nums) { int n = nums.size(); int j = 0; for (int i = 0; i < n; ++i) { if (nums[i] != 0) { swap(nums[i], nums[j]); j++; } } return nums; } // ========================================== // 2. 随机数生成工具 // ========================================== mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); // 生成 [min, max] 范围内的整数 int rand_int(int min, int max) { uniform_int_distribution<int> dist(min, max); return dist(rng); } // ========================================== // 3. 文件写入辅助函数 // ========================================== void write_case(int id, const vector<int>& input_data) { string in_name = to_string(id) + ".in"; string out_name = to_string(id) + ".out"; // 写入 .in 文件 ofstream fin(in_name); fin << input_data.size() << "\n"; for (size_t i = 0; i < input_data.size(); ++i) { fin << input_data[i] << (i == input_data.size() - 1 ? "" : " "); } fin << "\n"; fin.close(); // 计算答案并写入 .out 文件 vector<int> output_data = solve(input_data); ofstream fout(out_name); for (size_t i = 0; i < output_data.size(); ++i) { fout << output_data[i] << (i == output_data.size() - 1 ? "" : " "); } fout << "\n"; fout.close(); cout << "Generated Case " << id << ": N = " << input_data.size() << endl; } // ========================================== // 4. 数据生成主逻辑 // ========================================== int main() { // 题目数据范围常量 const int MIN_VAL = -2147483648; // INT_MIN const int MAX_VAL = 2147483647; // INT_MAX // 为了防止随机生成时溢出或方便控制,我们取个稍微安全点的范围, // 或者直接按题目要求生成全范围。这里为了展示方便,取稍小一点的常用范围, // 但特定测试点会覆盖极端值。 for (int i = 1; i <= 10; ++i) { vector<int> nums; int n; switch(i) { case 1: // 样例规模:极小 n = 10; for(int k=0; k<n; ++k) nums.push_back(k % 3 == 0 ? 0 : k); break; case 2: // 无 0 测试 n = 100; for(int k=0; k<n; ++k) nums.push_back(rand_int(1, 100)); break; case 3: // 全 0 测试 n = 100; for(int k=0; k<n; ++k) nums.push_back(0); break; case 4: // 前半部分是 0 n = 1000; for(int k=0; k<n; ++k) { if (k < n / 2) nums.push_back(0); else nums.push_back(rand_int(1, 1000)); } break; case 5: // 后半部分是 0 n = 1000; for(int k=0; k<n; ++k) { if (k >= n / 2) nums.push_back(0); else nums.push_back(rand_int(1, 1000)); } break; case 6: // 大数据,稀疏 0 (5% 是 0) n = 10000; for(int k=0; k<n; ++k) { if (rand_int(1, 100) <= 5) nums.push_back(0); else nums.push_back(rand_int(-10000, 10000)); } break; case 7: // 大数据,稠密 0 (90% 是 0) n = 10000; for(int k=0; k<n; ++k) { if (rand_int(1, 100) <= 90) nums.push_back(0); else nums.push_back(rand_int(-10000, 10000)); } break; case 8: // 大数据,完全随机,覆盖 INT 极值 n = 10000; for(int k=0; k<n; ++k) { // 50% 概率是 0 if (rand_int(0, 1) == 0) nums.push_back(0); // 50% 概率是非 0,且包含负数和极大值 else nums.push_back(rand_int(MIN_VAL / 2, MAX_VAL / 2)); } break; case 9: // 大数据,0 和 非0 交替 n = 10000; for(int k=0; k<n; ++k) { if (k % 2 == 0) nums.push_back(0); else nums.push_back(k); // 非0 } break; case 10: // 极端情况:只有一个非0在最后 n = 10000; for(int k=0; k<n-1; ++k) nums.push_back(0); nums.push_back(999); break; } // 调用函数生成文件 write_case(i, nums); } cout << "All test cases generated successfully!" << endl; return 0; }如何使用
- 确保你安装了 G++ 编译器。
- 保存上述代码为
gen.cpp。 - 在终端编译:
g++ gen.cpp -o gen -O2 -std=c++14。 - 运行程序:
./gen(Windows 下是gen.exe)。 - 你的目录下将会出现
1.in,1.out...10.in,10.out。
优化细节说明
- 非递归写法:所有逻辑均在循环中完成,避免了递归带来的栈溢出风险,虽然本题 并不大,但这是良好的工程习惯。
- 时间复杂度:
- 生成逻辑是线性的 。
- 解题逻辑(双指针)也是线性的 。
- 总时间复杂度为 ,生成速度极快(毫秒级)。
- 随机性:使用了
std::mt19937(Mersenne Twister),比传统的rand()随机性更好,分布更均匀。 - IO 效率:使用了
ofstream,配合 C++ 的流操作,对于 规模的数据完全足够。如果是 级别,可能需要换回fprintf,但此处为了代码可读性使用了流。
-
0
你好!我是你的OI金牌教练。
针对 LeetCode 283《移动零》改编的 NOI 风格题目《零的迁移》,下面给出标准的题解代码。这段代码完全符合 NOIP/CSP-J/S 的考场规范(C++14 标准),并附带了详细的注释和复杂度分析,帮助你深入理解。
NOIP 标准题解代码 (C++14)
/** * 题目: 零的迁移 (Move Zeroes) * 来源: 改编自 LeetCode 283 * 语言: C++14 * 算法: 双指针 (Two Pointers) / 快慢指针 */ #include <iostream> #include <vector> #include <algorithm> // 用于 std::swap using namespace std; // 核心解题函数 void solve() { int n; // 1. 输入数据规模 if (!(cin >> n)) return; // 根据数据范围 1 <= n <= 10^4,使用 vector 或全局数组皆可 // vector 在函数内定义是分配在堆上的(主要空间),栈溢出风险较小 vector<int> nums(n); // 2. 输入数组元素 for (int i = 0; i < n; ++i) { cin >> nums[i]; } /* --- 核心算法:双指针法 --- slow (慢指针): 始终指向“当前处理好的非零序列”的尾部下一个位置(即潜在的放置位)。 fast (快指针): 也就是循环变量 i,负责向后扫描寻找非零元素。 */ int slow = 0; for (int fast = 0; fast < n; ++fast) { // 如果当前快指针指向的数不是 0 if (nums[fast] != 0) { // 【关键点】:将非零数交换到慢指针的位置 // 易错点提示:必须使用 swap 而不是简单的赋值覆盖,否则会丢失原来的数据 // 除非你打算最后统一补 0(那是另一种写法) // 优化细节:如果 slow == fast,说明中间还没有出现过 0, // 此时交换是自己换自己,虽然结果正确但多余。 // 加上 if (slow != fast) 判断可以微量优化,但在竞赛中通常为了代码简洁可省略。 swap(nums[slow], nums[fast]); // 慢指针向前移动一步,准备接收下一个非零数 slow++; } // 如果 nums[fast] 是 0,则不做任何操作,快指针继续向前, // 此时 slow 停在原地(指向这个 0),等待被后续的非零数置换。 } // 3. 按照格式输出结果 for (int i = 0; i < n; ++i) { cout << nums[i] << (i == n - 1 ? "" : " "); } cout << endl; } int main() { // 【竞赛必备】:关闭同步流,加速 cin/cout // 避免在数据量达到 10^5 或 10^6 时出现 IO TLE ios::sync_with_stdio(false); cin.tie(nullptr); solve(); return 0; }
复杂度分析与思维过程
1. 时间复杂度分析
- 思考过程:
代码中只有一个主要的
for循环,循环变量fast从 走到 。 在循环内部,我们执行的操作是if判断、swap交换(涉及 3 次赋值)和slow++。这些都是 的常数级操作。 并没有嵌套循环,也没有递归。 - 结论: 时间复杂度为 ,其中 为数组长度。 这对于本题 的数据范围来说绰绰有余(通常 算法可在 1秒内处理 级别的数据)。
2. 空间复杂度分析
- 思考过程:
题目要求“原地操作” (In-place)。
我们只定义了两个整型变量
slow和fast(以及循环内的临时变量),没有开辟与 成正比的辅助数组(输入数组nums本身不算在额外空间内)。 - 结论: 空间复杂度为 。
深度教学:时间复杂度优化建议
虽然上述代码已经是 的最优时间复杂度,但在操作细节上,我们还可以从“操作次数”的角度进行微观优化。这在 OI 竞赛中处理卡常题(Time Limit 非常紧的题)时很有用。
优化方案:避免不必要的交换 (Assignment vs Swap)
当前 Swap 方案的特点: 每次遇到非零数都要执行
swap。swap包含了 3 次内存写入(Read A -> Tmp, Read B -> A, Read Tmp -> B)。优化思路(赋值覆盖法): 我们可以直接把非零数覆盖到前面,最后统一把剩下的位置填 0。
// 优化后的核心逻辑 int slow = 0; for (int fast = 0; fast < n; ++fast) { if (nums[fast] != 0) { // 直接赋值,减少内存读写次数 // 如果 slow != fast,说明我们实际上是在把后面的非零数移到前面 // 如果 slow == fast,这是自己赋值给自己(无害) nums[slow] = nums[fast]; slow++; } } // 循环结束后,slow 之后的所有位置都应该是 0 for (int i = slow; i < n; ++i) { nums[i] = 0; }对比分析:
- Swap版:代码极简,逻辑闭环,能保留
0之间的相对顺序(虽然题目没要求)。 - 赋值版:
- 写入操作总数通常更少(非零数只写一次,最后的 0 只写一次)。
- 在非零元素非常多(例如数组全是 1)的情况下,
nums[slow] = nums[fast]会变成大量的自我赋值。可以加if (slow != fast)来避免。 - 在非零元素非常少(例如数组全是 0)的情况下,赋值版不需要做任何写操作(除了最后填0),而 Swap 版也不会做操作。
教练总结: 在 NOIP/CSP 考场上,首选 Swap 版本或带判断的 Swap 版本,因为代码逻辑最不容易写错,且 的量级决定了这点常数差异不会导致 TLE。只有在极度追求运行效率的工程代码中,才会考虑赋值优化的细微差异。
- 思考过程:
代码中只有一个主要的
- 1
信息
- ID
- 19334
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者