今天是单调栈的第2天,第1道题是前面的延续,第2道题很难还常考。第2题双指针和DP解法重点是“当前位置的雨水量取决于左右两边柱子最高高度”,单调栈解法则要熟悉“左、中、右三个柱子各自的含义和作用”。
第1题(LeetCode 503. 下一个更大元素II)相比day 58中第1题(LeetCode 739. 每日温度)变成了循环数组,且要求返回的结果是更大的数字本身,而不是下标的差值。对于循环数组,某个值更大的元素可能出现在其左边部分,最极端的情况是某个元素的更大值出现在与其相邻的左边。所以要遍历两次nums才能保证所有元素对应的值更大元素在其之后被遍历到。
具体的做法有两种,第1种是将nums复制一份拼接在其后(代码如下),再用 day 58中第1题(LeetCode 739. 每日温度)中的方法遍历该数组。
vector nums1(nums.begin(), nums.end());
nums.insert(nums.end(), nums1.begin(), nums1.end());
第2种方法则是不占用额外空间,用下标取余数的方式来模拟两次遍历数组。这种方法更高效。
class Solution {
public:
vector nextGreaterElements(vector& nums) {
vector res(nums.size(), -1);
stack st;
st.push(0);
for (int i = 0; i < 2 * nums.size(); ++i) {
int ind = i % nums.size();
if (nums[ind] <= nums[st.top()]) {
st.push(ind);
}
else {
while (!st.empty() && nums[ind] > nums[st.top()]) {
res[st.top()] = nums[ind];
st.pop();
}
st.push(ind);
}
}
return res;
}
};
第2题(LeetCode 42. 接雨水)自己想了很久还是没想出,是一道高频考题需要注意。题解中有三种解法,分别是单调栈、双指针、DP解法。
单调栈是三种方法中最难的一种,核心思想是维持单调递减的栈,在出现非递减元素时,弹出栈中元素的同时加雨水,每次所加的雨水数量都是其宽度和高度的乘积。当前元素与栈顶元素有小于、等于和大于3种情况:
第3种情况的实现较为复杂,需要首先得到中间柱子,然后使其出栈,再得到左柱子,然后再计算宽度、高度,加雨水量。加完之后,有可能此时的栈顶元素,也就是左柱子,仍然小于当前非递减元素。所以要继续将其作为中柱子来重复上面的过程,所以这一过程要用while()来进行,其中的循环条件要首先保证栈非空,然后就是栈顶元素小于当前非递减元素。
// 单调栈
class Solution {
public:
int trap(vector& height) {
int ans = 0;
stack st;
st.push(0);
for (int i = 1; i < height.size(); ++i) {
if (height[i] < height[st.top()]) {
st.push(i);
}
else if (height[i] == height[st.top()]) {
st.pop();
st.push(i);
}
else {
while (!st.empty() && height[i] > height[st.top()]) {
int mid = st.top();
st.pop();
if (!st.empty()) {
int w = i - st.top() - 1;
int h = min(height[i], height[st.top()]) - height[mid];
ans += w * h;
}
}
st.push(i);
}
}
return ans;
}
};
双指针解法相对容易理解,但时间复杂度为O(n²),较高,会超时。这里采用按列计算的方法,也就是单独计算每个位置的雨水高度。这个高度取决于当前位置左边部分的最高柱子、和右边部分最高柱子,两者中较小值,再减去当前位置的柱子高度,就是当前位置的雨水高度。所以需要在每个位置分别向左、右遍历,计算左、右两边的最高柱子高度。在遍历之前,可以将最高柱子高度初始化为当前柱子高度,这样一来,如果左边或右边没有更高的柱子,最后当前位置的雨水量就是0。最后需要注意最左边和最右边位置是不会接到雨水的,所以循环范围应该不包括左、右两端点。
// 双指针法,超时
class Solution {
public:
int trap(vector& height) {
int ans = 0;
for (int i = 1; i < height.size() - 1; ++i) {
int leftMax = height[i], rightMax = height[i];
for (int j = i - 1; j >= 0; --j) {
leftMax = max(leftMax, height[j]);
}
for (int j = i + 1; j < height.size(); ++j) {
rightMax = max(rightMax, height[j]);
}
ans += min(leftMax, rightMax) - height[i];
}
return ans;
}
};
DP解法沿用了上面双指针方法的思路,只不过会用DP方法提前一次性计算出每个位置左、右两边的最高柱子高度。dpLeft[i]、dpRight[i]分别定义为位置i左、右两边的最高柱子高度。对于位置i,其左边部分最高柱子高度要么是其左邻居的左边部分最高柱子高度,要么是自己本身,对应max(dp[i - 1], height[i]);同理其右边部分最高柱子高度对应max(dp[i + 1], height[i])。初始化部分,dpLeft最左端、dpRight最右端需要分别初始化为height的最左端、最右端值。其他部分与双指针法一致。
// DP
class Solution {
public:
int trap(vector& height) {
vector dpLeft(height.size()), dpRight(height.size());
dpLeft[0] = height[0];
dpRight[height.size() - 1] = height[height.size() - 1];
for (int i = 1; i < height.size(); ++i) {
dpLeft[i] = max(dpLeft[i - 1], height[i]);
}
for (int i = height.size() - 2; i >= 0; --i) {
dpRight[i] = max(dpRight[i + 1], height[i]);
}
int ans = 0;
for (int i = 1; i < height.size() - 1; ++i) {
ans += min(dpLeft[i], dpRight[i]) - height[i];
}
return ans;
}
};