1. 积水面积问题概述
积水面积问题是一个经典的算法题目,主要考察对数据结构的理解和应用能力。题目描述了一组由正方体叠起的柱子,要求计算这些柱子之间能够积水的总面积。这个问题在实际生活中有很多应用场景,比如城市排水系统设计、地形蓄水计算等。
1.1 问题形象化理解
想象一排高低不齐的柱子排列在一起,下雨时雨水会积聚在柱子之间的凹陷处。积水的形成需要满足一个基本条件:两侧都必须有比当前位置更高的柱子,这样才能形成"容器"来盛装雨水。
举个例子,给定柱子高度序列:[0,1,0,2,1,2,0,0,2,0],我们可以这样理解:
- 第一个0和最后一个0是边界,它们不会积水
- 第一个1左右分别是0和2,可以积水1-0=1个单位
- 中间的0左右分别是1和2,可以积水1-0=1个单位
- 以此类推,最终总积水面积为6个单位
1.2 问题数学化表达
从数学角度看,对于每个位置i,它能积存的水量由以下因素决定:
- 它左侧所有柱子中的最大高度left_max
- 它右侧所有柱子中的最大高度right_max
- 当前位置的柱子高度height[i]
积水量计算公式为:
water[i] = min(left_max, right_max) - height[i]
如果结果为正,则计入总量;否则不计入。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 解法一:左右最大高度法
2.1 算法思路解析
左右最大高度法是最直观的解决方案,其核心思想是:
- 预先计算每个位置左侧的最大高度
- 预先计算每个位置右侧的最大高度
- 遍历每个位置,用左右最大高度的较小值减去当前高度,得到积水量
这种方法的时间复杂度是O(n),因为需要三次遍历:一次计算左最大,一次计算右最大,一次计算结果。空间复杂度也是O(n),需要存储左右最大高度数组。
2.2 代码实现详解
cpp复制#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> height(n);
for (int i = 0; i < n; ++i) {
cin >> height[i];
}
if (n < 3) { // 边界条件处理
cout << 0 << endl;
return 0;
}
// 计算左侧最大高度
vector<int> left_max(n, 0);
for (int i = 1; i < n; ++i) {
left_max[i] = max(left_max[i-1], height[i-1]);
}
// 计算右侧最大高度
vector<int> right_max(n, 0);
for (int i = n-2; i >= 0; --i) {
right_max[i] = max(right_max[i+1], height[i+1]);
}
// 计算总积水量
int total = 0;
for (int i = 0; i < n; ++i) {
int water = min(left_max[i], right_max[i]) - height[i];
if (water > 0) {
total += water;
}
}
cout << total << endl;
return 0;
}
2.3 算法优化思考
虽然这种方法已经很高效,但我们还可以做一些优化:
- 可以合并右最大高度计算和积水计算,减少一次遍历
- 可以使用双指针法进一步优化空间复杂度到O(1)
- 对于特殊输入情况(如单调递增/递减序列)可以提前判断返回0
3. 解法二:单调栈法
3.1 单调栈原理介绍
单调栈是一种特殊的栈结构,它保持栈内元素按照某种顺序(递增或递减)排列。在积水面积问题中,我们使用单调递减栈,即栈底到栈顶的元素对应的高度是递减的。
当遇到一个比栈顶高的柱子时,说明可能形成了一个可以积水的凹槽。这时我们弹出栈顶作为凹槽底部,新的栈顶是左边界,当前柱子是右边界,计算这两者之间的积水量。
3.2 代码实现解析
cpp复制#include <iostream>
#include <vector>
#include <stack>
using namespace std;
int main() {
int n;
cin >> n;
vector<int> height(n);
for (int i = 0; i < n; ++i) {
cin >> height[i];
}
stack<int> st; // 存储柱子索引,高度单调递减
int total = 0;
for (int i = 0; i < n; ++i) {
while (!st.empty() && height[i] > height[st.top()]) {
int bottom_idx = st.top();
st.pop();
if (st.empty()) break;
int left_idx = st.top();
int h = min(height[left_idx], height[i]) - height[bottom_idx];
int w = i - left_idx - 1;
total += h * w;
}
st.push(i);
}
cout << total << endl;
return 0;
}
3.3 算法执行过程示例
以输入[0,1,0,2,1,2,0,0,2,0]为例,单调栈法的执行过程如下:
- i=0: 栈空,push 0
- i=1: height[1]=1 > height[0]=0 → 计算积水(无左边界) → push 1
- i=2: height[2]=0 ≤ height[1]=1 → push 2
- i=3: height[3]=2 > height[2]=0 → 弹出2,计算(1和3之间)积水1 → push 3
- i=4: height[4]=1 ≤ height[3]=2 → push 4
- i=5: height[5]=2 > height[4]=1 → 弹出4,计算(3和5之间)积水1 → push 5
- 继续处理直到结束
4. 两种解法的对比分析
4.1 时间复杂度比较
两种方法的时间复杂度都是O(n),但实际运行时间有差异:
- 左右最大高度法:3次遍历
- 单调栈法:1次遍历,但内部有while循环
对于随机数据,单调栈法通常更快;对于极端数据(如完全递减序列),单调栈法退化为O(n²)。
4.2 空间复杂度比较
- 左右最大高度法:需要两个额外数组,O(n)空间
- 单调栈法:只需要一个栈,最坏情况O(n)空间,但通常更优
4.3 适用场景建议
- 初学者建议先掌握左右最大高度法,逻辑更直观
- 竞赛或性能敏感场景推荐单调栈法,效率更高
- 对于特殊形状数据(如锯齿形),单调栈法表现更好
5. 竞赛实战技巧
5.1 输入输出处理规范
在信息学竞赛中,输入输出有严格规范:
- 必须使用freopen重定向输入输出
- 文件名必须为英文小写
- main函数必须返回0
标准模板:
cpp复制#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
// 解题代码
fclose(stdin);
fclose(stdout);
return 0;
}
5.2 边界条件处理经验
在实际编程中,需要特别注意以下边界条件:
- 柱子数量少于3个时直接返回0
- 首尾柱子高度不为0时的处理(根据题目保证可忽略)
- 所有柱子高度相同的情况
- 柱子高度单调递增或递减的情况
5.3 调试与验证方法
推荐验证方法:
- 手工计算小样例(如题目给的例子)
- 测试极端情况(全0、单调序列等)
- 生成随机数据对拍两种解法
- 使用assert检查中间结果
调试技巧:
- 打印左右最大高度数组
- 跟踪单调栈的变化过程
- 可视化柱子高度和积水区域
6. 算法扩展与变种
6.1 三维积水问题
将问题扩展到三维空间,计算三维地形中的积水体积。这类问题通常需要使用优先队列或更复杂的数据结构,是二维问题的自然延伸。
6.2 动态高度变化问题
考虑柱子高度会随时间变化的情况,需要设计支持动态更新和快速查询的数据结构,如线段树或树状数组。
6.3 其他类似问题
- 柱状图中的最大矩形面积(LeetCode 84)
- 接雨水II(LeetCode 407)
- 容器盛水问题(LeetCode 11)
这些问题都可以使用类似的单调栈或双指针技术解决,体现了算法思想的通用性。
7. 个人实战心得
在实际解决这个问题的过程中,我总结了以下几点经验:
-
理解问题本质是关键。积水问题的核心在于找到每个位置的"容器边界",这个洞察直接引出了两种解法。
-
从暴力法开始思考是个好习惯。最直观的暴力法是对于每个位置,向左右扫描找最大值,时间复杂度O(n²)。优化这个思路就得到了左右最大高度法。
-
单调栈的应用需要多练习。虽然单调栈的解法更高效,但理解起来有一定难度。建议多画图模拟栈的变化过程。
-
边界条件容易出错。特别是单调栈法中,当栈弹出后为空时,说明没有左边界,不能形成积水区域。
-
性能优化要适度。对于竞赛来说,O(n)的解法已经足够,不必过度优化。但在工程实践中,可能需要考虑更极致的优化。
-
测试用例要全面。除了题目给的样例,还应该测试极端情况、随机生成数据,确保代码的鲁棒性。
-
可视化帮助理解。画出柱子高度和积水区域的示意图,可以直观地验证算法的正确性。
