当青训营遇上码上掘金。
前言
呦,这不接雨水吗?
本文是「青训营 X 码上掘金」主题创作活动主题4“攒青豆”的题解。接雨水是常考的面试题,本文将介绍动态规划、单调栈、双指针三种解题思路。
主题:攒青豆
现有 n 个宽度为 1 的柱子,给出 n 个非负整数依次表示柱子的高度,排列后如下图所示,此时均匀从上空向下撒青豆,计算按此排列的柱子能接住多少青豆。(不考虑边角堆积)
演示
点击码上掘金,查看详细代码。
思路
其实应该是能接住多少格豆子。
暴力做法就是对每个位置向左向右进行扫描,记录左边和右边的最大高度,然后取两边最小值减去自身高度,得出此位置能接多少青豆。这种做法时间复杂度会来到 O(n^2) ,并不是较好的做法,还有明显的优化空间。
思路 1:动态规划
但其实按照朴素算法,在每个点向左右两边扫描时存在重复扫描的情况,可以维护两个数组 leftMax 和 rightMax一次性扫描出每个位置的单侧最大高度值,这样时间复杂度可以达到 O(n) 。动态规划的解法十分通俗易懂。
var trap = function (height) {
const n = height.length;
if (n == 0) {
return 0;
}
// leftMax
const leftMax = new Array(n).fill(0);
leftMax[0] = height[0];
for (let i = 1; i < n; ++i) {
leftMax[i] = Math.max(leftMax[i - 1], height[i]);
}
// rightMax
const rightMax = new Array(n).fill(0);
rightMax[n - 1] = height[n - 1];
for (let i = n - 2; i >= 0; --i) {
rightMax[i] = Math.max(rightMax[i + 1], height[i]);
}
// ans
let ans = 0;
for (let i = 0; i < n; ++i) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
}
思路 2:双指针
因为动态规划需要维护两个数组表示左侧和右侧最大值,空间复杂度任然很高,但数组每个元素最后其实只被用到了一次,十分浪费。我们可以使用双指针的方法,进一步降低空间复杂度。
一个指针从左向右遍历,一个指针从右向左遍历,只要左指针位置高度小于右指针位置高度,则代表其右侧支持形成水坑,但还需左侧 leftMax 支持,即 leftMax - height[left] > 0 ,但此处无需用 if 额外判断,反之亦然。若右侧支持,则左指针可不断向右增加,并更新 leftMax 直到右边不支持,反之右侧向左减少,直到指针相等。
var trap = function (height) {
let ans = 0,
left = 0,
right = height.length - 1,
leftMax = 0,
rightMax = 0;
while (left < right) {
leftMax = Math.max(leftMax, height[left]);
rightMax = Math.max(rightMax, height[right]);
if (height[left] < height[right]) {
ans += leftMax - height[left];
++left;
} else {
ans += rightMax - height[right];
--right;
}
}
return ans;
}
思路 3:单调栈
类似括号匹配,维护一个栈,低柱子如果匹配到高柱子,代表它们之间有空间盛水(豆子)。
- 情况一:遍历柱子高度,若栈为空,将此位置入栈;
- 情况二:若此处高度小于等于栈顶(上一位置),代表此位置可能可以盛水(如果后面又更高的柱子),将此位置入栈;
- 情况三:若此处高度大于栈顶(上一位置),代表之前可能有位置可以盛水,此时将栈顶出栈,计算盛水量,直到此处高度小于等于栈顶(回到上一情况)。
计算盛水量时,指针 left 指向下一个栈顶位置,其实就是积水左边的柱子,而此时位置 i 就是右边的柱子,两者最小值减去之前 top 位置的高度就是可以形成积水的高度。
小细节:为什么两根柱子最小值一定大于等于 top 位置高度?即为什么 top 位置高度一定小于等于 left 位置高度?因为如果 top 位置大于 left 位置就会进入情况三, left 就被出栈结算了。
单调栈方法很优秀但不太容易思考。
var trap = function (height) {
let ans = 0;
const stack = [];
const n = height.length;
for (let i = 0; i < n; ++i) {
while (stack.length && height[i] > height[stack[stack.length - 1]]) {
const top = stack.pop();
if (stack.length) {
const left = stack[stack.length - 1],
currWidth = i - left - 1,
currHeight = Math.min(height[left], height[i]) - height[top];
ans += currWidth * currHeight;
}
}
stack.push(i);
}
return ans;
}
参考资料
42. 接雨水
接雨水 - 接雨水
【接雨水】单调递减栈,简洁代码,动图模拟 - 接雨水
详细通俗的思路分析,多解法 - 接雨水