
一、暴力搜索
第一种解法核心思路是:从左到右遍历,对于每个位置,找到能形成"水槽"的左右边界。但实际写起来很复杂,因为要处理多种情况。
1.1完整代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size(), sum = 0;
if(n < 3) return 0; // 少于3根柱子接不了水
int left = 0;
for(int i = 1; i < n - 1; i++) {
if(height[i] < height[left]) {
// 向右寻找不低于height[left]的柱子
int right = i + 1;
while(right < n && height[right] < height[left]) {
right++;
}
if(right < n) {
// 找到了右边的高点,计算中间水量
for(int j = left + 1; j < right; j++) {
sum += height[left] - height[j];
}
left = right; // 左指针跳到高点
i = right; // i也跳到高点,继续搜索
} else {
// 右边没有比left更高的柱子,找右边最高的柱子
right = i + 1;
int index = i + 1; // 记录右边最高柱子的位置
for(int x = i + 1; x < n; x++) {
if(height[index] < height[x]) {
index = x;
}
}
if(height[index] > height[i]) {
// 以左右边界较矮者作为水面高度
int high = min(height[left], height[index]);
for(int j = left + 1; j < index; j++) {
sum += high - height[j];
}
left = index; // 左指针跳到找到的最高点
i = index; // i也跳到该点
} else {
left++; // 右边没有合适高点,左指针右移
}
}
} else {
left++; // 当前柱子不矮于left,左指针右移
}
}
return sum;
}
};
1.2代码解析
①主要逻辑分支:
- 当前柱子比左边最高柱子矮:
cpp
if(height[i] < height[left]) {
// 需要寻找右边有没有更高的柱子
}
这种情况下,当前柱子可能储水,需要向右找边界。
- 向右寻找高点:
cpp
int right = i+1;
while(right < n && height[right] < height[left]) {
right++;
}
这里的 right < n 是防止数组越界。在向右寻找时,找一个不低于 height[left] 的柱子,这样就能和左边的柱子形成一个完整的水槽。
- 找到了右边的高点:
cpp
if(right < n) {
// 计算 left 到 right 之间的水量
for(int j = left+1; j < right; j++) {
sum += height[left] - height[j];
}
left = right; // 移动左边界
i = right; // 跳过已处理的部分
}
这里水量计算简单,因为左边柱子是 height[left],右边找到的柱子不低于它,所以水面高度就是 height[left]。
- 没找到右边的高点:
这是最复杂的情况。当右边所有柱子都比 height[left] 矮时,就不能以左边柱子为基准了。
cpp
int index = i+1;
for(int x = i+1; x < n; x++) {
if(height[index] < height[x]) index = x;
}
if(height[index] > height[i]) {
// 以 min(height[left], height[index]) 为水面高度
int high = min(height[left], height[index]);
for(int j = left+1; j < index; j++) {
sum += high - height[j];
}
left = index;
i = index;
} else {
left++; // 实在找不到合适的右边界,左指针右移
}
在右边找一个相对最高的柱子,以左右两个柱子的较小值作为水面高度
为什么这么复杂?
因为要处理右边找不到更高柱子的情况,这时水面高度由左右两个边界中的较矮者决定。而且指针移动要小心,不能漏掉位置也不能重复计算。
②注意点:
-
多个
right < n检查都是为了数组越界保护 -
指针移动(
left = right和i = right)要同步,否则会漏掉位置 -
处理右边没有更高柱子时需要重新寻找最高点
二、双指针
核心思想:一个位置能接多少水,取决于它左边最高和右边最高的柱子中的较矮者,如果知道左边最大值和右边最大值,就能判断当前柱子能接多少水
2.1完整代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if(n < 3) return 0;
int left = 0, right = n - 1;
int left_max = 0, right_max = 0;
int water = 0;
while(left < right) {
// 比较左右指针所指柱子的高度
if(height[left] < height[right]) {
// 处理左边:左边较矮,水量由左边最大值决定
if(height[left] > left_max) {
left_max = height[left]; // 更新左边最大值
} else {
water += left_max - height[left]; // 计算当前柱子储水量
}
left++; // 左指针右移
} else {
// 处理右边:右边较矮或不高于左边,水量由右边最大值决定
if(height[right] > right_max) {
right_max = height[right]; // 更新右边最大值
} else {
water += right_max - height[right]; // 计算当前柱子储水量
}
right--; // 右指针左移
}
}
return water;
}
};
2.2代码解析
①关键点 :比较 height[left] 和 height[right]
-
如果
height[left] < height[right]:说明左边较矮,那么对于
left位置,它的储水量由左边的最大值决定(因为右边已经有足够高的height[right]了)两种情况:
-
height[left] >= left_max:更新左边最大值 -
height[left] < left_max:可以储水,水量为left_max - height[left]
然后左指针右移
-
-
如果
height[left] >= height[right]:同理,处理右边,右指针左移
为什么这样是对的?
因为当 height[left] < height[right] 时,我们知道:
-
左边当前位置的值可能不是左边最大的
-
但右边已经有
height[right]这个足够高的柱子 -
所以对于
left位置,它的储水量只受左边历史最大值影响
②代码中的注意点:
-
初始化时
left_max和right_max都是 0 -
移动指针时,总是移动较矮的那一侧
-
计算储水量的条件是当前高度小于该侧的最大值
-
更新最大值时不计算储水量
③关键理解 :
这种方法的精妙之处在于,通过比较左右指针的高度,可以确定当前处理的位置的储水量由哪一侧的最大值决定。而且因为是相向移动,可以确保另一侧有足够高的柱子。
三、动态规划
动态规划解法的核心思路是先预处理出每个位置左右两侧的最大高度,然后根据木桶原理计算每个位置的储水量。
3.1 完整代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n < 3) return 0;
// left_max[i]: 位置i左边(包括i)的最大高度
vector<int> left_max(n);
left_max[0] = height[0]; // 第一个位置左边最大就是它自己
for (int i = 1; i < n; i++) {
// 当前位置左边最大 = max(前一个位置左边最大, 当前高度)
left_max[i] = max(left_max[i - 1], height[i]);
}
// right_max[i]: 位置i右边(包括i)的最大高度
vector<int> right_max(n);
right_max[n - 1] = height[n - 1]; // 最后一个位置右边最大就是它自己
for (int i = n - 2; i >= 0; i--) {
// 当前位置右边最大 = max(后一个位置右边最大, 当前高度)
right_max[i] = max(right_max[i + 1], height[i]);
}
// 计算总雨水量
int water = 0;
for (int i = 0; i < n; i++) {
// 木桶原理:当前位置水量 = min(左边最高, 右边最高) - 当前高度
water += min(left_max[i], right_max[i]) - height[i];
}
return water;
}
};
3.2 代码解析
① 核心思想
对于每个位置 i,它能接的雨水量由以下三个因素决定:
-
它左边所有柱子的最大高度
left_max[i] -
它右边所有柱子的最大高度
right_max[i] -
它自身的高度
height[i]
根据木桶原理,水位高度取决于左右两边较矮的那一边,所以水量 = min(left_max[i], right_max[i]) - height[i]。
② 预处理过程
从左往右遍历,left_max[i] 表示从位置 0 到位置 i 的最大高度。每个位置的值要么是前一个位置的最大值,要么是当前高度,取较大者。
从右往左遍历,right_max[i] 表示从位置 i 到位置 n-1 的最大高度。
③ 计算水量
遍历每个位置,取左右最大高度的较小值作为水位高度,减去当前位置的实际高度,就是该位置的储水量。
④ 注意点
- 边界处理: 第一个位置左边最大就是它自己,最后一个位置右边最大也是它自己。
四、单调栈
单调栈解法的核心思路是按行计算雨水,而不是按列计算。通过维护一个高度递减的栈来寻找可以形成"凹槽"的柱子组合。这种方法巧妙地将二维的雨水问题转化为一维的水平条带累加问题。
4.1完整代码
cpp
class Solution {
public:
int trap(vector<int>& height) {
int n = height.size();
if (n < 3) return 0;
stack<int> stk; // 单调递减栈,存储柱子索引
int water = 0;
for (int i = 0; i < n; i++) {
// 当栈非空且当前柱子高于栈顶柱子时,可能形成凹槽
while (!stk.empty() && height[i] > height[stk.top()]) {
// 栈顶元素是凹槽的底部
int bottom = stk.top();
stk.pop();
// 如果栈为空,说明没有左边界,无法形成凹槽
if (stk.empty()) {
break;
}
// 左边界是栈顶元素(栈底到栈顶高度递减)
int left = stk.top();
// 计算雨水高度:左右边界较矮者 - 底部高度
int h = min(height[left], height[i]) - height[bottom];
// 计算雨水宽度:右边界索引 - 左边界索引 - 1
int w = i - left - 1;
// 累加雨水量:高度 × 宽度
water += h * w;
}
// 当前柱子入栈
stk.push(i);
}
return water;
}
};
4.2 代码解析
① 核心思想
单调栈解法模拟了雨水一层一层填充的过程:
-
维护一个高度递减的栈(栈底到栈顶高度依次降低)
-
当遇到一个比栈顶高的柱子时,说明可能形成了可以储水的"凹槽"
-
计算这个凹槽能接多少雨水,然后继续处理
② 栈操作逻辑
**入栈条件:**当当前柱子高度 <= 栈顶柱子高度时,直接入栈(保持递减特性)。
**出栈条件:**当当前柱子高度 > 栈顶柱子高度时,说明可能形成了凹槽,需要出栈计算。
③ 雨水计算
当找到凹槽时(栈顶元素是底部,当前元素是右边界,栈顶下一个元素是左边界):
cpp
int bottom = stk.top(); // 凹槽底部
stk.pop();
int left = stk.top(); // 左边界
// 雨水高度 = 左右边界较矮者 - 底部高度
int h = min(height[left], height[i]) - height[bottom];
// 雨水宽度 = 右边界 - 左边界 - 1
int w = i - left - 1;
// 雨水量 = 高度 × 宽度
water += h * w;
④ 特殊情况处理
**没有左边界的情况:**如果弹出底部后栈为空,说明没有左边界,无法形成凹槽,直接跳出。
⑤ 注意点
-
栈中存储的是索引,不是高度值,方便计算宽度。
-
栈维护的是递减序列,栈底存储的是目前为止遇到的最大值。
-
按行计算:每次计算的是一个水平条状的雨水区域。
-
宽度计算 :
i - left - 1中的减1是因为左右边界本身不储水。
这种方法其实模拟了雨水一层一层填充的过程,虽然时间复杂度也是 O(n),但理解起来稍微复杂一些。
五、四种解法对比
| 特性 | 暴力搜索 | 双指针 | 动态规划 | 单调栈 |
|---|---|---|---|---|
| 时间复杂度 | O(n²) | O(n) | O(n) | O(n) |
| 空间复杂度 | O(1) | O(1) | O(n) | O(n) |
| 核心思想 | 逐个寻找水槽 | 两边向中间逼近 | 预处理左右最大值 | 按层计算雨水 |
| 优点 | 直观 | 空间最优 | 思路清晰 | 按行计算 |
| 缺点 | 效率低 | 理解稍难 | 需要额外空间 | 栈操作复杂 |
写在最后:
这里展示了解决问题的多种视角:从暴力搜索的直接尝试,到双指针的巧妙优化,再到动态规划的系统分解,以及单调栈的结构转换。
掌握多种解法不仅是为了解题,更是为了培养多角度思考的能力。在实际中,根据不同的约束条件选择最合适的方案,这种灵活性往往比单纯追求最优解更为重要。如果还有其他想法的朋友可以在评论区交流讨论。