LeetCode 单调栈法

739。每日温度 中等

1.暴力法 来自官方题解
对于温度列表中的每个元素 T[i],需要找到最小的下标 j,使得 i < j 且 T[i] < T[j]。

由于温度范围在 [30, 100] 之内,因此可以维护一个数组 next 记录每个温度第一次出现的下标。数组 next 中的元素初始化为无穷大,在遍历温度列表的过程中更新 next 的值。

反向遍历温度列表。对于每个元素 T[i],在数组 next 中找到从 T[i] + 1 到 100 中每个温度第一次出现的下标,将其中的最小下标记为 warmerIndex,则 warmerIndex 为下一次温度比当天高的下标。如果 warmerIndex 不为无穷大,则 warmerIndex - i 即为下一次温度比当天高的等待天数,最后令 next[T[i]] = i。

为什么上述做法可以保证正确呢?因为遍历温度列表的方向是反向,当遍历到元素 T[i] 时,只有 T[i] 后面的元素被访问过,即对于任意 t,当 next[t] 不为无穷大时,一定存在 j 使得 T[j] == t 且 i < j。又由于遍历到温度列表中的每个元素时都会更新数组 next 中的对应温度的元素值,因此对于任意 t,当 next[t] 不为无穷大时,令 j = next[t],则 j 是满足 T[j] == t 且 i < j 的最小下标。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/daily-temperatures/solution/mei-ri-wen-du-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        int n = T.size();
        vector<int> ans(n), next(101, INT_MAX);
        for (int i = n - 1; i >= 0; --i) {
            int warmerIndex = INT_MAX;
            for (int t = T[i] + 1; t <= 100; ++t) {
                warmerIndex = min(warmerIndex, next[t]);
            }
            if (warmerIndex != INT_MAX) {
                ans[i] = warmerIndex - i;
            }
            next[T[i]] = i;
        }
        return ans;
    }

};
  1. 单调栈法
    可以维护一个存储下标的单调栈,从栈底到栈顶的下标对应的温度列表中的温度依次递减。如果一个下标在单调栈里,则表示尚未找到下一次温度更高的下标。

正向遍历温度列表。对于温度列表中的每个元素 T[i],如果栈为空,则直接将 i 进栈,如果栈不为空,则比较栈顶元素 prevIndex 对应的温度 T[prevIndex] 和当前温度 T[i],如果 T[i] > T[prevIndex],则将 prevIndex 移除,并将 prevIndex 对应的等待天数赋为 i - prevIndex,重复上述操作直到栈为空或者栈顶元素对应的温度小于等于当前温度,然后将 i 进栈。

为什么可以在弹栈的时候更新 ans[prevIndex] 呢?因为在这种情况下,即将进栈的 i 对应的 T[i] 一定是 T[prevIndex] 右边第一个比它大的元素,试想如果 prevIndex 和 i 有比它大的元素,假设下标为 j,那么 prevIndex 一定会在下标 j 的那一轮被弹掉。

由于单调栈满足从栈底到栈顶元素对应的温度递减,因此每次有元素进栈时,会将温度更低的元素全部移除,并更新出栈元素对应的等待天数,这样可以确保等待天数一定是最小的。

以下用一个具体的例子帮助读者理解单调栈。对于温度列表 [73,74,75,71,69,72,76,73][73,74,75,71,69,72,76,73],单调栈 \textit{stack}stack 的初始状态为空,答案 \textit{ans}ans 的初始状态是 [0,0,0,0,0,0,0,0][0,0,0,0,0,0,0,0],按照以下步骤更新单调栈和答案,其中单调栈内的元素都是下标,括号内的数字表示下标在温度列表中对应的温度。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/daily-temperatures/solution/mei-ri-wen-du-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {
public:
    vector<int> dailyTemperatures(vector<int>& T) {
        int n = T.size();
        vector<int> ans(n);
        stack<int> s;
        for (int i = 0; i < n; ++i) {
            while (!s.empty() && T[i] > T[s.top()]) {
                int previousIndex = s.top();
                ans[previousIndex] = i - previousIndex;
                s.pop();
            }
            s.push(i);
        }
        return ans;
    }
};

