2 条题解
-
0
你好!我是金牌教练。为了方便你快速搭建 OJ(在线判题系统)测试环境,我为你编写了这个集成化的数据生成器与标程(Generator & Solver)。
在“寻找峰值”这类题目中,数据构造的关键在于确保 ,并且通过构造单调序列、锯齿序列和大规模随机序列来全面测试选手的二分逻辑。
一、 测试点设计矩阵
测试点 规模 序列特征 考核重点 1-2 随机小规模 基础逻辑正确性 3 1 边界:只有一个元素 边界处理 4 100 严格单调递增 峰值在最右端 () 5 严格单调递减 峰值在最左端 (0) 6 1000 先增后减(山峰形) 峰值在中间 7 先减后增(山谷形) 峰值在两端 8 锯齿形(极多峰值) 随机返回任意峰值的稳定性 9-10 极限随机规模 效率与大数据稳定性
二、 数据生成器 C++ 代码
该程序采用 C++14 标准。它会自动生成
1.in到10.out。为了确保 ,生成逻辑采用了增量随机法。#include <iostream> #include <fstream> #include <vector> #include <string> #include <random> #include <algorithm> #include <ctime> using namespace std; typedef long long ll; // --- 标程:最优解 O(log n) --- int findPeakStd(int n, const vector<int>& nums) { int l = 0, r = n - 1; while (l < r) { int mid = l + (r - l) / 2; if (nums[mid] > nums[mid + 1]) { r = mid; } else { l = mid + 1; } } return l; } // --- 数据构造工具 --- mt19937 rng(time(0)); // 生成 [min, max] 范围内的随机整数 ll randInt(ll min_v, ll max_v) { return uniform_int_distribution<ll>(min_v, max_v)(rng); } void make_data() { for (int t = 1; t <= 10; t++) { string in_name = to_string(t) + ".in"; string out_name = to_string(t) + ".out"; ofstream fout(in_name); ofstream fans(out_name); int n; if (t <= 2) n = randInt(5, 10); else if (t == 3) n = 1; else if (t <= 5) n = 100; else if (t <= 7) n = 1000; else if (t == 8) n = 50000; else n = 100000; vector<int> nums(n); if (t == 3) { nums[0] = randInt(-1e9, 1e9); } else if (t == 4) { // 严格递增 nums[0] = -1e9 + randInt(0, 1e5); for (int i = 1; i < n; i++) nums[i] = nums[i - 1] + randInt(1, 100); } else if (t == 5) { // 严格递减 nums[0] = 1e9 - randInt(0, 1e5); for (int i = 1; i < n; i++) nums[i] = nums[i - 1] - randInt(1, 100); } else if (t == 6) { // A 字形 int mid = n / 2; nums[0] = randInt(-1e8, 0); for (int i = 1; i <= mid; i++) nums[i] = nums[i - 1] + randInt(1, 100); for (int i = mid + 1; i < n; i++) nums[i] = nums[i - 1] - randInt(1, 100); } else if (t == 7) { // V 字形 int mid = n / 2; nums[0] = randInt(1e8, 2e8); for (int i = 1; i <= mid; i++) nums[i] = nums[i - 1] - randInt(1, 100); for (int i = mid + 1; i < n; i++) nums[i] = nums[i - 1] + randInt(1, 100); } else { // 随机震荡 nums[0] = randInt(-1e8, 1e8); for (int i = 1; i < n; i++) { int delta = randInt(1, 1000); if (rng() % 2) nums[i] = nums[i - 1] + delta; else nums[i] = nums[i - 1] - delta; } } // 写入 .in fout << n << "\n"; for (int i = 0; i < n; i++) { fout << nums[i] << (i == n - 1 ? "" : " "); } fout << "\n"; // 写入 .out (标程跑一遍) int ans_idx = findPeakStd(n, nums); fans << ans_idx << "\n"; cout << "Test Case " << t << " [n=" << n << "] generated." << endl; fout.close(); fans.close(); } } int main() { make_data(); return 0; }
三、 生成器关键细节分析
-
文件大小控制:
- 当 时,每个整数约占 10~11 字节,单个
.in文件约 1.1MB。 - 10 个测试点总大小约 5MB。虽然略超“全套 2MB”的理想值,但在 的 NOI 标准下,这是必须的。如果需要极度缩小,可将 的点减少到 1 个。
- 当 时,每个整数约占 10~11 字节,单个
-
强制相邻不等:
- 通过
nums[i] = nums[i-1] + delta(其中 )来生成数据,完美规避了 的非法情况,确保了二分的单调性判定有效。
- 通过
-
计算稳定性:
- 非递归:标程使用
while循环实现,杜绝了栈溢出风险。 - 防溢出:在计算
mid时使用了l + (r - l) / 2。虽然下标不会溢出int,但这是良好的竞赛编程习惯。
- 非递归:标程使用
-
区分算法复杂度:
- 的线性扫描在 规模下通常耗时在 10ms~30ms。
- 的二分查找耗时趋近于 0ms。
- 建议:在 OJ 上将此题时限设置为 0.05s (50ms),这样可以精准地强制选手使用 算法,而过滤掉 算法。
四、 读题关键词总结 (OJ 命题视角)
- “时间复杂度 ”:这是命题人给出的死命令,意味着线性遍历哪怕能过也是“非标准解”。
- “相邻元素不相等”:这是保证“二段性”的救命稻草,没有它,二分将无法通过局部比较判定方向。
- “”:隐含了“山脚下必有山顶”的性质,保证了答案一定存在。
同学,你可以运行这个生成器来检查你自己的代码对各种边界情况(单调序列、单点序列)的处理。加油!
-
-
0
你好!我是金牌教练。寻找峰值这道题非常有趣,它完美诠释了 “局部性质决定全局存在” 的数学思想。
在 NOI 竞赛中,虽然 的线性扫描通常能过 的数据,但掌握 的二分法是区分普通选手与顶尖选手的关键,因为在更复杂的问题中(如高维峰值查找),线性复杂度会彻底失效。
版本 1:暴力扫描版 (Linear Scan)
思路提示: 最直截了当的方法。遍历数组,检查每一个元素是否满足比左边大且比右边大。 由于题目规定 ,所以:
- 如果 ,索引 0 就是峰值。
- 检查 是否大于 。
- 检查 是否大于 。
- 中间元素需满足 且 。
复杂度分析:
- 时间复杂度:。最坏情况下需要遍历整个数组。
- 空间复杂度:(存储输入数据)。
/* * 暴力扫描版:严格按照峰值定义检查 * 易错点:数组越界处理(尤其是 0 和 n-1 处) */ #include <iostream> #include <vector> using namespace std; int main() { int n; if (!(cin >> n)) return 0; vector<int> nums(n); for (int i = 0; i < n; i++) cin >> nums[i]; // 特判:只有一个元素 if (n == 1) { cout << 0 << endl; return 0; } for (int i = 0; i < n; i++) { bool left_ok = (i == 0) || (nums[i] > nums[i - 1]); bool right_ok = (i == n - 1) || (nums[i] > nums[i + 1]); if (left_ok && right_ok) { cout << i << endl; return 0; // 只要找到一个就退出 } } return 0; }
版本 2:线性爬坡版 (Linear Climb)
思路提示: 由于 ,只要数组开始增长(),我们就处于上坡。 第一个满足 的位置 ,一定是一个峰值。 为什么?因为既然它是第一个满足该条件的,说明它之前的元素都是递增的(即 ),而现在它又比右边大,它就是峰。
复杂度分析:
- 时间复杂度:。虽然比版本 1 简洁,但本质仍是线性。
- 空间复杂度:(不计数组)。
/* * 线性爬坡版:寻找第一个下降点 * 逻辑:只要在上升,就还没到峰顶。一旦下降,当前点就是峰。 */ #include <iostream> #include <vector> using namespace std; int main() { int n; cin >> n; vector<int> nums(n); for (int i = 0; i < n; i++) cin >> nums[i]; for (int i = 0; i < n - 1; i++) { // 如果当前点比右边大,由于前面一直在上升,这里就是峰值 if (nums[i] > nums[i + 1]) { cout << i << endl; return 0; } } // 如果一直没找到,说明数组单调递增,最后一个元素是峰 cout << n - 1 << endl; return 0; }
版本 3:最优解 - 二分查找版 (Binary Search)
思路提示: 版本 2 的“爬坡”思想可以被二分加速。
- 取中点
mid。 - 比较
nums[mid]和nums[mid+1]。 - 如果
nums[mid] < nums[mid+1],说明右侧必有峰值(上坡必有顶)。 - 如果
nums[mid] > nums[mid+1],说明左侧(含mid)必有峰值。
复杂度分析:
- 时间复杂度:。对于 数据,仅需 17 次比较。
- 空间复杂度:。
/* * 二分查找版:NOI 满分标准代码 * 关键点:利用局部单调性缩小搜索范围 * 易错点:循环条件 L < R 以及 L = mid + 1 的细节 */ #include <iostream> #include <vector> using namespace std; int main() { // 提高 IO 速度 ios::sync_with_stdio(false); cin.tie(0); int n; if (!(cin >> n)) return 0; vector<int> nums(n); for (int i = 0; i < n; i++) cin >> nums[i]; int left = 0, right = n - 1; // 使用左闭右开或左闭右闭逻辑。这里采用 L < R,最终 L == R 即为答案 while (left < right) { // 防止溢出的中点写法 int mid = left + (right - left) / 2; // 核心判定:往高处走 if (nums[mid] < nums[mid + 1]) { // 右边更高,说明峰值在右侧区间 (不含 mid) left = mid + 1; } else { // 左边更高或相等(虽然题目说不等),说明峰值在左侧区间 (含 mid) right = mid; } } // 循环结束时,left == right,指向其中一个峰值 cout << left << endl; return 0; }
时间复杂度优化建议与总结
-
二段性分析: 通常二分查找需要全局有序,但本题告诉我们,只要能通过某种判定(相邻元素大小)排除掉一半不可能包含特定解(其中一个峰值)的区间,就可以使用二分。这在处理复杂几何问题(如三分法找极值)时非常有用。
-
边界保护: 在版本 3 中,
mid = left + (right - left) / 2。 当left < right时,mid永远能够取到left但永远取不到right。 这意味着mid + 1永远不会导致数组下标越界(最大为right)。这是竞赛中处理边界的小技巧。 -
数据范围注意事项: 题目中 的范围达到了 ,这是
int的极限。在进行比较时没问题,但如果你在代码中涉及nums[i] + nums[j]这种操作,必须开启long long,否则会溢出导致逻辑错误。 -
优化建议: 对于 的规模,
std::vector的分配耗时可以忽略。但如果 达到 以上,建议使用静态数组int a[1000005],以获得更极致的运行速度。
总结:这道题考察的是你对二分本质的理解——二分不是为了找“值”,而是为了通过“判定”不断缩小“候选区间”。加油!
- 1
信息
- ID
- 19503
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者