2 条题解
-
0
在 NOI 竞赛中,构造“柱状图最大矩形”的数据时,必须针对单调栈的特性构造:单调递增序列(考察栈的深度)、单调递减序列(考察出栈结算)、锯齿形序列(考察频繁出栈入栈)以及全等序列(考察对等于高度的处理)。
以下是完整的测试数据生成器,它将自动生成
1.in/1.out到10.in/10.out。【数据生成器 C++ 代码】
#include <iostream> #include <fstream> #include <vector> #include <string> #include <random> #include <algorithm> #include <stack> using namespace std; typedef long long ll; // --- 标准答案算法 (O(n) 单调栈) --- // 使用迭代写法,添加首尾哨兵,保证不爆栈且逻辑严密 ll solve(int n, vector<int>& h) { // 构造带哨兵的临时数组 vector<int> heights; heights.push_back(0); // 左哨兵 for (int x : h) heights.push_back(x); heights.push_back(0); // 右哨兵 int m = heights.size(); static int stk[100005 + 2]; // 静态数组模拟栈 int top = 0; ll max_area = 0; for (int i = 0; i < m; i++) { // 当发现当前高度破坏了栈的单调递增性时,结算 while (top > 0 && heights[i] < heights[stk[top]]) { int mid = stk[top--]; int height = heights[mid]; int width = i - stk[top] - 1; max_area = max(max_area, (ll)height * width); } stk[++top] = i; } return max_area; } // --- 文件读写辅助 --- void writeData(int id, int n, const vector<int>& h) { // 写入输入文件 string inName = to_string(id) + ".in"; ofstream fout(inName); fout << n << "\n"; for (int i = 0; i < n; i++) { fout << h[i] << (i == n - 1 ? "" : " "); } fout << endl; fout.close(); // 计算答案并写入输出文件 vector<int> h_copy = h; ll ans = solve(n, h_copy); string outName = to_string(id) + ".out"; ofstream fans(outName); fans << ans << endl; fans.close(); } int main() { // 随机数引擎 mt19937 rng(20251223); auto getRand = [&](int l, int r) { return uniform_int_distribution<int>(l, r)(rng); }; for (int t = 1; t <= 10; t++) { int n; vector<int> h; if (t == 1) { // 样例数据 n = 6; h = {2, 1, 5, 6, 2, 3}; } else if (t == 2) { // 边界:最小 n n = 1; h = {getRand(0, 10000)}; } else if (t == 3) { // 边界:所有高度相等 (最大面积 n * h) n = 100000; h.assign(n, 10000); } else if (t == 4) { // 严格递增序列 n = 100000; for (int i = 1; i <= n; i++) h.push_back(i % 10001); sort(h.begin(), h.end()); } else if (t == 5) { // 严格递减序列 n = 100000; for (int i = n; i >= 1; i--) h.push_back(i % 10001); sort(h.begin(), h.end(), greater<int>()); } else if (t == 6) { // 锯齿形序列 (10000, 1, 10000, 1...) n = 100000; for (int i = 0; i < n; i++) h.push_back(i % 2 == 0 ? 10000 : 1); } else if (t == 7) { // 随机小规模 (容易目测调试) n = 100; for (int i = 0; i < n; i++) h.push_back(getRand(0, 100)); } else if (t == 8) { // 包含大量高度为 0 的柱子 (切断矩形) n = 100000; for (int i = 0; i < n; i++) h.push_back(getRand(0, 10) == 0 ? 0 : getRand(1, 10000)); } else if (t == 9) { // 随机大规模 A n = 80000; for (int i = 0; i < n; i++) h.push_back(getRand(0, 10000)); } else { // 随机大规模 B n = 100000; for (int i = 0; i < n; i++) h.push_back(getRand(0, 10000)); } writeData(t, n, h); cout << "Test Case " << t << " finished. N=" << n << endl; } return 0; }
测试点设计逻辑说明:
- 测试点 1 (Sample):确保程序能通过官方样例。
- 测试点 2 (Mini): 是最容易被忽视的边界,测试初始化是否正确。
- 测试点 3 (Flat):全相等序列。测试对
heights[i] == heights[stk[top]]情况的处理,此时宽度应能正确跨越整个数组。 - 测试点 4 (Increasing):测试栈的增长上限,确保栈数组没有开小。
- 测试点 5 (Decreasing):测试连续弹栈逻辑。
- 测试点 6 (Sawtooth):测试极端频繁的入栈和出栈切换。
- 测试点 7 (Small Random):用于在选手本地调试时提供易读的小规模数据。
- 测试点 8 (Sparse):大量高度为 0 的柱子会将直方图分割成许多小的孤立区域,考察程序是否会因为中间高度为 0 而逻辑中断。
- 测试点 9-10 (Large Random): 级别的综合压力测试。
生成器技术细节:
- 哨兵实现:在
solve函数中,通过向vector插入首尾 0,确保了所有有效数据都能被弹出并计算。这是解决本题最稳健的方法。 - 避免爆栈:该算法是纯迭代(
for循环嵌套while弹出),没有递归调用。即便 ,也只受限于内存大小(Memory Limit),不会产生栈溢出(Stack Overflow)。 - 长整型安全:面积计算使用了
(ll)height * width。在 的极限下,答案为 ,虽然在int范围内,但为了防止题目变体(如 ),代码中使用了long long保证通用性。 - 内存优化:
stk数组使用static修饰,避免在 10 次测试点生成的循环中反复在栈内存上分配大块空间。
-
0
你好,同学。这道题是单调栈的高级应用。在处理这类“高度受限”的区间问题时,单调栈能帮我们迅速锁定“左右边界”。
以下是按照 NOIP 竞赛标准编写的 C++14 代码,使用了哨兵技巧和手动模拟栈来保证最优性能。
【标准题解代码】
#include <cstdio> #include <algorithm> using namespace std; /** * 易错点 1:数据范围。 * n = 10^5, h = 10^4,最大面积可能达到 10^9。 * 虽然 int 极限是 2.1e9,但在计算过程中使用 long long 是保险的做法, * 尤其是在题目变体中数据范围更大时。 */ typedef long long ll; const int MAXN = 100005; int h[MAXN]; // 存储高度 int stk[MAXN]; // 手写栈,存储下标 int top = 0; // 栈顶指针 int main() { int n; if (scanf("%d", &n) != 1) return 0; /** * 关键点 2:哨兵 (Sentinel) 技巧。 * 我们在高度数组的首尾各添加一个高度为 0 的柱子。 * - 左侧的 0:保证栈永远不会为空(栈底始终有一个 0),避免 while 循环中判断 stack.empty()。 * - 右侧的 0:保证遍历结束时,栈中所有剩余的柱子都能被弹出并计算面积。 */ h[0] = 0; for (int i = 1; i <= n; i++) { scanf("%d", &h[i]); } h[n + 1] = 0; ll max_area = 0; // 放入第一个哨兵的下标 stk[++top] = 0; // 遍历从 1 到 n+1(包含右哨兵) for (int i = 1; i <= n + 1; i++) { /** * 维护一个单调递增栈。 * 如果当前柱子比栈顶矮,说明以栈顶柱子为高度的矩形无法再向右延伸了。 */ while (top > 0 && h[i] < h[stk[top]]) { int mid = stk[top--]; // 确定要计算哪根柱子的高度 int height = h[mid]; /** * 关键点 3:计算宽度。 * 此时栈顶剩下的元素,就是 mid 柱子左边第一个比它矮的柱子。 * 当前的 i,就是 mid 柱子右边第一个比它矮的柱子。 * 所以宽度 w = (右边界 - 左边界 - 1) = (i - stk[top] - 1)。 */ int width = i - stk[top] - 1; ll current_area = (ll)height * width; if (current_area > max_area) { max_area = current_area; } } // 当前柱子下标入栈 stk[++top] = i; } printf("%lld\n", max_area); return 0; }
二、 复杂度分析思考过程
1. 时间复杂度:
- 思考过程:
- 外层
for循环从 到 ,总计 次。 - 内层
while循环:虽然看似嵌套,但请观察stk。每个下标 只会在for循环末尾执行一次stk[++top](入栈),并且在while逻辑中被执行一次top--(出栈)。 - 结论:总的操作次数是 。时间复杂度为线性的 。在 规模下,执行时间通常在 20ms 以内。
- 外层
2. 空间复杂度:
- 思考过程:
h[]数组占用 字节。stk[]数组占用 字节。- 总占用空间约为 MB。
- 结论:在 256MB 的限制下,绰绰有余。
三、 时间复杂度优化建议
虽然 已经是理论最优复杂度,但在竞赛(尤其是大数据量 )中,我们可以做如下微调:
-
快读 (Fast I/O): 如果 的范围进一步扩大,
scanf可能会成为瓶颈。使用getchar封装的read()函数能显著提升读入速度。 -
避免强制类型转换: 虽然我们在计算
height * width时用了(ll),如果对常数极其敏感,可以预先将h[]数组定义为long long,减少类型转换指令。 -
哨兵的硬编码: 如果你不想修改原数组
h[],也可以在循环内部通过逻辑判断模拟哨兵,但这样会增加if-else分支预测失败的概率,通常不建议。
四、 关键逻辑总结 (避坑指南)
- 宽度计算公式:很多同学会写错成
i - mid或者i - stk[top]。请务必记住:矩形是跨越在左右两个“矮子”之间的空隙里,所以是 。 - 栈内存放的是什么:单调栈通常存放下标而不是具体数值。因为下标既能帮我们找到高度(
h[stk[top]]),又能帮我们计算宽度(i - stk[top])。 - 等于高度的处理:当
h[i] == h[stk[top]]时,可以入栈也可以不处理(弹出后再压入)。在本题中,由于我们要找的是“第一个比自己矮的”,相等的柱子不会限制高度,因此可以合并到h[i] >= h[stk[top]]的情况中。
教练寄语: 单调栈是一种“延迟结算”的艺术。每一个入栈的元素都在等待属于它的“右边界”。一旦边界出现,它就完成使命,贡献出它的面积。掌握了哨兵和宽度公式,你就掌握了这道题的灵魂。
- 思考过程:
- 1
信息
- ID
- 19365
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 10
- 标签
- (无)
- 递交数
- 1
- 已通过
- 1
- 上传者