2 条题解
-
0
在构建 OJ(在线评测系统)的测试数据时,针对滑动窗口类题目,我们需要重点考察边界条件(如 刚好等于总和)、无解情况以及最大规模的随机数据。
以下是为你准备的数据生成器源码。它包含了标准逻辑并自动循环生成 10 组测试数据。
数据生成器 (C++14)
#include <iostream> #include <fstream> #include <vector> #include <string> #include <algorithm> #include <random> #include <chrono> #include <numeric> using namespace std; // --- 标程:用于根据输入生成 .out 文件 --- int getStandardOutput(int n, long long x, const vector<int>& nums) { long long totalSum = 0; for (int v : nums) totalSum += v; long long target = totalSum - x; if (target < 0) return -1; if (target == 0) return n; int maxLen = -1; long long currentSum = 0; int left = 0; for (int right = 0; right < n; ++right) { currentSum += nums[right]; while (left <= right && currentSum > target) { currentSum -= nums[left]; left++; } if (currentSum == target) { maxLen = max(maxLen, right - left + 1); } } return (maxLen == -1) ? -1 : (n - maxLen); } // --- 数据生成主逻辑 --- void make_data() { // 初始化高质量随机数引擎 mt19937_64 rng(chrono::steady_clock::now().time_since_epoch().count()); for (int t = 1; t <= 10; ++t) { int n; long long x; vector<int> nums; // --- 针对性构造不同类型的测试点 --- if (t == 1) { // 边界:最小规模,刚好达成 n = 1; nums = {5}; x = 5; } else if (t == 2) { // 边界:x 大于总和,必无解 n = 5; nums = {1, 2, 3, 4, 5}; x = 20; } else if (t == 3) { // 边界:x 刚好等于总和 n = 10; nums.resize(n, 100); x = 1000; } else if (t == 4) { // 特殊:中间一段非常大,导致 x 只能由两端极小值构成 n = 100; nums.resize(n, 1); nums[50] = 10000; // 中间混入大数 x = 10; // 目标值小 } else if (t == 5) { // 随机中等规模:构造一个确定的解 n = 1000; nums.resize(n); long long sum = 0; for(int i=0; i<n; ++i) { nums[i] = rng() % 1000 + 1; sum += nums[i]; } // 随机选取中间一段 [L, R] 留着,剩下的作为 x int L = rng() % 300, R = n - 1 - (rng() % 300); long long midSum = 0; for(int i=L; i<=R; ++i) midSum += nums[i]; x = sum - midSum; } else if (t >= 6 && t <= 7) { // 随机大规模:可能无解 n = 100000; nums.resize(n); for(int i=0; i<n; ++i) nums[i] = rng() % 10000 + 1; x = rng() % 1000000000 + 1; } else { // 最大规模:确保有解的构造 n = 100000; nums.resize(n); long long sum = 0; for(int i=0; i<n; ++i) { nums[i] = rng() % 1000 + 1; sum += nums[i]; } int L = rng() % 10000, R = n - 1 - (rng() % 10000); long long midSum = 0; for(int i=L; i<=R; ++i) midSum += nums[i]; x = sum - midSum; } // --- 写入 .in 文件 --- string in_name = to_string(t) + ".in"; ofstream fin(in_name); fin << n << " " << x << "\n"; for (int i = 0; i < n; ++i) { fin << nums[i] << (i == n - 1 ? "" : " "); } fin << "\n"; fin.close(); // --- 写入 .out 文件 --- string out_name = to_string(t) + ".out"; ofstream fout(out_name); fout << getStandardOutput(n, x, nums) << endl; fout.close(); cout << "Generated Case " << t << ": n=" << n << ", x=" << x << endl; } } int main() { make_data(); cout << "\nAll 10 test cases (1.in~10.out) generated successfully!" << endl; return 0; }
测试点规划说明 (OJ 建设参考)
测试点 特征描述 考查意图 1 1 5 最小边界 验证基础逻辑及单元素处理。 2 5 20 验证对无解情况()的快速判定。 3 10 1000 验证对全选情况()的处理。 4 100 10 存在极端大数干扰 考查滑动窗口在数值跨度大时的稳定性。 5 1000 随机 确保有解构造 验证 逻辑在普通规模下的正确性。 6-7 最大规模随机 压力测试,验证是否会因为使用哈希表或暴力而超时。 8-10 构造 最大规模确保有解 压力测试,考察指针移动的边界及 long long的溢出处理。
教练的生成器编写建议
- 构造有解数据的重要性:
单纯的随机数据很难产生“和恰好等于 ”的情况,会导致测试点全是
-1。通过先生成数组,再随机抠掉中间一段来反推 ,可以确保测试点具有极强的区分度。 - 数据类型的选择:
本题中 , ,总和会达到 。虽然
int勉强能存,但在生成器中使用mt19937_64和long long可以有效避免生成数据时的溢出风险。 - 非递归与性能: 本生成器逻辑完全基于迭代,生成 10 组 规模的数据在 1 秒内即可完成,不会出现爆栈或生成超时。
- 格式规范:
注意
(i == n - 1 ? "" : " ")的处理,这是保证输入数据不带多余行末空格的竞赛标准写法。
- 构造有解数据的重要性:
单纯的随机数据很难产生“和恰好等于 ”的情况,会导致测试点全是
-
0
这是“将 x 减到 0 的最小操作数”问题的标准题解。在 NOI 竞赛中,这道题的精髓在于逆向思维转换:将“寻找两端最小”转化为“寻找中间最长”。
一、 题解标准代码 (C++14)
#include <iostream> #include <vector> #include <algorithm> using namespace std; /** * 核心思路: * 设数组总和为 totalSum。 * 我们要在数组两端移除元素之和为 x,等价于在数组中间找到一段连续子数组, * 其和为 target = totalSum - x。 * 为了让移除的元素最少,我们需要让中间剩下的这段连续子数组长度“最长”。 */ int main() { // 竞赛优化:提升输入输出效率 ios::sync_with_stdio(false); cin.tie(0); int n; long long x; // x 范围可达 10^9,建议使用 long long if (!(cin >> n >> x)) return 0; vector<int> nums(n); long long totalSum = 0; for (int i = 0; i < n; ++i) { cin >> nums[i]; totalSum += nums[i]; // 累加总和 } // 目标和:中间连续部分应该达到的数值 long long target = totalSum - x; // 边界情况 1:如果总和小于 x,无论如何也减不到 0 if (target < 0) { cout << -1 << endl; return 0; } // 边界情况 2:如果总和恰好等于 x,说明必须移除所有元素 if (target == 0) { cout << n << endl; return 0; } int maxLen = -1; // 记录中间最长子数组的长度 long long currentSum = 0; int left = 0; // 滑动窗口左指针 // 滑动窗口遍历 for (int right = 0; right < n; ++right) { currentSum += nums[right]; // 右指针向右扩展,增加窗口和 // 关键点:当窗口和超过 target 时,左指针向右移动缩小窗口 // 因为所有元素都是正数,所以 currentSum 具有单调性 while (left <= right && currentSum > target) { currentSum -= nums[left]; left++; } // 判定:当前窗口和是否恰好等于目标值 if (currentSum == target) { maxLen = max(maxLen, right - left + 1); } } // 如果 maxLen 仍为 -1,说明没有找到和为 target 的连续子数组 if (maxLen == -1) { cout << -1 << endl; } else { // 结果 = 总长度 - 中间最长长度 cout << n - maxLen << endl; } return 0; }
二、 复杂度分析思考过程
1. 时间复杂度分析
- 输入与求和:遍历一次数组,。
- 滑动窗口过程:
- 右指针
right从 遍历到 。 - 左指针
left虽然在嵌套的while循环里,但它在整个程序运行过程中只会从左向右移动,不会回退。 - 这意味着数组中的每个元素最多被“加入窗口”一次,被“移出窗口”一次。
- 右指针
- 结论:总时间复杂度为 。
- 对于 ,运算量约为 次,远远低于 NOI 1秒限时(通常支持 次运算)。
2. 空间复杂度分析
- 数组存储:使用了
vector<int> nums存储 个整数,占用内存约为 KB。 - 辅助变量:仅使用了若干个
long long和int变量。 - 结论:总空间复杂度为 ,内存消耗极低。
三、 时间复杂度优化建议
本题 的线性解法已经是理论上的最优解,但在极端竞赛环境下,仍有细微的优化方向:
- 早期停止 (Early Exit):
- 在求和过程中,如果
totalSum < x,可以直接输出-1并退出。 - 如果
totalSum == x,直接输出n。
- 在求和过程中,如果
- 前缀和 + 哈希表 (替代方案):
- 如果数组中存在负数,滑动窗口将失效(因为窗口和不再具有单调性)。此时必须使用前缀和配合
std::unordered_map。 - 但在本题全是正数的前提下,滑动窗口比哈希表的常数更小,速度更快,且空间复杂度更优(哈希表会有额外的内存开销)。
- 如果数组中存在负数,滑动窗口将失效(因为窗口和不再具有单调性)。此时必须使用前缀和配合
- 读入优化 (Fast I/O):
- 在 NOI 赛场上,如果 达到 或更大,建议使用
getchar()实现的快读函数,这比cin优化后还要快。
- 在 NOI 赛场上,如果 达到 或更大,建议使用
四、 易错点总结(避坑指南)
- 逆向思维缺失:很多新手会尝试用暴力递归去模拟“切左边还是切右边”,导致 的复杂度而超时。
long long溢出:虽然单个元素 ,但 时的总和可达 。虽然int理论上能承载 ,但考虑到中间计算,使用long long存储totalSum和target是更稳健的竞赛习惯。target为 0 的判定:当 恰好等于数组总和时,target为 0。此时滑动窗口需要正确处理空窗口的情况,或者在循环前特判。- 无法满足要求:一定要初始化
maxLen = -1,并根据此标志位判断是否输出-1。如果不加判断,可能会输出 这种错误结果。
- 1
信息
- ID
- 19357
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者