1 - 单调栈
准备春招,算法题冲冲冲!
单调栈
单调栈即满足单调性的栈结构,分从小到大和从大到小两种。
例如,栈中自顶向下的元素为
为了让单调栈可用,应遵循 “及时去掉无用数据,保证栈中数据有序” 这一原则。
用数组模拟栈
除了可以用标准库的stack
外,还能用数组简单模拟栈:
// 定义
const int N = 100010;
int stk[N], tt = 0; // tt是栈顶
// 插入
stk[++tt] = x;
// 删除
tt--;
// 判断是否为空
if (tt > 0) 不空;
else 空;
// 取栈顶元素
stk[tt];
例题
单调栈
830. 单调栈 - AcWing题库
这一题要保证栈顶元素始终是较小的数,如果栈顶数比当前数大,就让它出栈。
#include <iostream>
#include <stack>
#include <vector>
using namespace std;
int main()
{
int n;
cin >> n;
vector<int> nums(n);
for (int i = 0; i < n; ++i)
cin >> nums.at(i);
stack<int> stk;
for (int i = 0; i < n; ++i)
{
while (!stk.empty() && stk.top() >= nums.at(i))
{
stk.pop();
}
if (!stk.empty())
{
cout << stk.top() << " ";
}
else
{
cout << "-1" << " ";
}
stk.push(nums.at(i));
}
}
739. 每日温度 - 力扣(LeetCode)
这道题可以从右往左考虑,也能从左往右考虑。
首先看看从右往左的思路:
- 遍历到6,没有比6大的数,把6加入集合,答案为0
- 遍历到3,发现6比3大,把3加入集合,答案为1(6的下标 - 3的下标)
- 遍历到2,发现3比2大,把2加入集合,答案为1
- 遍历到5,发现6比5大,中间的2和3不要了,把5加入集合,答案为3
- 遍历到5,发现6比5大,中间的5不要了,。。。。。。
我们发现这个集合管理的数据刚好满足单调栈的特性(先进先出,单调性),可以用单调栈来做这道题。
vector<int> dailyTemperatures(vector<int>& temperatures)
{
vector<int> ans;
ans.reserve(temperatures.size());
// 从右往左, 单调栈
stack<pair<int, int>> stk;
for (int i = temperatures.size() - 1; i >= 0; --i)
{
while (!stk.empty() && stk.top().first <= temperatures.at(i))
{
stk.pop();
}
if (stk.empty())
{
stk.push({temperatures.at(i), i});
ans.emplace_back(0);
}
else
{
ans.emplace_back(stk.top().second - i);
stk.push({temperatures.at(i), i});
}
}
reverse(ans.begin(), ans.end());
return ans;
}
需要注意的是,由于我们是从右往左记录答案的,需要在最后一步把答案翻转过来。
然后是从左往右的思路:
- 遍历1,1入栈
- 遍历4,发现栈顶1比4小,说明比1大的数是4,1出栈;4入栈
- 遍历3,3入栈
- 遍历5,发现栈顶3比5小,3出栈;发现栈顶4比5小,4出栈;5入栈
- ……
vector<int> dailyTemperatures(vector<int>& temperatures) {
vector<int> ans(temperatures.size());
// 存储下标
stack<int> stk;
for (int i = 0; i < temperatures.size(); ++i)
{
while (!stk.empty() && temperatures.at(stk.top()) < temperatures.at(i))
{
ans.at(stk.top()) = i - stk.top();
stk.pop();
}
stk.push(i);
}
return ans;
}
42. 接雨水 - 力扣(LeetCode)
可以用单调栈来写,以示例1为例:
- 栈空,0入栈
- 由于1比0大,0出栈作为坑的中间,由于此时栈空,没有坑的左边,水会漏,因此放弃计算;1入栈
- 0比1小,0入栈
- 由于2比0大,0出栈作为坑的中间,此时栈中的1作为坑的左边不出栈(因为这个左边的1下次就会变成中间的1了),计算得出雨水为1;2入栈
- 1比2小,1入栈
- 0比1小,0入栈
- 由于1比0大,0出栈,此时左边1中间0右边1,计算水为1;1入栈
- 由于3比1大,1出栈,此时左边2中间1右边3,计算水为3(隔了3格宽);3入栈
- 。。。。。。
代码如下:
int trap(vector<int>& height) {
int ans = 0;
stack<int> stk;
for (int i = 0; i < height.size(); ++i)
{
while (!stk.empty() && height.at(i) >= height.at(stk.top()))
{
int midHeight = height.at(stk.top());
int midPos = stk.top();
stk.pop();
if (stk.empty())
break;
// 不pop Left是因为要让它成为下一个mid
int leftHeight = height.at(stk.top());
int leftPos = stk.top();
ans += (min(leftHeight, height.at(i)) - midHeight) * (i - leftPos - 1);
}
stk.push(i);
}
return ans;
}
矩形系列
84. 柱状图中最大的矩形 - 力扣(LeetCode)
如图,我们可以发现最大矩形的高总是height[i]
,那么剩下的就是求矩形宽的问题了。可以发现,最大矩形旁边总是有两个比它低的小矩形,那么问题就转换为如何找到这两个小矩形了。
可以用单调栈来找,比height[i]
高的元素都出栈,最后栈顶元素就是旁边的矩形了,不过得要从左到右和从右到左找两次。最后遍历所有情况,取最大值即可。
参考代码如下:
int largestRectangleArea(vector<int>& heights) {
int size = heights.size();
// 找面积最大矩形左边的矩形
stack<int> stk;
vector<int> left(size, -1); // 默认-1, 即第一个矩形的左边
for (int i = 0; i < size; ++i)
{
// 让比最大矩形(假设为i)还大的矩形出栈
while (!stk.empty() && heights.at(stk.top()) >= heights.at(i))
{
stk.pop();
}
// 如果栈内有结果, 栈顶就是左边的矩形
if (!stk.empty())
{
left.at(i) = stk.top();
}
stk.push(i);
}
// 找面积最大矩形右边的矩形
while (!stk.empty())
{
stk.pop();
}
vector<int> right(size, size); // 默认size, 即最后一个矩形的右边
for (int i = size - 1; i >= 0; --i)
{
while (!stk.empty() && heights.at(stk.top()) >= heights.at(i))
{
stk.pop();
}
if (!stk.empty())
{
right.at(i) = stk.top();
}
stk.push(i);
}
// 遍历所有情况, 找出最大值
int ans = -1;
for (int i = 0; i < size; ++i)
{
ans = max(ans, (right.at(i) - left.at(i) - 1) * heights.at(i));
}
return ans;
}
字典序最小
贡献法
参考资料
- 分享|【题单】单调栈(矩形系列/字典序最小/贡献法) - 力扣(LeetCode)
- 算法基础课 - AcWing