42.接雨水

1.单调栈法
直观想法

我们可以不用像方法 2 那样存储最大高度,而是用栈来跟踪可能储水的最长的条形块。使用栈就可以在一次遍历内完成计算。

我们在遍历数组时维护一个栈。如果当前的条形块小于或等于栈顶的条形块,我们将条形块的索引入栈,意思是当前的条形块被栈中的前一个条形块界定。如果我们发现一个条形块长于栈顶,我们可以确定栈顶的条形块被当前条形块和栈的前一个条形块界定,因此我们可以弹出栈顶元素并且累加答案到 \text{ans}ans 。

算法

使用栈来存储条形块的索引下标。
遍历数组:
当栈非空且 \text{height}[current]>\text{height}[st.top()]height[current]>height[st.top()]
意味着栈中元素可以被弹出。弹出栈顶元素 \text{top}top。
计算当前元素和栈顶元素的距离,准备进行填充操作
\text{distance} = \text{current} - \text{st.top}() - 1distance=current−st.top()−1
找出界定高度
\text{bounded_height} = \min(\text{height[current]}, \text{height[st.top()]}) - \text{height[top]}bounded_height=min(height[current],height[st.top()])−height[top]
往答案中累加积水量\text{ans} \mathrel{+}= \text{distance} \times \text{bounded_height}ans+=distance×bounded_height
将当前索引下标入栈
将 \text{current}current 移动到下个位置

作者:LeetCode
链接:https://leetcode-cn.com/problems/trapping-rain-water/solution/jie-yu-shui-by-leetcode/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {
public:
    int trap(vector<int>& height) {
        int n=height.size();
        int ans=0;int current=0;
        stack<int> st;
        while (current < height.size()) {
            while (!st.empty() && height[current] > height[st.top()]) {
                int top = st.top();
                st.pop();
                if (st.empty()) break;
                int distance = current - st.top() - 1;
                int bounded_height = min(height[current], height[st.top()]) - height[top];
                ans += distance * bounded_height;
            }
            st.push(current++);
        }
        return ans;
    }
};

2.双指针

int trap(vector<int>& height)
{
    int left = 0, right = height.size() - 1;
    int ans = 0;
    int left_max = 0, right_max = 0;
    while (left < right) {
        if (height[left] < height[right]) {
            height[left] >= left_max ? (left_max = height[left]) : ans += (left_max - height[left]);
            ++left;
        }
        else {
            height[right] >= right_max ? (right_max = height[right]) : ans += (right_max - height[right]);
            --right;
        }
    }
    return ans;
}

84.柱状图中最大的矩形

单调栈

思路

我们归纳一下枚举「高」的方法:

首先我们枚举某一根柱子 ii 作为高 h = \textit{heights}[i]h=heights[i];

随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 hh。换句话说,我们需要找到左右两侧最近的高度小于 hh 的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 hh,并且就是 ii 能够扩展到的最远范围。

那么我们先来看看如何求出一根柱子的左侧且最近的小于其高度的柱子。除了根据「前言」部分暴力地进行枚举之外,我们可以通过如下的一个结论来深入地进行思考:

对于两根柱子 j_0j
0

以及 j_1j
1

,如果 j_0 < j_1j
0

<j
1

并且 \textit{heights}[j_0] \geq \textit{heights}[j_1]heights[j
0

]≥heights[j
1

],那么对于任意的在它们之后出现的柱子 ii(j_1 < ij
1

<i),j_0j
0

一定不会是 ii 左侧且最近的小于其高度的柱子。

换句话说,如果有两根柱子 j_0j
0

和 j_1j
1

,其中 j_0j
0

在 j_1j
1

的左侧,并且 j_0j
0

的高度大于等于 j_1j
1

,那么在后面的柱子 ii 向左找小于其高度的柱子时,j_1j
1

会「挡住」j_0j
0

,j_0j
0

就不会作为答案了。

