2 条题解
-
0
你好!很高兴看到你继续亲自动手制作测试数据。
针对题目二:最大子数组乘积(Maximum Subarray Product),出数据的最大难点在于**“防溢出(Overflow Prevention)”**。 既然题目保证“答案在
long long范围内”,那么作为出题人,我们就不能随便生成 个 的随机数——因为只要有几十个2连乘,就会瞬间撑爆 ,不仅参赛者的程序会溢出,连你生成的标准答案.out也会是错的!为了保证生成的测试点科学、严谨且合法,我为你设计了防溢出熔断机制(Overflow Breaker):在生成随机数时,动态侦测当前连续非零子段的绝对值乘积,一旦发现濒临
long long的极限(),就强制插入0斩断连乘,或者退化为插入1和-1。测试点梯度与覆盖策略设计
测试点 的规模 数据特征与出题目的 预期拦截的错误做法 1 全随机混合:极小数据,用于人工验证。 无 2 全负数无零:专门测试初值为0导致全错的漏洞。 初始值设为 0的代码3 全正数:测试基础连乘。 双状态更新有 Bug 的代码 4 正负交替:测试乘积的频繁符号反转。 仅维护 max而丢弃min的代码5 包含大量0:验证遇到 0时的状态切断与重置逻辑。遇到 0处理出错的代码6 全 -1 与 1 混合:绝对值不增加,但符号疯狂翻转。 错把绝对值当答案的代码 7 清一色的 -1:10万个 -1,考验 效率与奇偶校验。算法 (TLE) 8 零星大数+海量零:大量 0中点缀几个极值,卡打擂台。常数大的 算法 (TLE) 9 极端防溢出测试:长条小正数后突袭一个负数,再反转。 状态变量未用 temp隔离的代码10 终极规模随机:利用防溢出机制生成的 极限数据。 任何非 或变量污染的代码 (注:数据总体积完美控制在 1.5MB 左右,非常轻量)
C++ 数据生成器代码 (
data_maker_prod.cpp)请新建一个文件夹,将以下代码保存并编译运行。
#include <iostream> #include <fstream> #include <vector> #include <string> #include <random> #include <algorithm> #include <cmath> using namespace std; // 枚举数据生成的特殊模式 enum DataType { RANDOM_MIXED = 1, // 全随机混合 ALL_NEGATIVE, // 全负数 ALL_POSITIVE, // 全正数 ALTERNATING, // 正负交替 WITH_ZEROS, // 包含大量0 ONLY_ONES, // 只有 1 和 -1 ALL_MINUS_ONE, // 清一色 -1 SPARSE_VALUES // 大量0,夹杂极少数有效值 }; // 使用 mt19937 作为高质量随机数生成器,固定种子保证可复现 mt19937 rng(1024); // 生成指定范围内的随机整数 [L, R] int get_rand(int L, int R) { uniform_int_distribution<int> dist(L, R); return dist(rng); } // 核心求解函数 (标准答案,时间 O(N) 空间 O(1)) long long solve_correct_answer(const vector<int>& a) { if (a.empty()) return 0; long long curr_max = a[0]; long long curr_min = a[0]; long long global_max = a[0]; for (size_t i = 1; i < a.size(); ++i) { long long val = a[i]; // 【关键防御】使用 temp 隔离 long long temp_max = curr_max; curr_max = max({val, temp_max * val, curr_min * val}); curr_min = min({val, temp_max * val, curr_min * val}); global_max = max(global_max, curr_max); } return global_max; } // 带有防溢出熔断机制的数组生成器 vector<int> generate_safe_array(int n, DataType type) { vector<int> a(n); long long current_abs_prod = 1; const long long LIMIT = 2e18; // 设定溢出警戒线 (long long 极值约 9e18) for (int i = 0; i < n; ++i) { int v; // 1. 按照模式初步生成数字 switch (type) { case ALL_NEGATIVE: v = get_rand(-5, -1); break; case ALL_POSITIVE: v = get_rand(1, 5); break; case ALTERNATING: v = get_rand(1, 10); if (i % 2) v = -v; break; case ONLY_ONES: v = get_rand(0, 1) ? 1 : -1; break; case ALL_MINUS_ONE: v = -1; break; case SPARSE_VALUES: v = (get_rand(1, 20) == 1) ? get_rand(-10, 10) : 0; break; default: v = get_rand(-10, 10); break; } // 2. 防溢出熔断校验(核心黑科技) // 如果加入这个数会导致连乘绝对值越界,立刻篡改为 0 或 ±1 打断魔法! if (v != 0 && abs(v) > 1) { if (LIMIT / abs(v) < current_abs_prod) { // 触发熔断:强制改为 0 (50%概率) 或 1/-1 (50%概率) if (get_rand(0, 1) == 0) v = 0; else v = get_rand(0, 1) ? 1 : -1; } } // 3. 特殊模式微调:增加 0 的密度 if (type == WITH_ZEROS && get_rand(1, 10) <= 2) { v = 0; } // 4. 更新当前连续子段的绝对值乘积 if (v == 0) { current_abs_prod = 1; } else { current_abs_prod *= abs(v); } a[i] = v; } return a; } // 生成单个测试点文件的全套流程 void generate_test_case(int test_id, int n, DataType type) { // 生成合法数据 vector<int> a = generate_safe_array(n, type); // --- 写入 .in 文件 --- string in_filename = to_string(test_id) + ".in"; ofstream fin(in_filename); if (!fin) { cerr << "无法创建文件: " << in_filename << "\n"; return; } fin << n << "\n"; for (int i = 0; i < n; ++i) { fin << a[i] << (i == n - 1 ? "" : " "); } fin << "\n"; fin.close(); // --- 计算并写入 .out 文件 --- long long ans = solve_correct_answer(a); string out_filename = to_string(test_id) + ".out"; ofstream fout(out_filename); if (!fout) { cerr << "无法创建文件: " << out_filename << "\n"; return; } fout << ans << "\n"; fout.close(); cout << "Test Case " << test_id << " 成功生成. (N = " << n << ", 答案 = " << ans << ")\n"; } int main() { // 提升流读写性能 ios::sync_with_stdio(false); cin.tie(nullptr); cout << "开始生成《最大子数组乘积》测试数据...\n"; // 梯度 1:基础小型数据 generate_test_case(1, 10, RANDOM_MIXED); generate_test_case(2, 20, ALL_NEGATIVE); generate_test_case(3, 50, ALL_POSITIVE); // 梯度 2:中型数据,卡 O(n^3) 暴力 generate_test_case(4, 500, ALTERNATING); generate_test_case(5, 5000, WITH_ZEROS); // 梯度 3:大型边界数据,针对性卡错解 generate_test_case(6, 10000, ONLY_ONES); generate_test_case(7, 100000, ALL_MINUS_ONE); generate_test_case(8, 100000, SPARSE_VALUES); // 梯度 4:终极防爆测试,检验 $O(N)$ 标准解法的变量隔离逻辑 generate_test_case(9, 100000, RANDOM_MIXED); generate_test_case(10, 100000, RANDOM_MIXED); cout << "全部10组测试点生成完毕!\n"; return 0; }👨🏫 教练的工程心得(出题人视角):
- 除以零异常(Divide by Zero):在这份生成器代码的
LIMIT / abs(v)中,你可能会担心abs(v)等于0导致崩溃。请注意我在if条件里严格加了v != 0 && abs(v) > 1的前置短路判断,完美规避了 Exception 崩溃的问题。这也是出数据时必须养成的肌肉记忆! - 极小化文件策略:即便数组长度高达
100,000,由于数值均在[-10, 10]范围内,单个数值大多只占 1~2 个字符。因此最大的.in文件也不会超过 300KB。不仅符合2MB的限制,上传到各路OJ都会极速秒传。 - 检验生成质量:控制台在生成文件时,同时输出了该测试点的答案(
ans)。你运行后可以重点观察,答案是否严格控制在了9e18以下。只有作为出题人对数据边界心如明镜,你才能彻底掌握这套算法。
祝你的OJ建题顺利!接下来需要讲解或者出剩下几道题的数据随时找我。
- 除以零异常(Divide by Zero):在这份生成器代码的
-
0
你好!继续我们的打怪升级之旅。针对题目二:最大子数组乘积(Maximum Subarray Product),这道题相比求和有一个巨大的陷阱:负负得正。
为了让你在赛场上思路清晰,我们依然采用阶梯式拆解,从最暴力的 一路优化到终极形态的 空间 。请在脑海中牢记乘法的特性,我们开始!
版本一:朴素暴力枚举(时间 ,空间 )
【思考过程】 最无脑的暴力法,枚举子数组的左边界
L和右边界R,然后再套一层循环把L到R里的所有数乘起来,打擂台取最大值。 【竞赛评价】 这是保底写法。当毫无思路或只剩5分钟时,写下它能稳拿前两三个测试点()的 20~30 分。由于计算量在 时达到 级别,毫无疑问会 TLE。#include <iostream> #include <vector> #include <algorithm> using namespace std; // 使用极小的长整型值初始化,防止全是负数的情况 const long long INF = 1e18; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<long long> a(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i]; } long long global_max = -INF; // 枚举左端点 L for (int L = 1; L <= n; ++L) { // 枚举右端点 R for (int R = L; R <= n; ++R) { long long current_prod = 1; // 每次都从 L 重新连乘到 R for (int k = L; k <= R; ++k) { current_prod *= a[k]; } // 更新全局最大值 global_max = max(global_max, current_prod); } } cout << global_max << "\n"; return 0; }
版本二:累乘递推优化(时间 ,空间 )
【时间复杂度优化建议】 和加法题一样,当区间从
[L, R-1]扩展到[L, R]时,我们没必要把前面的数再乘一遍。直接让Product[L...R] = Product[L...R-1] * a[R]即可消掉最内层循环。 【竞赛评价】 拿到了常识性的优化分。能够通过 的数据,预期得分 50~60 分。但对于 依然会 TLE。#include <iostream> #include <vector> #include <algorithm> using namespace std; const long long INF = 1e18; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<long long> a(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i]; } long long global_max = -INF; for (int L = 1; L <= n; ++L) { long long current_prod = 1; for (int R = L; R <= n; ++R) { // 【关键优化】复用历史乘积,O(1) 更新 current_prod *= a[R]; global_max = max(global_max, current_prod); } } cout << global_max << "\n"; return 0; }
版本三:双状态动态规划(时间 ,空间 )—— 核心破局点
【复杂度分析与思考过程】 现在必须把复杂度降到 。我们尝试定义
max_dp[i]为恰好以第 个数结尾的最大连续乘积。 但遇到负数时,最大的正数乘上负数会变成最小的负数,而最小的负数乘上负数,却会反转成最大的正数! 因此,我们必须同时追踪两个状态:max_dp[i]:以 结尾的最大乘积。min_dp[i]:以 结尾的最小乘积。
转移时,当前位置的最大值/最小值,必然产生于三个候选者中:
- 当前元素单干:
a[i] - 延续前面最大的:
max_dp[i-1] * a[i] - 延续前面最小的(赌负负得正):
min_dp[i-1] * a[i]
【竞赛评价】 非常优秀的解法,利用状态扩展(State Extension)完美化解了负负得正的难题,能够拿下 100% (AC)。
#include <iostream> #include <vector> #include <algorithm> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; vector<long long> a(n + 1); for (int i = 1; i <= n; ++i) { cin >> a[i]; } // 分别记录以 i 结尾的 最大乘积 和 最小乘积 vector<long long> max_dp(n + 1, 0); vector<long long> min_dp(n + 1, 0); // 初始化边界 max_dp[1] = a[1]; min_dp[1] = a[1]; long long global_max = a[1]; for (int i = 2; i <= n; ++i) { // C++11 特性:使用初始化列表 {...} 在多个值中求最值 long long cand1 = a[i]; long long cand2 = max_dp[i - 1] * a[i]; long long cand3 = min_dp[i - 1] * a[i]; // 新的最大值和最小值,必定在这三个数之间产生 max_dp[i] = max({cand1, cand2, cand3}); min_dp[i] = min({cand1, cand2, cand3}); global_max = max(global_max, max_dp[i]); } cout << global_max << "\n"; return 0; }
版本四:动态规划 + 空间压缩(时间 ,空间 )—— 满分标准答案
【空间复杂度分析与优化】 观察版本三的状态转移方程,计算第 步时,只依赖第 步的
max_dp和min_dp,更早的数据再也用不到了。 因此,我们可以将其压缩为两个标量:curr_max和curr_min。🚨【致命易错点:更新污染(The Temp Variable Trap)】 如果你这样写:
curr_max = max({a[i], curr_max * a[i], curr_min * a[i]}); curr_min = min({a[i], curr_max * a[i], curr_min * a[i]}); // 错误!这里的 curr_max 已经被上一行改写了!计算
curr_min时使用的curr_max必须是上一轮旧的值!因此必须引入一个temp_max先把它保存下来。【竞赛评价】 考场的完美斩杀版本,充分体现了编程素养,运行速度飞快且内存只占用常数级别。
#include <iostream> #include <algorithm> using namespace std; int main() { // NOI 竞赛快速 IO 标配 ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; long long first_val; cin >> first_val; // 滚动变量初始化 long long curr_max = first_val; long long curr_min = first_val; long long global_max = first_val; for (int i = 2; i <= n; ++i) { long long val; cin >> val; // 【易错点重点防御】备份上一轮的 curr_max,防止交叉更新时数据被污染! long long temp_max = curr_max; // C++11/14 语法:通过 {...} 一次性找出三个候选值的极值 curr_max = max({val, temp_max * val, curr_min * val}); curr_min = min({val, temp_max * val, curr_min * val}); // 随时打擂台更新全局最高纪录 global_max = max(global_max, curr_max); } cout << global_max << "\n"; return 0; }🏆 教练的延伸点拨:
- 为什么要考虑0? 在上述 DP 转移中,如果
val是0,那么三个候选值算出来分别是0, 0, 0。于是curr_max和curr_min都会乖乖变成0。这就相当于:遇到0,自动斩断了前面累乘的羁绊(因为前面不论多大,乘0都等于0了),后续从 0 开始重新积累。逻辑完全自洽! - 能否只用两个状态转移? 聪明的同学可能会问,如果
val小于0,我直接把上一轮的curr_max和curr_min交换(swap),然后再各自乘以val行不行?——完全可以! 因为负数必然导致原来的最大变最小,最小变最大。用if (val < 0) swap(curr_max, curr_min);的写法也是十分主流且简洁的做法(避免了三个候选值的比较),但在理解上,三个数无差别打擂台(版本四代码)更具普适性(防御了遇到0时的繁琐判定)。
- 1
信息
- ID
- 19480
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者