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输出:
102. 算法思路与选择
2.1 暴力解法及其局限性
最直观的解法是双重循环遍历所有可能的(i,j)对,检查它们之间的所有元素是否满足条件。这种方法的时间复杂度为O(n²),对于n=5e5的情况,显然会超时(约需处理2.5e11次操作)。
2.2 单调栈的引入
这个问题本质上是在寻找数组中每个元素能够"看到"的左侧元素,这提示我们可以使用单调栈(Monotonic Stack)这种数据结构来优化。单调栈能在O(n)时间内解决这类"下一个更大元素"或"可见性"问题。
具体思路:
- 维护一个从栈底到栈顶单调递减的栈
- 对于每个新元素,弹出栈中比它小的元素(这些元素会被当前元素挡住)
- 统计弹出的元素数量以及可能存在的相等情况
2.3 算法正确性证明
单调栈之所以适用于此问题,是因为:
- 栈中元素保持递减顺序,保证了之前较高的元素不会被后来较矮的元素遮挡
- 当遇到一个比栈顶高的元素时,栈顶元素会被挡住,因此可以安全弹出
- 相等的元素需要特殊处理,因为它们可以互相看到但不遮挡后面的相同高度元素
3. 详细实现步骤
3.1 数据结构设计
我们只需要一个标准的栈结构,可以用数组模拟或用语言内置的栈实现。以C++为例:
#include <stack> using namespace std; stack<int> s; // 存储的是人的索引,实际比较时用H[index]3.2 核心算法流程
- 初始化空栈和结果计数器res=0
- 遍历每个人i(从0到n-1): a. 当栈不为空且H[s.top()] < H[i]时:
- 弹出栈顶元素
- res += 1(这个被弹出的人能看到当前人i) b. 如果栈不为空:
- 如果H[s.top()] == H[i]:
- 处理相等情况(需要统计连续相等的人数)
- res += 1(栈顶的人能看到当前人i) c. 将i压入栈中
- 返回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 常数优化技巧
- 使用数组模拟栈比STL stack更快:
int stack[MAXN], top = 0; // push: stack[top++] = x; // pop: --top;- 在比较高度时可以存储上一次弹出的高度,避免重复比较:
int last = -1; while (top > 0 && H[stack[top-1]] < H[i]) { last = H[stack[--top]]; res++; }5. 边界条件与测试用例
5.1 常见边界情况
所有人身高相同:
3 5 5 5输出应为3((1,2), (1,3), (2,3))
严格递增序列:
4 1 2 3 4输出应为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 类似问题应用
这种单调栈方法可以解决一系列类似问题:
- 柱状图中最大矩形(LeetCode 84)
- 接雨水问题(LeetCode 42)
- 每日温度(LeetCode 739)
6.2 三维扩展思考
如果问题扩展到三维空间(如城市天际线问题),则需要使用单调栈的二维变种或扫描线算法。
6.3 其他解法对比
分治法(Divide and Conquer)也可以解决这个问题,但实现更复杂且时间复杂度为O(n log n),不如单调栈高效。
7. 实际应用场景
这种算法在实际中有多种应用:
- 城市天际线分析
- 无线网络基站信号覆盖计算
- 股票分析中的支撑位/阻力位识别
- 游戏中的视野计算
8. 常见错误与调试技巧
8.1 典型错误
- 忽略相等高度的处理,导致计数不足
- 结果变量使用int导致溢出(最大可能对数约为n²/2)
- 栈操作顺序错误,导致逻辑混乱
8.2 调试建议
- 先用小规模数据测试(n≤10),打印中间结果
- 对相等高度的情况单独测试
- 检查结果变量是否为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. 竞赛中的应用策略
在编程竞赛中遇到此类问题时:
- 首先识别问题模式(寻找元素间的可见性/阻挡关系)
- 考虑单调栈是否适用
- 特别注意边界条件和相等元素的处理
- 预先考虑结果可能的数量级,选择合适的数据类型
12. 进一步学习资源
- 《算法竞赛入门经典》- 单调栈章节
- LeetCode单调栈专题
- 经典论文《The Art of Computer Programming》中关于栈的应用
- 可视化算法学习网站(如VisuAlgo)中的单调栈演示
在实际编程竞赛训练中,建议至少完成20道单调栈相关题目来熟练掌握这种数据结构的应用场景和变种。