☰
单调栈算法解析:解决COI 2007排队问题
2026/11/4 10:33:34 网站建设 项目流程

1. 题目背景与核心需求解析

这道来自COI 2007的编程竞赛题目,描述了一个有趣的现实场景:在Patrik的音乐会现场,N个人排成一列等待入场,每个人有特定的身高。题目要求我们计算能够互相看到对方的两人组合数量。这里的"互相看到"定义为:两人之间(不含他们自己)的所有人都比他们矮。

1.1 问题建模

这个问题可以抽象为:给定一个整数数组H(代表排队人群的身高序列),找出所有满足以下条件的(i,j)对:

  • i < j
  • 对于所有i < k < j,H[k] < min(H[i], H[j])

换句话说,我们需要统计数组中所有"相互可见"的元素对,这些元素对之间的所有元素都小于它们中的较小值。

1.2 输入输出分析

典型输入格式:

N H1 H2 ... HN

其中N≤500,000,Hi≤1,000,000

输出为一个整数,表示满足条件的对数。

示例: 输入:

7 2 4 1 2 2 5 1

输出:

10

2. 算法思路与选择

2.1 暴力解法及其局限性

最直观的解法是双重循环遍历所有可能的(i,j)对,检查它们之间的所有元素是否满足条件。这种方法的时间复杂度为O(n²),对于n=5e5的情况,显然会超时(约需处理2.5e11次操作)。

2.2 单调栈的引入

这个问题本质上是在寻找数组中每个元素能够"看到"的左侧元素,这提示我们可以使用单调栈(Monotonic Stack)这种数据结构来优化。单调栈能在O(n)时间内解决这类"下一个更大元素"或"可见性"问题。

具体思路:

  • 维护一个从栈底到栈顶单调递减的栈
  • 对于每个新元素,弹出栈中比它小的元素(这些元素会被当前元素挡住)
  • 统计弹出的元素数量以及可能存在的相等情况

2.3 算法正确性证明

单调栈之所以适用于此问题,是因为:

  1. 栈中元素保持递减顺序,保证了之前较高的元素不会被后来较矮的元素遮挡
  2. 当遇到一个比栈顶高的元素时,栈顶元素会被挡住,因此可以安全弹出
  3. 相等的元素需要特殊处理,因为它们可以互相看到但不遮挡后面的相同高度元素

3. 详细实现步骤

3.1 数据结构设计

我们只需要一个标准的栈结构,可以用数组模拟或用语言内置的栈实现。以C++为例:

#include <stack> using namespace std; stack<int> s; // 存储的是人的索引,实际比较时用H[index]

3.2 核心算法流程

  1. 初始化空栈和结果计数器res=0
  2. 遍历每个人i(从0到n-1): a. 当栈不为空且H[s.top()] < H[i]时:
    • 弹出栈顶元素
    • res += 1(这个被弹出的人能看到当前人i) b. 如果栈不为空:
    • 如果H[s.top()] == H[i]:
      • 处理相等情况(需要统计连续相等的人数)
    • res += 1(栈顶的人能看到当前人i) c. 将i压入栈中
  3. 返回res

3.3 处理相等高度的特殊情况

当遇到与栈顶相同高度的元素时,需要特殊处理:

  • 这些相同高度的元素可以互相看到
  • 但不遮挡后面相同高度的元素

实现技巧:

int cnt = 1; while (!s.empty() && H[s.top()] == H[i]) { res += cnt++; s.pop(); } if (!s.empty()) res++; s.push(i);

3.4 完整代码实现

#include <iostream> #include <stack> using namespace std; const int MAXN = 5e5+5; int H[MAXN]; int main() { int n; cin >> n; for (int i = 0; i < n; i++) cin >> H[i]; stack<int> s; long long res = 0; for (int i = 0; i < n; i++) { int cnt = 1; while (!s.empty() && H[s.top()] < H[i]) { res++; s.pop(); } if (!s.empty()) { if (H[s.top()] == H[i]) { int j = s.top(); cnt = 1; while (!s.empty() && H[s.top()] == H[i]) { res += cnt++; s.pop(); } } if (!s.empty()) res++; } s.push(i); } cout << res << endl; return 0; }

4. 复杂度分析与优化

4.1 时间复杂度

每个元素最多被压入和弹出栈一次,因此时间复杂度为O(n),完美处理n=5e5的规模。

