今天是力扣刷题的第一天,就先从某宇宙厂的经典算法面试题开始刷题吧~能力有限,思路做不到每次都达到最优解或推荐解法(甚至有的可能直接不会了),因此有的答案是对官方题解或评论题解的进一步解释,尽量让题目答案容易理解,主要用于加深自己的理解,希望也能给大家伙带来帮助。
题目链接:接雨水
问题描述:给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
思路:题目中的“柱子”是个二维平面,那么,默认的可接到雨水的条件是:左右两侧均存在柱子,两侧柱子高度均高于中间柱子高度。另外,其实也能发现一个隐含条件:至少存在 3 根柱子,才有可能达到能接到雨水的条件。而能接到雨水的多少,则取决于两侧最高柱子中较矮的那根柱子和当前柱子的高度差。
题解分析:
- 动态规划
官方中的第一个思路,是采取动态规划——判断任一位置柱子可接雨水的最大值。这需要对每个柱子都要向两边扩散扫描,时间复杂度较高,且很多步骤其实是重复的,只是不好判断出来。因此,官方实际上是转换了一个思路——直接分别从左右两边扫描柱子,如果当前柱子的高度低于之前记录的最大高度,那么该位置在对应方向上,该最大高度是那个坑的“壁”,否则,当前位置的柱子可能成为后面可接雨水区域的新的“壁”,然后再取两者中较小的高度,减去当前柱子高度,即是当前位置可接雨水的量。理解也不难:对于某一位置可接雨水的能力,是由两侧中最小可接雨水的能力决定的(木桶的短板效应)。
代码如下:
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
const n = height.length;
// 这里和官方题解的地方不太一样,改动的原因在“思路”中有提到
if (n <= 2) {
return 0;
}
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]);
}
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]);
}
let ans = 0;
for (let i = 0; i < n; ++i) {
ans += Math.min(leftMax[i], rightMax[i]) - height[i];
}
return ans;
};
- 双指针
双指针其实是对动态规划思路的再一次优化——直接从柱子的两边向中间遍历,且始终让较矮的柱子往前走。怎么理解这回事呢?可以这样理解——当我们假设最两边的柱子分别是“木桶”的短板和长板后,对于中间的任一柱子,都有三种状态变化:1. 柱子高度不高于短板高度,作为“坑”接雨水,可接雨水的量就是短板和当前柱子高度的差值;2. 柱子高度高于短板高度但不高于长板高度,此时该柱子作为新的短板,作为后续“坑”的最大可容纳雨水的量的参考;3. 柱子高度高于长板高度,此时该柱子作为新的长板,原长板成为新的短板,后续从该短板的指针向中心移动。直到两指针相遇,最大可接雨水的量就确定了。能这样做的原因是因为指针移动过程中始终保证了指针移动的那一侧始终是决定中间可容纳雨水的量的那一侧。 代码如下:
/**
* @param {number[]} height
* @return {number}
*/
var trap = function(height) {
let ans = 0;
let left = 0, right = height.length - 1;
let 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;
};
另外官方题解还有单调栈的解法,暂时不是很能直观理解到,等再理解一下后补充~