前言:
啥为单调队列?
单调队列,顾名思义,是队列内元素保持一定单调性(单调递增或单调递减)的队列。这里的单调递增或递减是指的从队首到队尾单调递增或递减。既然是队列,就满足先进先出的特点。与之相对应的是单调栈。这里要说明一下,单调队列是可以变成单调栈的,只要保持队首永远不往前推进即可,所以下文只讲单调队列!
注:队首的数一般就是最值
单调队列(也称滑动窗口)套路
//三步走,掐头,去尾,入队
//我们可以用下标作为元素存入队列中,这样好判断窗口长度是否在范围内
int hh=0,tt=-1;
for(;;){
if(hh<=tt && ...) { ... } //判断窗口长度,去掉范围外的元素
while(hh<=tt && ...){
...
}
q[++tt] = ... ;
}
例题一:
给定一个大小为n≤106的数组。
有一个大小为k的滑动窗口,它从数组的最左边移动到最右边。
您只能在窗口中看到k个数字。
每次滑动窗口向右移动一个位置。
您的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。
输入格式
输入包含两行。
第一行包含两个整数n和k,分别代表数组长度和滑动窗口的长度。
第二行有n个整数,代表数组的具体数值。
同行数据之间用空格隔开。
输出格式
输出包含两个。
第一行输出,从左至右,每个位置滑动窗口中的最小值。
第二行输出,从左至右,每个位置滑动窗口中的最大值。
# include <iostream>
# include <algorithm>
# include <string>
using namespace std;
const int N = 1e6+10;
int a[N],q[N];
int main(void)
{
int n,k;
cin>>n>>k;
for(int i=1;i<=n;++i){
cin>>a[i];
}
int hh=1,tt=0;
for(int i=1;i<=n;++i){
if(hh<=tt && i-k+1>q[hh]) ++hh;
while(hh<=tt && a[i]<=a[q[tt]]) --tt;
q[++tt] = i;
if(i>=k) printf("%d ",a[q[hh]]);
}
//cout<<"*"<<endl;;
cout<<endl;
hh=1,tt=0;
for(int i=1;i<=n;++i){
if(hh<=tt && i-k+1>q[hh]) ++hh;
while(hh<=tt && a[i]>=a[q[tt]]) --tt;
q[++tt] = i;
if(i>=k) cout<<a[q[hh]]<<' ';
}
return 0;
}
例题二:
给定一个长度为N的整数数列,输出每个数左边第一个比它小的数,如果不存在则输出-1。
输入格式
第一行包含整数N,表示数列长度。
第二行包含N个整数,表示整数数列。
输出格式
共一行,包含N个整数,其中第i个数表示第i个数的左边第一个比它小的数,如果不存在则输出-1。
数据范围
1≤N≤1e5
1≤数列中元素≤1e9
//这里我们可以看做是单调栈
# include <iostream>
# include <string>
# include <algorithm>
using namespace std;
const int N = 1e5+10;
int a[N],q[N];
int main(void)
{
int n;
cin>>n;
for(int i=1;i<=n;++i) cin>>a[i];
int hh=1,tt=0;
for(int i=1;i<=n;++i){
while(hh<=tt && a[i]<=a[q[tt]]) --tt;
if(hh<=tt) cout<<a[q[tt]]<<' ';
else cout<<-1<<' ';
q[++tt] = i;
}
return 0;
}
例题三:
上面两道题都是在一维的维度上进行单调队列,这一题是在二维上进行单调队列优化的一道算法题,同时也是一道面试题
题目:记一个矩阵所有元素中的最大值为这个矩阵的C值。给出一个nn的矩阵M,求所有kk(k<n)的子矩阵的C值之和。
例如下面这个5 * 5的矩阵,若k=3,那么图中绿色矩形框圈出的3*3子矩阵的C值为9,蓝色矩形框圈出的 3 * 3子矩阵的C值则为6,它的9个3 * 3的子矩阵的C值分别为9,9,8,7,7,6,6,6,6,其C值之和为64。
第一种做法是暴力做,不讲。
第二种做法如下:
如果我们先用单调队列,把每一列长度为k的滑动窗口的最大值先预处理出来,就像这样,先变成这么一个k*n的矩阵,然后再用一次单调队列,把每一行中长度为k的滑动窗口中的最大值求出来做一下累加,时间复杂度为O((n-k+1) * k)
也就是说,我们把子矩阵里的最大值浓缩成一个二维数组的一个元素,简单点来说,就是先对 行 进行区间k的合并,再进行 列 的区间k的合并。这样最后我们只要遍历这个二维数组,即可得到对应的k * k子矩阵的最大值之和
# include <iostream>
# include <algorithm>
using namespace std;
const int N = 5e3+10;
long long a[N][N];
int n,m,k;
int main(void)
{
cin>>n>>m>>k;
for(int i=1;i<=n;++i){ //打表数组
for(int j=1;j<=m;++j){
int t = __gcd(i,j);
a[i][j] = (i*j)/t;
}
}
for(int j=1;j<=m;++j){ /先合并行
long long b[5010];
int hh=0,tt=-1;
for(int i=1;i<=n;++i){
if(hh<=tt && i-b[hh]+1 > k) ++hh;
while(hh<=tt && a[b[tt]][j]<a[i][j]) --tt;
b[++tt] = i;
if(i>=k){
//cout<<b[hh]<<' ';
a[i-k+1][j] = a[b[hh]][j];
// ++hh;
}
}
}
// cout<<"&&&&&&&&&&&&&&&"<<endl;
// for(int i=1;i<=n-k+1;++i){
// for(int j=1;j<=m;++j){
// cout<<a[i][j]<<' ';
// }cout<<endl;
// }
// cout<<"&&&&&&&&&&&&&&&&&&&&&&"<<endl;
for(int i=1;i<=n-k+1;++i){ //再合并列
long long b[5010];
int hh=0,tt=-1;
for(int j=1;j<=m;++j){
if(hh<=tt && j-b[hh]+1 > k) ++hh;
while(hh<=tt && a[i][b[tt]]<a[i][j]) --tt;
b[++tt] = j;
if(j>=k){
//cout<<b[hh]<<' ';
a[i][j-k+1] = a[i][b[hh]];
// ++hh;
}
}
}
// for(int i=1;i<=n-k+1;++i){
// for(int j=1;j<=m-k+1;++j){
// cout<<a[i][j]<<' ';
// }cout<<endl;
// }
long long ans = 0;
//遍历二维数组,得到目标值
for(int i=1;i<=n-k+1;++i){
for(int j=1;j<=m-k+1;++j){
ans+=a[i][j];
}
}
cout<<ans<<endl;
return 0;
}
单调栈例题
NO.1
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
思路:首先我们枚举某一根柱子 i作为高h=heights[i];
随后我们需要进行向左右两边扩展,使得扩展到的柱子的高度均不小于 h。换句话说,我们需要找到左右两侧最近的高度小于 h的柱子,这样这两根柱子之间(不包括其本身)的所有柱子高度均不小于 h,并且就是 i 能够扩展到的最远范围。
暴力解法
//java
public class Solution {
public int largestRectangleArea(int[] heights) {
int len = heights.length;
// 特判
if (len == 0) {
return 0;
}
int res = 0;
for (int i = 0; i < len; i++) {
// 找左边最后 1 个大于等于 heights[i] 的下标
int left = i;
int curHeight = heights[i];
while (left > 0 && heights[left - 1] >= curHeight) {
left--;
}
// 找右边最后 1 个大于等于 heights[i] 的索引
int right = i;
while (right < len - 1 && heights[right + 1] >= curHeight) {
right++;
}
int width = right - left + 1;
res = Math.max(res, width * curHeight);
}
return res;
}
}
这时候我们就要用上单调栈的特性来解题
- 栈中存放了 j 值。从栈底到栈顶,j 的值严格单调递增,同时对应的高度值也严格单调递增;
- 当我们枚举到第 i 根柱子时,我们从栈顶不断地移除 height[j]≥height[i] 的 j 值。在移除完毕后,栈顶的 jj 值就一定满足 height[j]<height[i],此时 j 就是 i 左侧且最近的小于其高度的柱子。
- 这里会有一种特殊情况。如果我们移除了栈中所有的 j 值,那就说明 i 左侧所有柱子的高度都大于 height[i],那么我们可以认为 i 左侧且最近的小于其高度的柱子在位置 j=-1,它是一根「虚拟」的、高度无限低的柱子。这样的定义不会对我们的答案产生任何的影响,我们也称这根「虚拟」的柱子为「哨兵」。
我们再将 i 放入栈顶。
单调栈
//C++
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;
}
};
NO.2
给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
思路:我们可以将此题转换成上一题来做,看下图,你会发现这就是跟上一题的解法是一样的,只不过我们一层一层传递heights这个数组给之前的代码解题即可。
开个一维数组,记录每一列的情况,当出现某一个元素为0的情况时,应当把那一列的值改为0,因为要保证那一列必须是连续的1才行。
//C++
class Solution {
public:
int maximalRectangle(vector<vector<char>>& matrix) {
if(matrix.size()==0 || matrix[0].size()==0) return 0;
int n = matrix[0].size();
vector<int> height(n);
for(int i=0;i<n;++i) height[i] = 0;
int ans = 0;
for(int i=0;i<matrix.size();++i){
for(int j=0;j<n;++j){
if(matrix[i][j]=='1') height[j]++;
else height[j]=0;
// cout<<height[j]<<' ';
}
// cout<<endl;
ans = max(ans,largestRectangleArea(height));
}
return ans;
}
int largestRectangleArea(vector<int>& heights) {
stack<int> left;
stack<int> right;
vector<int> q;
for(int i=0;i<heights.size();++i){
while(left.size() && heights[left.top()]>=heights[i]) left.pop();
if(left.size()==0) q.push_back(-1);
else q.push_back(left.top());
left.push(i);
}
vector<int> p;
for(int i=heights.size()-1;i>=0;--i){
while(right.size() && heights[right.top()]>=heights[i]) right.pop();
if(right.size()==0) p.push_back(heights.size());
else p.push_back(right.top());
right.push(i);
}
long long ans = 0;
for(int i=0;i<heights.size();++i){
int k = (p[heights.size()-i-1]-q[i]-1) * heights[i];
if(ans<k) ans=k;
}
return ans;
}
};