2 条题解
-
0
在 NOI 竞赛中,构造高质量的测试数据需要包含极端边界测试、满额规模测试、逻辑陷阱测试(如 )。
以下是为你编写的数据生成器。它包含了标准 解法作为标程,并自动循环生成 10 组测试点。
数据生成器 (C++14)
#include <iostream> #include <fstream> #include <vector> #include <string> #include <random> #include <chrono> #include <algorithm> using namespace std; // --- 标程逻辑:用于生成 .out 答案文件 --- long long getStandardOutput(int n, long long k, const vector<int>& nums) { if (k <= 1) return 0; long long ans = 0, prod = 1; int l = 0; for (int r = 0; r < n; ++r) { prod *= nums[r]; while (prod >= k && l <= r) { prod /= nums[l]; l++; } ans += (r - l + 1); } return ans; } // --- 数据生成主逻辑 --- void make_data() { // 高质量随机数引擎 mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); for (int t = 1; t <= 10; ++t) { int n; long long k; vector<int> nums; // --- 针对性构造不同类型的测试点 --- if (t == 1) { // 边界:k 为 0,结果必为 0 n = 10; k = 0; for(int i=0; i<n; ++i) nums.push_back(rng() % 100 + 1); } else if (t == 2) { // 边界:n 为 1,k 恰好等于元素值 n = 1; k = 100; nums.push_back(100); // 应该输出 0,因为要求严格小于 } else if (t == 3) { // 边界:所有元素都是 1,k 为 2 n = 1000; k = 2; nums.assign(n, 1); // 结果应为 n*(n+1)/2 } else if (t == 4) { // 特殊:所有元素都大于 k n = 100; k = 10; for(int i=0; i<n; ++i) nums.push_back(rng() % 50 + 11); } else if (t == 5) { // 特殊:交替出现大数和小数 n = 500; k = 1000; for(int i=0; i<n; ++i) { if (i % 2 == 0) nums.push_back(1); else nums.push_back(2000); } } else if (t == 6) { // 中等规模随机 n = 5000; k = 50000; for(int i=0; i<n; ++i) nums.push_back(rng() % 1000 + 1); } else if (t >= 7 && t <= 8) { // 满额规模随机 n = 30000; k = 1000000; for(int i=0; i<n; ++i) nums.push_back(rng() % 1000 + 1); } else if (t == 9) { // 满额规模:全为 1,且 k 为最大 n = 30000; k = 1000000; nums.assign(n, 1); } else { // 满额规模:构造大量符合条件的长区间 n = 30000; k = 1000000; for(int i=0; i<n; ++i) nums.push_back(rng() % 5 + 1); } // --- 写入 .in 文件 --- string in_name = to_string(t) + ".in"; ofstream fin(in_name); fin << n << " " << k << "\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, k, nums) << endl; fout.close(); cout << "Generated Case " << t << ": n=" << n << ", k=" << k << endl; } } int main() { make_data(); cout << "\nAll 10 test cases (1.in~10.out) generated successfully!" << endl; return 0; }
测试点规划说明 (OJ 建设参考)
测试点序号 规模 的特征 特征描述 考查意图 1 10 0 边界情况:严格小于 0 是不可能的。 2 1 100 边界情况:验证“严格小于”不含等于。 3 1000 2 全为 1 极端情况:子数组数量达到最大值。 4 100 10 考查算法在所有单元素都不成立时的表现。 5 500 1000 巨数间隔 考查滑动窗口频繁开启和关闭。 6 5000 50000 随机分布 中等规模逻辑验证。 7-8 30000 1,000,000 满额随机 考察 时间复杂度的稳定性。 9 最大全 1 考察答案变量是否使用了 long long防止溢出。10 密集分布 考察窗口长时间不收缩的情况。
生成器设计要点总结
- 答案溢出预防:
当 且子数组全符合条件时,答案约为 。虽然
int能存下,但如果 扩大到 ,则必须使用long long。本生成器标程已统一使用long long。 - 乘法溢出预防:
在
prod *= nums[r]这一步,由于prod之前一定小于 (),且nums[r]最大 ,乘积最大为 ,int依然能存下,但标程使用了long long以应对未来数据范围扩大的风险。 - 非递归实现:
生成器与标程均采用简单的
for和while循环,时间复杂度 ,空间复杂度 ,生成 10 组数据总耗时小于 0.1 秒,绝无爆栈风险。 - 符合 NOI 规范: 输出格式严格遵循“行末无空格,文末有换行”的竞赛要求,方便直接在各类 OJ 系统上配置。
- 答案溢出预防:
当 且子数组全符合条件时,答案约为 。虽然
-
0
这是“乘积小于 K 的子数组”问题的标准题解。在 NOI 竞赛中,这道题是考察双指针(滑动窗口)和计数原理的入门级好题。
一、 题解标准代码 (C++14)
#include <iostream> #include <vector> using namespace std; /** * 核心思路: * 由于数组元素均为正整数,区间乘积随区间长度增加而具有单调性(不减)。 * 我们维护一个滑动窗口 [l, r],确保窗口内所有元素的乘积 prod < k。 */ int main() { // 竞赛优化:加速输入输出 ios::sync_with_stdio(false); cin.tie(0); int n; long long k; // k 的范围虽然在 10^6,但为了防止乘积计算溢出,建议使用 long long if (!(cin >> n >> k)) return 0; vector<int> nums(n); for (int i = 0; i < n; ++i) { cin >> nums[i]; } // 特判:由于 nums[i] >= 1,如果 k <= 1,没有任何子数组乘积严格小于 1 // 如果不加这个特判,下面的 while 循环逻辑可能会导致 l 超过 r 甚至越界 if (k <= 1) { cout << 0 << endl; return 0; } long long ans = 0; // 最终子数组计数 long long prod = 1; // 当前窗口内的乘积 int l = 0; // 左指针 // 右指针从左向右移动 for (int r = 0; r < n; ++r) { prod *= nums[r]; // 将右边界元素计入乘积 // 易错点:当窗口积 >= k 时,必须缩小左边界 // 这里的循环条件 prod >= k 保证了窗口缩小的正确性 while (prod >= k && l <= r) { prod /= nums[l]; l++; } /** * 关键计数逻辑: * 当窗口 [l, r] 满足 prod < k 时,所有以 r 为结尾的子数组: * [nums[r]], [nums[r-1], nums[r]], ..., [nums[l], ..., nums[r]] * 都必然满足乘积 < k。 * 这些子数组的数量恰好是 r - l + 1。 */ ans += (r - l + 1); } cout << ans << endl; return 0; }
二、 复杂度分析思考过程
1. 时间复杂度分析
- 指针移动:右指针
r从 遍历到 ,共移动 次。 - 窗口收缩:左指针
l在整个for循环的过程中,只可能从左向右移动,绝不回退。因此l也最多移动 次。 - 计算开销:每次移动只涉及一次乘法或一次除法,都是 的。
- 结论:总时间复杂度为 。
- 在 的规模下,程序运行时间远小于 1 毫秒,表现极其优异。
2. 空间复杂度分析
- 存储开销:使用
vector<int>存储 个整数,占用空间为 。 - 额外空间:只使用了
l, r, ans, prod等若干个变量,占用空间为 。 - 结论:总空间复杂度为 ,在 NOI 竞赛 128MB 的内存限制下绰绰有余。
三、 算法优化与进阶建议
-
关于溢出的深度思考:
- 在本题中,,且每次
prod在乘以nums[r]之前必然是 的。 - 最大的可能中间结果是
(k-1) * nums[r],即 。这个数值没有超过int的上限(约 ),但为了代码的鲁棒性(Robustness),在 NOI 赛场上处理乘法建议首选long long。
- 在本题中,,且每次
-
对数转换法(进阶技巧):
- 如果题目给定的 非常大,或者数组元素很大,导致连
long long也会乘积溢出怎么办? - 优化方案:利用对数性质 。
- 将
nums[i] < k转换为 。 - 由于 后的数值是增加的,问题变成了“和小于某个定值”,可以使用前缀和 + 二分查找在 时间内解决,且完全避免了溢出风险。
- 如果题目给定的 非常大,或者数组元素很大,导致连
-
读题关键词回顾:
- “严格小于”:意味着
prod == k时必须收缩左边界。 - “正整数”:这是滑动窗口成立的基石。如果存在 ,乘积会突变为 ;如果存在负数,区间乘积不再具有单调性。
- “严格小于”:意味着
教练点评:
此题最精妙之处在于
ans += (r - l + 1)这一行。很多同学会尝试枚举所有区间,那将导致 的复杂度。学会 “固定右端点,看左端点能向左延伸多远” 的计数技巧,是掌握线性模拟算法的关键。加油! - 指针移动:右指针
- 1
信息
- ID
- 19358
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者