问题描述
暴力解法
首先,对于每一列来说,如果它的左边、右边都存在比自己高的柱子,那么它一定能储水,储水量等同于左边和右边最大值中较小的那个值减去自身高度的差。有了这个思路,我们就可以通过暴力遍历的方法解决该问题:
public int trap(int[] height) {
int result = 0;
int length = height.length;
// 最左和最右的柱子一定不能储水,无需遍历
for (int i = 1; i < length - 1; i++) {
int val = height[i], left = val, right = val;
// 寻找左边最高柱子
for (int j = 0; j < i; j++) {
if (height[j] > left) {
left = height[j];
}
}
// 寻找右边最高柱子
for (int j = i + 1; j < length; j++) {
if (height[j] > right) {
right = height[j];
}
}
result = result + (Math.min(left, right) - val);
}
return result;
}
dp 解法
通过上述暴力解法我们得知,每一列储水的量只由左边最高以及右边最高的柱子决定,而最高柱子的计算又存在一种规则:假设第 i 列右边最高柱子的高度为 x,那么第 i - 1 列右边最高柱子的高度就为 Math.max( 第 i 列的高度 ,x),也就是说第 i 列左边最高柱子和右边最高柱子和第 i+1 列、i-1 列存在计算关系,因此我们可以通过 dp 优化暴力解法:
public int trap2(int[] height) {
int result = 0, length = height.length;
int[] left = new int[length];
int[] right = new int[length];
// 计算第i列左边最高柱子高度
for (int i = 1; i < length - 1; i++) {
left[i] = Math.max(left[i - 1], height[i - 1]);
}
// 计算第i列右边最高柱子高度
for (int i = length - 2; i > 0; i--) {
right[i] = Math.max(right[i + 1], height[i + 1]);
}
// 遍历计算
for (int i = 1; i < length - 1; i++) {
int temp = Math.min(left[i], right[i]) - height[i];
result += temp > 0 ? temp : 0;
}
return result;
}
双指针解法
上面的 dp 解法提醒我们,当前列的储水量只由左边右边柱子最高值的较小值决定,假设此时我们已经知道右边柱子更高一些,那么当前列的最大储水量就只由左边柱子高度决定。此时我们可以通过双指针分别从两头开始遍历,如果右边的柱子高,那么左引用向右遍历,向右遍历的同时也就是在提示:右边存在更高的柱子,当前列的储水量就由左边最高列来决定
public int trap3(int[] height) {
int result = 0, leftMax = 0, rightMax = 0;
int left = 0, right = height.length - 1;
while (left < right) {
// 如果左边小于右边,说明较高的那一列在右边
// 这里虽然不是右边的最高列,但已经比当前列左边任何一列高了
if (height[left] < height[right]) {
// 判断当前列是否大于左边最高列,如果是,当前列不能储水
if (height[left] > leftMax) {
leftMax = height[left++];
// 当前列的储水量由左边最高列决定
} else {
result += leftMax - height[left++];
}
// 右边高同理
} else {
if (height[right] > rightMax) {
rightMax = height[right--];
} else {
result += rightMax - height[right--];
}
}
}
return result;
}
栈解法
前三种解题思路都是根据列来计算,除了这种方式外,还可以使用行的方式来计算。如果第 i 列 比 第 i - 5 列高,并且第 i - 5 列比 i - 6 列低,并且我们确定第 i - 4 列 到 第 i - 1 列都比 i - 5 列低,那么这行可以储水的量就是:长度5乘以第 i 列高和第 i - 6 列高中较小的那个和 i - 5 列高的差值。因此我们可以使用栈将参数数组的下标入栈,方便计算两列之间的距离,如果下一列比栈顶元素高,就向前计算当前行可以储多少水。
public int trap4(int[] height) {
int result = 0, temp = 0;
Stack<Integer> stack = new Stack<Integer>();
while (temp < height.length) {
// 1.首先栈不能为空,其次栈顶元素对应列的高小于遍历列的高
// 2.此时要遍历到栈为空或者栈顶元素比当前列高为止
while (!stack.isEmpty() && height[temp] > height[stack.peek()]) {
// 取出栈顶元素队列应的下标
int top = stack.pop();
// 如果出栈后栈为空,说明栈顶列要么是首列,要么比前面所有列都高,无法储水
if (stack.isEmpty()) {
break;
}
// 计算当前列和栈顶元素列的距离
int distance = temp - stack.peek() - 1;
// 计算可以存储的高度,用当前高度和栈顶元素减去出栈列的高度
int boundLength = Math.min(height[temp], height[stack.peek()]) - height[top];
result += distance * boundLength;
}
// 遍历下标
stack.push(temp++);
}
return result;
}