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;
}

【算法】单调栈 & 单调队列学习笔记的更多相关文章

  1. ACM金牌选手讲解LeetCode算法《栈和队列的高级应用》

    大家好,我是编程熊,双非逆袭选手,字节跳动.旷视科技前员工,ACM金牌,保研985,<ACM金牌选手讲解LeetCode算法系列>作者. 上一篇文章讲解了<线性表>中的数组.链 ...

  2. 单调栈&单调队列学习笔记!

    ummm,,,都是单调系列就都一起学了算了思想应该都差不多呢qwq 其实感觉这俩没有什么可说的鸭QAQ就是维护一个单调的东西,区别在于单调栈是一段进一段出然后单调队列是一段进另一段出?没了 好趴辣重点 ...

  3. 单调栈&单调队列入门

    单调队列是什么呢?可以直接从问题开始来展开. Poj 2823 给定一个数列,从左至右输出每个长度为m的数列段内的最小数和最大数. 数列长度:\(N <=10^6 ,m<=N\) 解法① ...

  4. HZNU-ACM寒假集训Day10小结 单调栈-单调队列

    数据结构往往可以在不改变主算法的前提下题高运行效率,具体做法可能千差万别,但思路却是有规律可循 经典问题:滑动窗口  单调队列O(n) POJ 2823 我开始写的: TLE 说明STL的库还是有点慢 ...

  5. 小结:单调栈 & 单调队列

    概要: 对于维护信息具有单调性的性质或者问题可以转化为具有单调性质的模型的题,我们可以考虑用单调栈或单调队列. 技巧及注意: 技巧很多,只要能将问题转化为单调性问题,就好解决了. 当维护固定长度的单调 ...

  6. 单调栈&单调队列

    最近打了三场比赛疯狂碰到单调栈和单调队列的题目,第一,二两场每场各一个单调栈,第三场就碰到单调队列了.于是乎就查各种博客,找单调栈,单调队列的模板题去做,搞着搞着发现其实这两个其实是一回事,只不过利用 ...

  7. [CSP-S模拟测试]:Cover(单调栈++单调队列+DP)

    题目传送门(内部题126) 输入格式 第一行两个个整数$n,m$表示区间的长度与彩灯的数量. 接下来$m$行,每行三个整数$l_i,r_i,a_i$表示一条彩灯能够覆盖的区间以及它的美观程度. 输出格 ...

  8. POJ 3250 Bad Hair Day --单调栈(单调队列?)

    维护一个单调栈,保持从大到小的顺序,每次加入一个元素都将其推到尽可能栈底,知道碰到一个比他大的,然后res+=tail,说明这个cow的头可以被前面tail个cow看到.如果中间出现一个超级高的,自然 ...

  9. 用JS描述的数据结构及算法表示——栈和队列(基础版)

    前言:找了上课时数据结构的教程来看,但是用的语言是c++,所以具体实现在网上搜大神的博客来看,我看到的大神们的博客都写得特别好,不止讲了最基本的思想和算法实现,更多的是侧重于实例运用,一边看一边在心里 ...

  10. Java数据结构和算法之栈与队列

    二.栈与队列 1.栈的定义 栈(Stack)是限制仅在表的一端进行插入和删除运算的线性表. (1)通常称插入.删除的这一端为栈顶(Top),另一端称为栈底(Bottom). (2)当表中没有元素时称为 ...

随机推荐

  1. Redis之消息队列实现

    文章目录 秒杀场景 采用消息队列实现 List实现消息队列 PubSub(发布订阅)实现消息队列 基于Stream实现消息队列 消费者组 实践 总结 秒杀问题是非常重要且比较难实现的,如果不进行架构的 ...

  2. Stream方法的介绍

    文章目录 前言 Lambda表达式 格式 函数式接口 Stream的方法介绍 forEach filter collect count sum limit 和skip groupingBy reduc ...

  3. 2020-11-16:手写代码:leetcode第406题。假设有打乱顺序的一群人站成一个队列。 每个人由一个整数对(h, k)表示,其中h是这个人的身高,k是排在这个人前面且身高大于或等于h的人数。 编写一个算法来重建这个队列。

    福哥答案2020-11-16: ①排序.按照[身高]降序排列.如果[身高]一样,按照[人数]升序排列.②插入.遍历这个队列,按照[人数]插入相应位置. 采用leetcode里的代码,golang代码如 ...

  4. 2020-06-11-ASP.NET Core Blazor 子组件父组件数据同步的问题

    上一篇写数据绑定的文章,写到最后留了一个坑.当子组件绑定父组件的一个字段,并且子组件修改它的时候父组件不能实时进行同步更新UI的问题,最近终于在Blazui作者的指导下搞定了. UserInfo类要实 ...

  5. 7-8 估值一亿的AI核心代码

    题目描述: 以上图片来自新浪微博. 本题要求你实现一个稍微更值钱一点的 AI 英文问答程序,规则是: 无论用户说什么,首先把对方说的话在一行中原样打印出来: 消除原文中多余空格:把相邻单词间的多个空格 ...

  6. flutter 填坑之旅(dart学习笔记篇)

    俗话说 '工欲善其事必先利其器' 想要撸flutter app 而不懂 dart 那就像一个不会英语的人在和英国人交流,懵! 安装 dart 就不用说了,比较简单dart 官网 https://dar ...

  7. adb查看端口号,杀进程

    1.先查看端口号占用的进程 netstat -ano | findstr 8000 2.在杀掉我们查出的进程15812 3.再次查看8000端口号的进程

  8. Spring Boot 3.1中如何整合Spring Security和Keycloak

    在今年2月14日的时候,Keycloak 团队宣布他们正在弃用大多数 Keycloak 适配器.其中包括Spring Security和Spring Boot的适配器,这意味着今后Keycloak团队 ...

  9. 【踩坑记录】字节流数据按照string的方式读取然后按照string的方案存储,编码导致二进制数据发生变化,原理记录

    ​ 目录 问题缘由 背后原理 C#代码示例 总结 问题缘由 由于公司需求,需要读取游戏Redis数据做内外网数据迁移,没有与游戏组过多的沟通.  使用的数据类型是Hash, key是string,va ...

  10. 如何通过CAD图中的坐标来确定是哪个坐标系

    国内常见的坐标系 坐标系分为以下两种: 地理坐标系(Geographic Coordinate System, GCS) 投影坐标系(Projected Coordinate System, PCS) ...