这样以来,我们可以对数组从左向右进行遍历,同时维护一个「可能作为答案」的数据结构,其中按照从小到大的顺序存放了一些 jj 值。根据上面的结论,如果我们存放了 j_0, j_1, \cdots, j_sj
0

,j
1

,⋯,j
s

,那么一定有 \textit{height}[j_0] < \textit{height}[j_1] < \cdots < \textit{height}[j_s]height[j
0

]<height[j
1

]<⋯<height[j
s

],因为如果有两个相邻的 jj 值对应的高度不满足 << 关系,那么后者会「挡住」前者,前者就不可能作为答案了。

当我们枚举到第 ii 根柱子时,我们的数据结构中存放了 j_0, j_1, \cdots, j_sj
0

,j
1

,⋯,j
s

,如果第 ii 根柱子左侧且最近的小于其高度的柱子为 j_ij
i

,那么必然有

\textit{height}[j_0] < \textit{height}[j_1] < \cdots < \textit{height}[j_i] < \textit{height}[i] \leq \textit{height}[j_{i+1}] < \cdots < \textit{height}[j_s]
height[j
0

]<height[j
1

]<⋯<height[j
i

]<height[i]≤height[j
i+1

]<⋯<height[j
s

]

这样我们就可以使用二分查找的方法找到 ii 对应的 j_ij
i

,但真的需要吗?当我们枚举到 i+1i+1 时,原来的 ii 也变成了 jj 值,因此 ii 会被放入数据结构。由于所有在数据结构中的 jj 值均小于 ii,那么所有高度大于等于 \textit{height}[i]height[i] 的 jj 都不会作为答案,需要从数据结构中移除。而我们发现,这些被移除的 jj 值恰好就是

j_{i+1}, \cdots, j_s
j
i+1

,⋯,j
s

这样我们在枚举到第 ii 根柱子的时候,就可以先把所有高度大于等于 \textit{height}[i]height[i] 的 jj 值全部移除,剩下的 jj 值中高度最高的即为答案。在这之后,我们将 ii 放入数据结构中,开始接下来的枚举。此时,我们需要使用的数据结构也就呼之欲出了,它就是栈。

栈中存放了 jj 值。从栈底到栈顶,jj 的值严格单调递增,同时对应的高度值也严格单调递增;

当我们枚举到第 ii 根柱子时,我们从栈顶不断地移除 \textit{height}[j] \geq \textit{height}[i]height[j]≥height[i] 的 jj 值。在移除完毕后,栈顶的 jj 值就一定满足 \textit{height}[j] < \textit{height}[i]height[j]<height[i],此时 jj 就是 ii 左侧且最近的小于其高度的柱子。

这里会有一种特殊情况。如果我们移除了栈中所有的 jj 值,那就说明 ii 左侧所有柱子的高度都大于 \textit{height}[i]height[i],那么我们可以认为 ii 左侧且最近的小于其高度的柱子在位置 j=-1j=−1,它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。
我们再将 ii 放入栈顶。

栈中存放的元素具有单调性,这就是经典的数据结构「单调栈」了。

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

class Solution {
public:
    int largestRectangleArea(vector<int>& heights) {
        int n = heights.size();
        vector<int> left(n), right(n);
        
        stack<int> mono_stack;
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
                mono_stack.pop();
            }
            left[i] = (mono_stack.empty() ? -1 : mono_stack.top());
            mono_stack.push(i);
        }

        mono_stack = stack<int>();
        for (int i = n - 1; i >= 0; --i) {
            while (!mono_stack.empty() && heights[mono_stack.top()] >= heights[i]) {
                mono_stack.pop();
            }
            right[i] = (mono_stack.empty() ? n : mono_stack.top());
            mono_stack.push(i);
        }
        
        int ans = 0;
        for (int i = 0; i < n; ++i) {
            ans = max(ans, (right[i] - left[i] - 1) * heights[i]);
        }
        return ans;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/largest-rectangle-in-histogram/solution/zhu-zhuang-tu-zhong-zui-da-de-ju-xing-by-leetcode-/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值