4.2 空间复杂度

最坏情况下所有元素都在栈中,空间复杂度为O(n)。

4.3 常数优化技巧

  1. 使用数组模拟栈比STL stack更快:
int stack[MAXN], top = 0; // push: stack[top++] = x; // pop: --top;
  1. 在比较高度时可以存储上一次弹出的高度,避免重复比较:
int last = -1; while (top > 0 && H[stack[top-1]] < H[i]) { last = H[stack[--top]]; res++; }

5. 边界条件与测试用例

5.1 常见边界情况

  1. 所有人身高相同:

    3 5 5 5

    输出应为3((1,2), (1,3), (2,3))

  2. 严格递增序列:

    4 1 2 3 4

    输出应为3(只能看到相邻的人)

  3. 严格递减序列:

    4 4 3 2 1

    输出应为3(同样只能看到相邻的人)

5.2 大规模数据测试

生成随机数据测试:

import random n = 500000 print(n) for _ in range(n): print(random.randint(1, 1000000))

确保程序在最大规模下仍能在1秒内完成。

6. 算法扩展与变种

6.1 类似问题应用

这种单调栈方法可以解决一系列类似问题:

  1. 柱状图中最大矩形(LeetCode 84)
  2. 接雨水问题(LeetCode 42)
  3. 每日温度(LeetCode 739)

6.2 三维扩展思考

如果问题扩展到三维空间(如城市天际线问题),则需要使用单调栈的二维变种或扫描线算法。

6.3 其他解法对比

分治法(Divide and Conquer)也可以解决这个问题,但实现更复杂且时间复杂度为O(n log n),不如单调栈高效。

7. 实际应用场景

这种算法在实际中有多种应用:

  1. 城市天际线分析
  2. 无线网络基站信号覆盖计算
  3. 股票分析中的支撑位/阻力位识别
  4. 游戏中的视野计算

8. 常见错误与调试技巧

8.1 典型错误

  1. 忽略相等高度的处理,导致计数不足
  2. 结果变量使用int导致溢出(最大可能对数约为n²/2)
  3. 栈操作顺序错误,导致逻辑混乱

8.2 调试建议

  1. 先用小规模数据测试(n≤10),打印中间结果
  2. 对相等高度的情况单独测试
  3. 检查结果变量是否为long long类型

关键提示:在竞赛中遇到此类问题时,先手动计算小样例的预期结果,再与程序输出对比,可以快速定位逻辑错误。

9. 性能优化实战

对于极端情况(如所有身高相同),原始算法可能退化为O(n²)。可以通过预处理连续相同高度的区间来优化:

struct Person { int height; int count; }; stack<Person> s; for (int i = 0; i < n; i++) { Person current{H[i], 1}; while (!s.empty() && s.top().height <= H[i]) { if (s.top().height == H[i]) { current.count += s.top().count; } res += s.top().count; s.pop(); } if (!s.empty()) res++; s.push(current); }

这种优化将最坏情况也保持在O(n)时间复杂度。

10. 不同语言实现要点

10.1 Python实现

Python中使用list作为栈,注意:

s = [] res = 0 for i in range(n): cnt = 1 while s and H[s[-1]] < H[i]: res += 1 s.pop() # ...处理相等情况... s.append(i)

10.2 Java实现

Java中使用Deque作为栈更高效:

Deque<Integer> stack = new ArrayDeque<>(); long res = 0; for (int i = 0; i < n; i++) { int cnt = 1; while (!stack.isEmpty() && H[stack.peek()] < H[i]) { res++; stack.pop(); } // ...处理相等情况... stack.push(i); }

11. 竞赛中的应用策略

在编程竞赛中遇到此类问题时:

  1. 首先识别问题模式(寻找元素间的可见性/阻挡关系)
  2. 考虑单调栈是否适用
  3. 特别注意边界条件和相等元素的处理
  4. 预先考虑结果可能的数量级,选择合适的数据类型

12. 进一步学习资源

  1. 《算法竞赛入门经典》- 单调栈章节
  2. LeetCode单调栈专题
  3. 经典论文《The Art of Computer Programming》中关于栈的应用
  4. 可视化算法学习网站(如VisuAlgo)中的单调栈演示

在实际编程竞赛训练中,建议至少完成20道单调栈相关题目来熟练掌握这种数据结构的应用场景和变种。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询