【算法】单调栈 & 单调队列
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\),求
\(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;
}