2 条题解
-
0
这是一个非常实用的需求。为了方便你搭建OJ(Online Judge)的测试数据,我编写了一个全自动的C++数据生成器。
这个程序运行时,会在当前目录下直接生成
1.in~10.in以及对应的1.out~10.out文件。数据生成策略设计
为了保证测试数据的全面性和强度,我设计了如下的测试点分布,覆盖了边界情况、特殊构造和大数据压力测试:
- 测试点 1-3 (小数据/边界):
- #1: (题目最小限制),验证极小情况。
- #2: ,完全随机。
- #3: ,数值全部相等(平地),验证特殊逻辑。
- 测试点 4-6 (中等数据 ):
- #4: 单调递增序列。
- #5: 单调递减序列。
- #6: 随机分布。
- 测试点 7-10 (大数据 ):
- #7: 凹型数据(两端高,中间低),考察贪心是否会被两端误导。
- #8: 凸型数据(两端低,中间高),考察向内收缩的正确性。
- #9: 锯齿状数据(0, 10000, 0, 10000...),考察极端波动。
- #10: 全随机大数据,最大强度压测。
数据生成器代码 (C++14)
请将以下代码保存为
generator.cpp,编译并运行即可。/** * NOI Style Data Generator for "Container With Most Water" * Generates 1.in/1.out to 10.in/10.out * * Compiler: C++14 * Author: OI Coach */ #include <iostream> #include <vector> #include <string> #include <fstream> #include <random> #include <algorithm> #include <chrono> using namespace std; // ========================================== // Part 1: 标准解法 (Solver) // 用于生成 .out 文件,确保答案绝对正确 // ========================================== long long solve(const vector<int>& a) { int n = a.size(); int L = 0; int R = n - 1; long long max_area = 0; while (L < R) { int width = R - L; int h = min(a[L], a[R]); max_area = max(max_area, (long long)width * h); if (a[L] < a[R]) { L++; } else { R--; } } return max_area; } // ========================================== // Part 2: 数据生成器核心 (Generator) // ========================================== // 随机数生成器初始化 (使用 mt19937 保证随机质量) mt19937 rng(chrono::steady_clock::now().time_since_epoch().count()); // 生成指定范围内的随机整数 [min, max] int random_int(int min, int max) { uniform_int_distribution<int> dist(min, max); return dist(rng); } // 不同类型的数据构造函数 // type 0: 完全随机 // type 1: 单调递增 // type 2: 单调递减 // type 3: 全部相等 // type 4: 凹型 (两头高中间低) // type 5: 凸型 (两头低中间高) // type 6: 锯齿型 (高低交错) vector<int> generate_data(int n, int max_val, int type) { vector<int> a(n); if (type == 0) { // Random for (int i = 0; i < n; ++i) a[i] = random_int(0, max_val); } else if (type == 1) { // Increasing for (int i = 0; i < n; ++i) a[i] = (long long)i * max_val / n; } else if (type == 2) { // Decreasing for (int i = 0; i < n; ++i) a[i] = max_val - ((long long)i * max_val / n); } else if (type == 3) { // All Same int val = random_int(1, max_val); for (int i = 0; i < n; ++i) a[i] = val; } else if (type == 4) { // Concave (Bowl) for (int i = 0; i < n; ++i) { int mid = n / 2; int dist = abs(i - mid); a[i] = (long long)dist * max_val / mid; if (a[i] == 0) a[i] = 1; // 避免0 } } else if (type == 5) { // Convex (Mountain) for (int i = 0; i < n; ++i) { int mid = n / 2; int dist = abs(i - mid); a[i] = max_val - ((long long)dist * max_val / mid); } } else if (type == 6) { // Sawtooth for (int i = 0; i < n; ++i) { a[i] = (i % 2 == 0) ? random_int(0, max_val / 10) : random_int(max_val * 0.9, max_val); } } return a; } void write_file(int id, int n, int max_val, int type, const string& desc) { string in_filename = to_string(id) + ".in"; string out_filename = to_string(id) + ".out"; // 生成数据 vector<int> a = generate_data(n, max_val, type); // 写入 .in 文件 ofstream fin(in_filename); 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(a); ofstream fout(out_filename); fout << ans << "\n"; fout.close(); cout << "Generated Test Case #" << id << ": " << desc << " (N=" << n << ", Type=" << type << ")" << endl; } int main() { cout << "Start generating data..." << endl; // 题目约束 const int MAX_VAL = 10000; const int MAX_N_SMALL = 1000; const int MAX_N_LARGE = 100000; // --- 测试点 1-3: 小数据与边界 --- write_file(1, 2, 100, 0, "Smallest N"); // N=2, 随机 write_file(2, 100, MAX_VAL, 0, "Small Random"); // N=100, 随机 write_file(3, 100, MAX_VAL, 3, "All Same Values");// N=100, 全等 // --- 测试点 4-6: 中等数据 (满足 30% 数据范围) --- write_file(4, MAX_N_SMALL, MAX_VAL, 1, "Increasing"); // 单调增 write_file(5, MAX_N_SMALL, MAX_VAL, 2, "Decreasing"); // 单调减 write_file(6, MAX_N_SMALL, MAX_VAL, 0, "Mid Random"); // 中等随机 // --- 测试点 7-10: 大数据 (满足 100% 数据范围) --- write_file(7, MAX_N_LARGE, MAX_VAL, 4, "Concave (Bowl)"); // 凹型 write_file(8, MAX_N_LARGE, MAX_VAL, 5, "Convex (Mountain)"); // 凸型 write_file(9, MAX_N_LARGE, MAX_VAL, 6, "Sawtooth (Extreme)"); // 锯齿 write_file(10, MAX_N_LARGE, MAX_VAL, 0, "Large Random"); // 满级随机 cout << "All data generated successfully!" << endl; return 0; }使用指南
- 编译:
g++ generator.cpp -o generator -O2 -std=c++14 - 运行:
- 在 Windows 上双击
generator.exe或命令行运行。 - 在 Linux/Mac 上运行
./generator。
- 在 Windows 上双击
- 结果:
程序运行结束后,你的文件夹里会出现
1.in,1.out...10.in,10.out共20个文件。
代码说明
- 非递归写法: 数据生成和求解过程全部采用迭代(循环)写法,避免了 时递归可能导致的栈溢出风险。
- 效率优化:
- 求解器使用的是 的双指针算法。对于 10 组数据,总计算量约为 次操作,运行时间在几毫秒以内,非常快。
vector预分配内存,IO 使用ofstream,确保生成效率。
- 随机性:
使用了
std::mt19937配合chrono时间种子,比rand()产生的随机数质量更高,分布更均匀。 - 格式规范:
- 输入文件:符合题目要求的第一行 ,第二行空格分隔的数组。
- 输出文件:单个整数结果,带换行符。
你可以直接把这些文件上传到你的 OJ 后台作为标准测试集。
- 测试点 1-3 (小数据/边界):
-
0
你好!作为你的教练,很高兴看到你开始关注代码实现的规范性和细节。在NOI/NOIP竞赛中,算法的正确性固然重要,但代码的鲁棒性(Robustness)、数据类型的选择以及常数优化往往决定了你是拿满分还是遗憾挂分。
下面是针对第11题“盛最多水的容器”的标准题解代码,完全符合NOI C++14标准。
1. 标准题解代码 (C++14)
/** * 题目:盛水容器 (Water Container) * 考察点:双指针 (Two Pointers)、贪心 (Greedy) * 时间复杂度:O(n) * 空间复杂度:O(n) * * 作者:OI金牌教练 * 编译器标准:C++14 */ #include <iostream> #include <vector> #include <algorithm> // 用于 std::max, std::min using namespace std; // 【关键点1】数据规模与类型定义 // 虽然本题最大乘积约为 10^5 * 10^4 = 10^9,正好在 int (约2*10^9) 范围内。 // 但在竞赛中,为了防止边界溢出或数据加强,涉及“乘积”、“累加”的结果, // 强烈建议习惯性使用 long long。 using ll = long long; // 【关键点2】全局数组定义 // 对于 N=10^5 级别的数据,尽量开在全局变量或使用 vector。 // 如果在 main 函数内部开 int a[100005],可能会导致栈溢出 (Stack Overflow)。 const int MAXN = 100005; int a[MAXN]; int main() { // 【关键点3】I/O 加速 // 必须关闭流同步,否则在大数据量读入时(>5*10^5)可能会因为IO过慢而超时。 // 注意:关闭后不能混用 scanf/printf 和 cin/cout。 ios::sync_with_stdio(false); cin.tie(nullptr); int n; if (!(cin >> n)) return 0; // 读入数据,这里使用 1-based indexing (下标从1开始) 还是 0-based 都可以。 // 为了方便计算宽度 (j - i),这里采用 0 到 n-1。 for (int i = 0; i < n; ++i) { cin >> a[i]; } // 【关键点4】双指针初始化 // L 代表左挡板下标,R 代表右挡板下标 int L = 0; int R = n - 1; ll max_area = 0; // 记录最大面积 // 【关键点5】循环终止条件 // 当 L 和 R 相遇时,容器宽度为0,没有意义,结束循环。 while (L < R) { // 计算当前底边宽度 int width = R - L; // 计算当前高度(短板效应:取两者较小值) int h = min(a[L], a[R]); // 更新答案 // 注意:计算时强制转为 long long,防止中间结果溢出(虽然本题不会,但要养成习惯) max_area = max(max_area, (ll)width * h); // 【关键点6】贪心策略核心 // 我们希望面积 = 宽度 * 高度 最大化。 // 现在的宽度已经是当前可能的最大值了(随着指针内移,宽度只会减小)。 // 想要面积变大,必须指望高度变高。 // 如果移动高板:宽度减小,新板子再高也受限于没动的那个短板,高度不会增加,面积一定变小。 // 如果移动短板:宽度减小,但新板子可能很高,从而让整体高度提升,面积有可能变大。 // 结论:谁短谁移动。 if (a[L] < a[R]) { L++; // 左边短,左指针右移 } else { R--; // 右边短(或相等),右指针左移 } } cout << max_area << endl; return 0; }
2. 复杂度分析与思考过程
时间复杂度分析
- 思考过程:
我们主要关注代码中循环的执行次数。
我们定义了两个指针
L和R,分别位于数组的两端。在while循环的每一步中,要么L增加 1,要么R减少 1。 循环终止条件是L == R。这意味着L和R加起来总共移动了 步。 循环内部的操作(比较、计算、赋值)都是 的常数操作。 - 结论:总时间复杂度为 。
- 对于 的数据,计算机大约需要执行 次运算,远小于竞赛通常的 1秒限时(约 次运算),因此该算法非常高效,可以通过 的数据。
空间复杂度分析
- 思考过程:
我们开了一个大小为 的数组
a[]来存储输入数据。 除此之外,只使用了L,R,max_area,width,h等几个辅助变量。 - 结论:总空间复杂度为 。
- 个
int大约占用 400KB 内存,远小于通常的 128MB 或 256MB 内存限制。
- 个
3. 时间复杂度优化建议
对于这道题, 已经是理论上的最优解了,因为你至少需要读取 个数(这是 的)。算法层面上无法进一步优化时间复杂度,但我们可以在常数层面(Constant Factor)进行微调,这在卡常题(Time Limit 非常紧的题目)中很有用:
-
I/O 优化(最重要):
- 如代码所示,使用
ios::sync_with_stdio(false); cin.tie(nullptr);。 - 如果数据量极大(例如 ),可以使用
快读 (Fast Read)模板(即使用getchar()解析整数),比cin和scanf都要快。
- 如代码所示,使用
-
避免不必要的内存操作:
- 代码中使用了全局数组
int a[MAXN]。相比于std::vector<int> a(n),静态数组通常略微快一点点,因为它没有动态分配内存和构造对象的开销(虽然这个差异微乎其微)。
- 代码中使用了全局数组
-
分支预测优化(高级,本题不强求):
- CPU 流水线不喜欢
if-else分支跳转。但在双指针算法中,分支是必须的逻辑,难以避免。有些同学会尝试用位运算消除分支,但在这种业务逻辑下往往得不偿失。保持代码逻辑清晰(如上的贪心判断)是首选。
- CPU 流水线不喜欢
4. 教练的总结(易错点复盘)
-
关于
long long:- 现象:很多同学看到 就觉得用
int足够了。 - 教训:在OI比赛中,出题人有时会把数据范围卡在
int溢出的边缘(例如 以上)。凡是涉及“乘法”计算出结果的,请无脑上long long,这不会增加多少运行时间,但能买个保险。
- 现象:很多同学看到 就觉得用
-
贪心的证明:
- 为什么移动长的那一边不行?一定要自己在草稿纸上画图确认:移动长边,短板不变(甚至更短),宽度减小 面积必减小。这个逻辑闭环是解题的关键。
希望这份标准题解能帮助你规范代码习惯!继续加油!
- 思考过程:
我们主要关注代码中循环的执行次数。
我们定义了两个指针
- 1
信息
- ID
- 19340
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者