【算法】单调栈 & 单调队列学习笔记
1. 单调栈简介
1.1 前言
今天是 2023/1/15,一中寒假集训阶段性的结束了。集训的学习笔记可以在本人 blogs 的【算法】标签栏中找。
马上就要过年了,提前祝大家新年快乐!
1.2 什么是单调栈
单调栈(monotone-stack)是一种基于栈进行的算法,且栈内元素(栈底到栈顶)都是(严格)单调递增或者单调递减的。
定义很抽象,不如拿一道题来直观的理解单调栈。
1.3 算法流程
1.3.1 [luoguP5788]【模板】单调栈
给出项数为 \(n\) 的整数数列 \(a_{1 \dots n}\)。
定义函数 \(f(i)\) 代表数列中第 \(i\) 个元素之后第一个大于 \(a_i\) 的元素的下标,即 \(f(i)=\min_{i<j\leq n, a_j > a_i} \{j\}\)。若不存在,则 \(f(i)=0\)。
试求出 \(f(1\dots n)\)。
对于 \(100\%\) 的数据,\(1 \le n\leq 3\times 10^6\),\(1\leq a_i\leq 10^9\)。
1.3.2 Solve
建一个单调不减栈。
先来看一下单调栈的算法流程:
No.1
No.2
No.3
No.4
No.5
No.6
No.7
No.8
看完流程,是不是对单调栈的原理有了一定的认知了呢。
每一次加入新元素,都会从栈中弹出一些元素使得栈保持单调。通过观察发现,设栈中的一个元素在原序列中的编号为 \(x\),如果新加入的元素可以使得栈中元素弹出的话,那么新加入的元素则是我们要找的第一个大于 \(a_x\) 的元素。(仅限于单调不减栈)
所以每一次就在弹栈时更新答案即可。
怎么样?是不是豁然开朗。
1.4 Code & 单调栈的实现
以上一道题为例:
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 3e6 + 10;
int n, a[N], f[N], stk[N], tot;
signed main(){
n = read();
For(i,1,n) {
a[i] = read();
while(tot && a[stk[tot]] < a[i]){//栈顶元素比新元素小,弹栈
f[stk[tot]] = i;
--tot;
}
stk[++tot] = i;//将新元素加入单调栈中
}
For(i,1,n) {
cout << f[i] << ' ';
}
return 0;
}
1.5 单调栈时间复杂度分析
有人会问了:上面的程序不是用了两个循环吗?两个循环不就是 \(O(n^2)\) 时间复杂度吗,那不还是超时了?
并非如此,我们发现,对于一个栈来说,在最坏的情况下,一个元素会进一次栈,出一次栈。\(n\) 个元素便最多会进出栈 \(2n\) 次。也就是说,尽管有两个循环,单调栈的时间复杂度依然是 \(O(n)\) 的!
因此我们可以看到,单调栈的时间复杂度是很优秀的。
2. 单调栈进阶
2.1 悬线法
我们来看一道经典的例题:
2.1.1 [luoguP1387] 最大正方形
在一个 \(n\times m\) 的只包含 \(0\) 和 \(1\) 的矩阵里找出一个不包含 \(0\) 的最大正方形,输出边长。\((1 \le n,m \le 100)\)
2.1.2 Solve
题意就是要找到一个最大全 \(1\) 正方形,并输出边长。
一眼暴力,枚举对顶点,\(O(1)\) 判断是否合法。很幸运,由于数据过水,您成功的过掉了此题。
这一题还有别的做法吗?,肯定有,它就是——悬线法+单调栈!
把样例搬过来:
由于它是一个矩阵,所以朴素版的单调栈已经对此“无能为力”。
既然单调栈不能变成二维,那么,我们就把矩阵转化成一维。即可用悬线法转化。
我们可以枚举每一行,然后用一个 \(h\) 数组记录一下此行到第 \(1\) 行连续的 \(1\)。长度。
然后用单调栈维护 \(h\) 数组中每一个数向左看和向右看第一个小于它的数的后一个数,最后把答案统计一下就行了。
算法流程如下:
No.1
统计第一层向上连续的 \(1\) 的长度,红色框住部分为连续的 \(1\)。
No.2
用单调栈维护 \(h\) 数组中每一个数向左看和向右看第一个小于它的数的后一个数(取范围最大的即可),如图中蓝色部分所示:
此时答案更新为 \(min(h_i,r-l+1)=min(1,2)=1\) (由于是正方形,所以答案应为长与宽中的最小值作为边长)。
No.3
第二行亦是如此,答案更新为 \(min(h_i,r-l+1)=min(2,2)=2\)。
No.4
第三行,答案不做更新。
注:虚线部分表示实际答案统计的范围,已在上文中解释,不在此过多赘述。
No.5
第四行,答案不做更新。
结束,答案为 \(2\)。
2.1.3 Code
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e2 + 10;
int n, m, h[N], a[N][N], l[N], r[N], ans, top, stk[N];
void work1() {
top = 0;
FOR(i,m,1) {
while(top && h[stk[top]] > h[i]) {
l[stk[top]] = i + 1;
top--;
}
stk[++top] = i;
}
while(top) {
l[stk[top]] = 1;
top--;
}
}
void work2() {
top = 0;
For(i,1,m) {
while(top && h[stk[top]] > h[i]) {
r[stk[top]] = i - 1;
top--;
}
stk[++top] = i;
}
while(top) {
r[stk[top]] = m;
top--;
}
}
signed main() {
n = read(), m = read();
For(i,1,n) {
For(j,1,m) {
a[i][j] = read();
h[j] = (a[i][j] == 0 ? 0 : h[j] + 1);
}
work1();
work2();
For(j,1,m) {
ans = max(ans, min(h[j], r[j] - l[j] + 1));
}
}
cout << ans << '\n';
return 0;
}
3. 单调栈例题
3.1 P2659 美丽的序列
Problem
给定一个长度为 \(n\) 的序列 \(A\),求
$\max _{1 \leq l \leq r \leq n}\left\{\left(\min _{i=l}^{r} A_{i}\right) \times\left(r-l+1\right)\right\}$。
\(1 \le n \le 10^6\)
Solve
枚举所有可能的区间最小值(即每一个数),再向外扩展区间。
我们发现最终答案肯定是按区间长度单调递增,所以我们要尽可能的把区间扩大。
扩大的极限就为每一个数左边第一个小于它的数的后一个数的位置与右边第一个小于它的数的前一个数的位置。
用单调栈解决即可。
Code
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 2e6 + 10;
int n, a[N], ans, l[N], r[N], stk[N], top;
void work1() {
top = 0;
FOR(i,n,1) {
while(top && a[stk[top]] > a[i]) {
l[stk[top]] = i + 1;
top--;
}
stk[++top] = i;
}
while(top) {
l[stk[top]] = 1;
top--;
}
}
void work2() {
top = 0;
For(i,1,n) {
while(top && a[stk[top]] > a[i]) {
r[stk[top]] = i - 1;
top--;
}
stk[++top] = i;
}
while(top) {
r[stk[top]] = n;
top--;
}
}
signed main() {
n = read();
For(i,1,n) a[i] = read();
work1();
work2();
For(i,1,n) {
ans = max(ans, 1ll * a[i] * (r[i] - l[i] + 1));
}
cout << ans << '\n';
return 0;
}
3.2 P1901 发射站
Problem
给定 \(n\) 个能量发射站,每一个能量发射站都能向左右两边发射能量,一个能量站 \(i\) 能接收到能量站 \(j\) 发射的能量当且仅当 \(h_i > h_j\)。求接收最多能量的发射站接收的能量是多少。
Solve
一眼单调栈。
建一个单调递减的栈,分别从左往右扫,从右往左扫找到每一个能量站向左/右看第一个大于此能量站高度的能量站,并把大于此能量站高度的能量站的能量加到此能量站里去。
时间复杂度 \(O(n)\)
Code
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e6 + 10;
int n, l[N], r[N], ans, h[N], v[N], stk[N], top;
signed main() {
n = read();
For(i,1,n) h[i] = read(), v[i] = read();
top = 0;
For(i,1,n) {
while(top && h[stk[top]] < h[i]) {
r[i] += v[stk[top]];
--top;
}
stk[++top] = i;
}
top = 0;
FOR(i,n,1) {
while(top && h[stk[top]] < h[i]) {
l[i] += v[stk[top]];
--top;
}
stk[++top] = i;
}
For(i,1,n) ans = max(ans, l[i] + r[i]);
cout << ans << '\n';
return 0;
}
3.3 P1950 长方形
Problem
给定一个 \(n \times m\) 的矩形,矩形上有‘*’和‘.’,两种符号。
问有多少个不同的由‘.’组成的矩形(“不同”当且仅当矩形的大小与位置均不相同)
Solve
暴力很难做,需要用到降为技巧。
类比于 2.1.1 最大正方形 那道题,这道题与之类似,都可以用到悬线法。
分别建一个单调递增和单调不递减的栈,然后遍历每一行,记录从这一行向上看‘.’的个数。然后做与 2.1.1 最大正方形 同样的操作即可。
注意这里记录 \(l\) 端点的栈为单调不递减的栈,目的是防止记重。
Code
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e3 + 10;
int n, m, h[N], stk[N], l[N], r[N], top, ans;
char c[N][N];
void stkl() {
top = 0;
FOR(i,m,1) {
while(top && h[stk[top]] >= h[i]) {
l[stk[top]] = i+1;
top--;
}
stk[++top] = i;
}
while(top) {
l[stk[top]] = 1;
top--;
}
}
void stkr() {
top = 0;
For(i,1,m) {
while(top && h[stk[top]] > h[i]) {
r[stk[top]] = i-1;
top--;
}
stk[++top] = i;
}
while(top) {
r[stk[top]] = m;
top--;
}
}
signed main() {
n = read(), m = read();
For(i,1,n) For(j,1,m) cin >> c[i][j];
For(i,1,n) {
For(j,1,m) {
if(c[i][j] == '.') h[j]++;
else h[j] = 0;
}
stkl();
stkr();
For(j,1,m) {
ans += (j - l[j] + 1) * (r[j] - j + 1) * h[j];
}
}
cout << ans << '\n';
return 0;
}
3.4 P3467 [POI2008] PLA-Postering
Problem
给定 \(n\) 个矩形,以及它们的长和宽。矩形和矩形之间紧挨着,没有间隙。现在想用长宽任意的矩形来完美覆盖它们,一个矩形被完美覆盖当且仅当长宽任意的矩形之间不相互重叠,且紧贴这 \(n\) 个矩形。求最少需要几个长宽任意的矩形覆盖它们。
Solve
容易发现,由于覆盖的矩形长宽任意,所以被覆盖的矩形的宽是没有用的。
对于 \(n\) 个高度互不相同的矩形。显然其覆盖矩形的数量为 \(n\)。又可以发现,当任意两个被覆盖的矩形高度相等且这两个矩形之间的矩形的高度都比它们高,那覆盖矩形的数量便可从原来的基础上减一。
所以建一个单调递增的栈,遇到于栈顶相同的元素,就记录一下。
时间复杂度 \(O(n)\)
Code
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 2e6 + 5e5 + 10;
int n, h[N], stk[N], top, ans, x;
signed main() {
n = read();
For(i,1,n) x = read(), h[i] = read();
For(i,1,n) {
while(top && h[stk[top]] >= h[i]) {
if(h[i] == h[stk[top]]) ans++;
top--;
}
stk[++top] = i;
}
cout << n - ans << '\n';
return 0;
}
3.5 CF1691D Max GEQ Sum
Problem
给定一个长度为 \(n\) 的序列。求 \(\forall l,\forall r,\max\limits_{i=l}^r a_i \ge \sum\limits_{i=l}^r a_i\) 是否成立。
Solve
转换一下角度:要求 \(\forall l,\forall r,\max\limits_{i=l}^r a_i \ge \sum\limits_{i=l}^r a_i\) 是否成立。它的逆命题是能否找到一个 \(l, r\),使得 \(\max\limits_{i=l}^r a_i < \sum\limits_{i=l}^r a_i\) 成立。
暴力可以做到了 \(O(n^2)\),预处理前缀和以及 ST 表即可。
换一种思路,枚举每个数作为最大值的区间,把区间分成左右两段,分别进行单调栈即可。
设 \(a_k\) 为枚举的区间最大值,则左半区间和为 \(\sum\limits_{i=l}^{k-1}\),左半区间和为 \(\sum\limits_{i=k+1}^{r}\)。
要是命题成立,则要满足:\(\sum\limits_{i=l}^{k-1} + a_k > a_k\) 或者 \(\sum\limits_{i=k+1}^{r} + a_k > a_k\),即 \(\sum\limits_{i=l}^{k-1} > 0\) 或 \(\sum\limits_{i=k+1}^{r} > 0\)。此时只要用单调栈找到以 \(k\) 为中心且 \(a_k\) 为最大值向左/右能到达的最靠前/后的点。然后再 check 就行了。
时间复杂度 \(O(n)\)。
Code
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 2e5 + 10;
int T, n, a[N], sum[N], pre[N], stk[N], top, f;
signed main() {
T = read();
while(T--) {
n = read();
For(i,1,n) a[i] = read();
// cout << "Tes" << '\n';
For(i,1,n) sum[i] = sum[i - 1] + a[i];
FOR(i,n,1) pre[i] = pre[i + 1] + a[i];
top = 0;
f = 0;
For(i,1,n) {
int maxi = 0;
while(top && a[stk[top]] <= a[i]) {
maxi = max(maxi, sum[i-1] - sum[stk[top]-1]);
top--;
}
if (maxi > 0) {
f = 1; break;
}
stk[++top] = i;
}
top = 0;
FOR(i,n,1) {
int maxi = 0;
while(top && a[stk[top]] <= a[i]) {
maxi = max(maxi, pre[i+1] - pre[stk[top]+1]);
top--;
}
if (maxi > 0) {
f = 1; break;
}
stk[++top] = i;
}
if(f) puts("NO");
else puts("YES");
}
return 0;
}
4. 单调队列
4.1 什么是单调队列
单调队列又称滑动窗口算法,它可以在 \(O(n)\) 的时间复杂度内算出固定长度的所有区间最大/最小值。
同样可以理解为有 淘汰性质 的单调栈。(本质就是单调栈 + 时间戳)。
什么是固定长度的所有区间最大/最小值呢?
请看下图:
4.2 算法流程
4.2.1 luoguP1886 滑动窗口 /【模板】单调队列
有一个长为 \(n\) 的序列 \(a\),以及一个大小为 \(k\) 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
【数据范围】
对于 \(50\%\) 的数据,\(1 \le n \le 10^5\);
对于 \(100\%\) 的数据,\(1\le k \le n \le 10^6\),\(a_i \in [-2^{31},2^{31})\)。
4.2.2 Solve
把样例贺过来,以最大值为例,建一个单调递减的单调队列。
No.1
No.2
No.3
No.4
No.5
No.6
No.7
值得注意的是,要是某个元素过期了(过了所求区间),就要将其从 head 弹出。
4.2.3 Code
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1000100;
int n, k, a[N], q[N], h = 1, t = 0;
signed main() {
n = read(), k = read();
For(i,1,n) a[i] = read();
For(i,1,n) {
while(h <= t && i - q[h] + 1 > k) h++;
while(h <= t && a[q[t]] > a[i]) t--;
q[++t] = i;
if(i >= k) cout << a[q[h]] << ' ';
}
h = 1, t = 0;
cout << '\n';
For(i,1,n) {
while(h <= t && i - q[h] + 1 > k) h++;
while(h <= t && a[q[t]] < a[i]) t--;
q[++t] = i;
if(i >= k) cout << a[q[h]] << ' ';
}
return 0;
}
4.3 单调队列时间复杂度分析
和单调栈一样,每个元素最多会进队出队一次。所以时间复杂度为 \(O(n)\)。
5. 单调队列例题
5.1 P2698 [USACO12MAR] Flowerpot S
Problem
给定 \(n\) 个水滴,坐标为 \((x_i,y_i)\)。
每滴水以每秒 \(1\) 个单位长度的速度下落。你需要把花盆放在 \(x\) 轴上的某个位置,使得从被花盆接着的第 \(1\) 滴水开始,到被花盆接着的最后 \(1\) 滴水结束,之间的时间差至少为 \(D\)。
求最小的花盆的宽度 \(w\)。
Solve
考虑二分答案,由于花盆的宽度与合法的概率有单调性,所以直接二分花盆宽度 \(w\)。
check 的时候用一个单调队列记录窗口长度为 \(w\) 时的最大最小值,只要某一时刻在窗口内的最大值减最小值大于 D,说明其合法。反之非法。
Code
#include <bits/stdc++.h>
#define ll long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 100100;
struct Node {
int x, y;
}a[N];
int n = read(), D = read(), w, l = 1, r = 1;
int q1[N], q2[N];
bool cmp(Node x, Node y) {
return x.x < y.x;
}
bool check(int k) {
int h1 = 1, t1 = 0, h2 = 1, t2 = 0;
For(i,1,n) {
while(h1 <= t1 && a[i].x - a[q1[h1]].x > k) h1++;
while(h2 <= t2 && a[i].x - a[q2[h2]].x > k) h2++;
while(h1 <= t1 && a[q1[t1]].y < a[i].y) t1--;
while(h2 <= t2 && a[q2[t2]].y > a[i].y) t2--;
q1[++t1] = q2[++t2] = i;
if(a[q1[h1]].y - a[q2[h2]].y >= D) return 1;
}
return 0;
}
signed main() {
For(i,1,n) {
int p;
a[i] = (Node){read(), p = read()};
r = max(r, p);
}
sort(a + 1, a + n + 1, cmp);
w = -1;
while(l <= r) {
int mid = l + r >> 1;
if(check(mid)) {
w = mid;
r = mid - 1;
} else {
l = mid + 1;
}
}
cout << w << '\n';
return 0;
}
5.2 P2216 [HAOI2007] 理想的正方形
Problem
给定一个 \(a \times b\) 的一个矩阵,找出一个 \(n \times n\) 的正方形,使得该区域所有数中的最大值和最小值的差最小。
Solve
这道题是二维单调队列模板,但是很考察代码实现能力。
先对每一行求一边窗口长度为 \(n\) 的单调队列,再把得到的值对每一列求一边窗口长度为 \(n\) 的单调队列,最后汇总答案即可。
Code
#include <bits/stdc++.h>
#define int long long
#define H 19260817
#define rint register int
#define For(i,l,r) for(rint i=l;i<=r;++i)
#define FOR(i,r,l) for(rint i=r;i>=l;--i)
#define MOD 1000003
#define mod 1000000007
using namespace std;
inline int read() {
rint x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if (ch=='-') f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
return x*f;
}
void print(int x){
if(x<0){putchar('-');x=-x;}
if(x>9){print(x/10);putchar(x%10+'0');}
else putchar(x+'0');
return;
}
const int N = 1e3 + 10;
int a, b, n, q[N], mp[N][N], lineMAX[N][N], lineMIN[N][N], zongMAX[N][N], zongMIN[N][N], ans = LLONG_MAX;
signed main() {
a = read(), b = read(), n = read();
For(i,1,a) For(j,1,b) mp[i][j] = read();
For(i,1,a) {
int h = 1, t = 0;
For(j,1,b) {
while(h <= t && j - q[h] + 1 > n) h++;
while(h <= t && mp[i][q[t]] < mp[i][j]) t--;
q[++t] = j;
if(j >= n) lineMAX[i][j] = mp[i][q[h]];
}
h = 1, t = 0;
For(j,1,b) {
while(h <= t && j - q[h] + 1 > n) h++;
while(h <= t && mp[i][q[t]] > mp[i][j]) t--;
q[++t] = j;
if(j >= n) lineMIN[i][j] = mp[i][q[h]];
}
}
For(j,n,b) {
int h = 1, t = 0;
For(i,1,a) {
while(h <= t && i - q[h] + 1 > n) h++;
while(h <= t && lineMAX[q[t]][j] < lineMAX[i][j]) t--;
q[++t] = i;
if(i >= n) zongMAX[i][j] = lineMAX[q[h]][j];
}
h = 1, t = 0;
For(i,1,a) {
while(h <= t && i - q[h] + 1 > n) h++;
while(h <= t && lineMIN[q[t]][j] > lineMIN[i][j]) t--;
q[++t] = i;
if(i >= n) zongMIN[i][j] = lineMIN[q[h]][j];
}
}
For(i,n,a) {
For(j,n,b) ans = min(ans, zongMAX[i][j] - zongMIN[i][j]);
}
cout << ans << '\n';
return 0;
}
【算法】单调栈 & 单调队列学习笔记的更多相关文章
- ACM金牌选手讲解LeetCode算法《栈和队列的高级应用》
大家好,我是编程熊,双非逆袭选手,字节跳动.旷视科技前员工,ACM金牌,保研985,<ACM金牌选手讲解LeetCode算法系列>作者. 上一篇文章讲解了<线性表>中的数组.链 ...
- 单调栈&单调队列学习笔记!
ummm,,,都是单调系列就都一起学了算了思想应该都差不多呢qwq 其实感觉这俩没有什么可说的鸭QAQ就是维护一个单调的东西,区别在于单调栈是一段进一段出然后单调队列是一段进另一段出?没了 好趴辣重点 ...
- 单调栈&单调队列入门
单调队列是什么呢?可以直接从问题开始来展开. Poj 2823 给定一个数列,从左至右输出每个长度为m的数列段内的最小数和最大数. 数列长度:\(N <=10^6 ,m<=N\) 解法① ...
- HZNU-ACM寒假集训Day10小结 单调栈-单调队列
数据结构往往可以在不改变主算法的前提下题高运行效率,具体做法可能千差万别,但思路却是有规律可循 经典问题:滑动窗口 单调队列O(n) POJ 2823 我开始写的: TLE 说明STL的库还是有点慢 ...
- 小结:单调栈 & 单调队列
概要: 对于维护信息具有单调性的性质或者问题可以转化为具有单调性质的模型的题,我们可以考虑用单调栈或单调队列. 技巧及注意: 技巧很多,只要能将问题转化为单调性问题,就好解决了. 当维护固定长度的单调 ...
- 单调栈&单调队列
最近打了三场比赛疯狂碰到单调栈和单调队列的题目,第一,二两场每场各一个单调栈,第三场就碰到单调队列了.于是乎就查各种博客,找单调栈,单调队列的模板题去做,搞着搞着发现其实这两个其实是一回事,只不过利用 ...
- [CSP-S模拟测试]:Cover(单调栈++单调队列+DP)
题目传送门(内部题126) 输入格式 第一行两个个整数$n,m$表示区间的长度与彩灯的数量. 接下来$m$行,每行三个整数$l_i,r_i,a_i$表示一条彩灯能够覆盖的区间以及它的美观程度. 输出格 ...
- POJ 3250 Bad Hair Day --单调栈(单调队列?)
维护一个单调栈,保持从大到小的顺序,每次加入一个元素都将其推到尽可能栈底,知道碰到一个比他大的,然后res+=tail,说明这个cow的头可以被前面tail个cow看到.如果中间出现一个超级高的,自然 ...
- 用JS描述的数据结构及算法表示——栈和队列(基础版)
前言:找了上课时数据结构的教程来看,但是用的语言是c++,所以具体实现在网上搜大神的博客来看,我看到的大神们的博客都写得特别好,不止讲了最基本的思想和算法实现,更多的是侧重于实例运用,一边看一边在心里 ...
- Java数据结构和算法之栈与队列
二.栈与队列 1.栈的定义 栈(Stack)是限制仅在表的一端进行插入和删除运算的线性表. (1)通常称插入.删除的这一端为栈顶(Top),另一端称为栈底(Bottom). (2)当表中没有元素时称为 ...
随机推荐
- js给元素设置样式
一.style 利用 "[元素].style.[CSS属性名] = [属性值]" 的方法 1 var Box = document.getElementById('box') 2 ...
- 搭建SpringBoot项目依赖和配置快速篇
maven依赖及一些配置 这里主要是搭建项目常用到的maven依赖以及搭建项目会需要用到的一些配置文件,可能下面这些依赖还不是很全,但是应该会满足日常大部分的需求了 Spring Spring项目的依 ...
- 小米商城主页展示HTML+CSS
大佬们呀,花了好几天的时间总算是看着页面展示可以了,求赐教! 小米商城主页,对大佬来说肯定简单爆了,我抄写了好久呀,总是有一点点的小问题,还搞不明白 主要是一个静态的小米商城页面,HTML前端代码不复 ...
- hibernate及SpringBoot集成Jpa实现对数据库操作
首先使用Maven工程和junit完成hibernate对数据库的简单操作,完成之后在使用SpringBoot集成Jap完成hibernate对数据库的操作.本文仅供新手学习查看,具体线上使用需要对代 ...
- 2022-02-02:最接近的二叉搜索树值 II。 给定一个不为空的二叉搜索树和一个目标值 target,请在该二叉搜索树中找到最接近目标值 target 的 k 个值。 注意: 给定的目标值 ta
2022-02-02:最接近的二叉搜索树值 II. 给定一个不为空的二叉搜索树和一个目标值 target,请在该二叉搜索树中找到最接近目标值 target 的 k 个值. 注意: 给定的目标值 tar ...
- 2021-10-18:乘积最大子数组。给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。力扣152。
2021-10-18:乘积最大子数组.给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积.力扣152. 福大大 答案2021-1 ...
- 用token辅助 密码爆破
第一步:打开皮卡丘,点击暴力破解,token防爆破,输入正确用户名,错误密码 BP拦截请求,点击皮卡丘Login,然后拦截后, 发送给Intruder 第二步: 爆破方式选择音叉方式, & ...
- odoo开发教程四:onchange、唯一性约束
一:onchange机制[onchange=前端js函数!可以实现前端实时更新以及修改验证] onchange机制:不需要保存数据到数据库就可以实时更新用户界面上的显示. @api.onchange( ...
- MQ系列12:如何保证消息顺序性
MQ系列1:消息中间件执行原理 MQ系列2:消息中间件的技术选型 MQ系列3:RocketMQ 架构分析 MQ系列4:NameServer 原理解析 MQ系列5:RocketMQ消息的发送模式 MQ系 ...
- go语言字符与字符串相关
ASCII ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁 字母的一套单字节编码系统 字符 本质上来 ...