1 单调队列
单调队列: 在原有的队列的基础上,使队列内元素维持一个 单调(递减或递增) 的特性。
单调队列的实现与单调栈类似,在一些情况下,也可以互相替换。但单调队列在维护单调性的同时,也可以维护区间长度。
单调队列需要头尾都可进可出,使用双端队列(deque)。
以单调递增队列为例:
从左往右遍历数组,如果队尾元素大于等于当前元素,则将队尾元素出队,知道队列为空或小于为止。
判断队头元素是否要更新,如果已超出区间长度,队头出队。
把当前元素从队尾入队。
此时队头元素即为以当前元素为右端点的区间内最小值。
注意:入队的是元素索引而非元素值。
如5 1 3 2 6
,区间长度为
2
2
2,它的操作过程如下:
5
队尾入队(区间长度不足 2 2 2,标记0
)5
队尾出队,1
队尾入队(标记队头1
的索引2
)3
队尾入队(标记队头1
的索引2
)3
队尾出队,1
过期队头出队,2
队尾入队(标记队头2
的索引4
)6
队尾入队(标记队头2
的索引4
)
得到结果0 2 2 4 4
。
代码如下:
for (int i = 1; i <= n; i++) {
while (!dq.empty() && a[dq.back()] >= a[i])
dq.pop_back();
if (!dq.empty() && i - dq.front() + 1 > k)
dq.pop_front();
dq.push_back(i);
if (i >= k)
ans[i] = dq.front();
}
具体写法视题目而定。
2 例题
2.1 滑动窗口
题目描述
有一个长为 n n n 的序列 a a a,以及一个大小为 k k k 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
题解
经典模板。代码如下:
#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
using namespace std;
typedef long long ll;
const int N = 1e6 + 10;
int n, k, a[N], minn[N], maxn[N];
deque<int> dq;
int main() {
CLOSE;
cin >> n >> k;
for (int i = 1; i <= n; i++)
cin >> a[i];
for (int i = 1; i <= n; i++) {
while (!dq.empty() && a[dq.back()] >= a[i])
dq.pop_back();
if (!dq.empty() && i - dq.front() + 1 > k)
dq.pop_front();
dq.push_back(i);
if (i >= k)
cout << a[dq.front()] << " ";
}
cout << endl;
while (!dq.empty())
dq.pop_back();
for (int i = 1; i <= n; i++) {
while (!dq.empty() && a[dq.back()] <= a[i])
dq.pop_back();
if (!dq.empty() && i - dq.front() + 1 > k)
dq.pop_front();
dq.push_back(i);
if (i >= k)
cout << a[dq.front()] << " ";
}
return 0;
}
2.2 [HAOI2007] 理想的正方形
题目描述
有一个 a × b a \times b a×b 的整数组成的矩阵,现请你从中找出一个 n × n n \times n n×n 的正方形区域,使得该区域所有数中的最大值和最小值的差最小。
题解
与 单调栈及应用 2.4 所有1的最大矩阵 很相似。
1
2
5
6
0
17
16
0
16
17
2
1
2
10
2
1
1
2
2
2
\def\arraystretch{1.2} \begin{array}{|c|c|c|c|}\hline 1&2 & 5 & 6 \\ \hline 0&17 &16 &0 \\ \hline 16&17 &2 &1 \\ \hline 2&10 & 2 &1 \\ \hline 1& 2& 2 &2 \\ \hline \end{array}
1016212171710251622260112
以样例求最小值为例:
- 对每一行从左向右进行单调队列(区间长度为 n n n)求最小值,得到矩阵
0 1 2 5 0 0 16 0 0 16 2 1 0 2 2 1 0 1 2 2 \def\arraystretch{1.2} \begin{array}{|c|c|c|c|}\hline 0&1 & 2 & 5 \\ \hline 0&0 &16 &0 \\ \hline 0&16 &2 &1 \\ \hline 0&2 & 2 &1 \\ \hline 0&1 & 2 &2 \\ \hline \end{array} 0000010162121622250112
- 再对每一列从上往下进行单调队列(区间长度为 n n n)求最小值,得到矩阵
0 0 0 0 0 0 2 0 0 0 2 0 0 2 2 1 0 1 2 1 \def\arraystretch{1.2} \begin{array}{|c|c|c|c|}\hline 0&0 &0 &0 \\ \hline 0&\color{blue}\bf0 &\color{blue}\bf2 &\color{blue}\bf0 \\ \hline 0&\color{blue}\bf0 &\color{blue}\bf2 &\color{blue}\bf0 \\ \hline 0&\color{blue}\bf2 &\color{blue}\bf2 &\color{blue}\bf1 \\ \hline 0&\color{blue}\bf1 &\color{blue}\bf2 &\color{blue}\bf1 \\ \hline \end{array} 00000000210222200011
标蓝色的位置即为以此构成的 n × n n\times n n×n 方阵的右下角,它的值是这个方阵内数字的最小值。
同理,重复以上步骤找出最大值矩阵,每个 n × n n\times n n×n 矩阵的最大值也在蓝色位置上。
不难发现,蓝色数字在第 n ∼ a n\sim a n∼a 行、 n ∼ b n\sim b n∼b 列,因此,遍历这个范围内的数,找到最大值和最小值差最小的数即可。
代码如下:
#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const ll N = 1010;
ll n, m, k, p[N][N], a[N], minn[N], maxn[N], ans = INT_MAX;
ll Min[N][N], Max[N][N];
deque<ll> dq;
void solve(ll f) {
memset(minn, 0, sizeof minn);
memset(maxn, 0, sizeof maxn);
dq.clear();
for (ll i = 1; i <= f; i++) {
while (!dq.empty() && a[dq.back()] >= a[i])
dq.pop_back();
if (!dq.empty() && i - dq.front() + 1 > k)
dq.pop_front();
dq.push_back(i);
minn[i] = dq.front();
}
dq.clear();
for (ll i = 1; i <= f; i++) {
while (!dq.empty() && a[dq.back()] <= a[i])
dq.pop_back();
if (!dq.empty() && i - dq.front() + 1 > k)
dq.pop_front();
dq.push_back(i);
maxn[i] = dq.front();
}
}
int main() {
CLOSE;
cin >> n >> m >> k;
for (ll i = 1; i <= n; i++)
for (ll j = 1; j <= m; j++)
cin >> p[i][j];
for (ll i = 1; i <= n; i++) { //每一行单调队列
for (ll j = 1; j <= m; j++)
a[j] = p[i][j];
solve(m);
for (ll j = 1; j <= m; j++)
Min[i][j] = a[minn[j]], Max[i][j] = a[maxn[j]];
}
for (ll i = 1; i <= m; i++) { //每一列单调队列
for (ll j = 1; j <= n; j++) //找最小值矩阵
a[j] = Min[j][i];
solve(n);
for (ll j = 1; j <= n; j++)
Min[j][i] = a[minn[j]];
for (ll j = 1; j <= n; j++) //找最大值矩阵
a[j] = Max[j][i];
solve(n);
for (ll j = 1; j <= n; j++)
Max[j][i] = a[maxn[j]];
}
for (ll i = k; i <= n; i++)
for (ll j = k; j <= m; j++)
ans = min(ans, Max[i][j] - Min[i][j]);
cout << ans;
return 0;
}
2.3 平衡播放列表
题目描述
n n n 首歌循环播放,每首歌都有一个评分 x x x,听歌时时刻保持已听歌曲的评分最大值 x max x_{\max} xmax,一旦听到的歌曲小于当前的 x max 2 \frac{x_{\max}}{2} 2xmax,就会产生负面情绪,立刻停止播放。问以第 i i i 首歌为起点,将会完整听完几首歌曲。输出每个 i ( 1 ≤ i ≤ n ) i(1\le i\le n) i(1≤i≤n) 的完整听歌数量(若不会停止,输出 − 1 -1 −1)。
题解
遇到环的问题,一般拆解成链,通常是环的 2 2 2 倍。但在本题中,不难发现,需要重复 3 3 3 次。
显然,当歌曲里的最大值仍比最小值的两倍小时,无论从哪首歌开始,都不会停止。
设 f i f_i fi 表示从第 i i i 首歌开始听,还可以听几首。则 f i − i f_i - i fi−i 即为所求的结果。
f i ≤ f i + 1 f_i\le f_{i+1} fi≤fi+1,因为如果 i + 1 i + 1 i+1 在 f i f_i fi 之前停止,那么 i i i 一定也可以在那首歌停止,那么 f i = f i + 1 f_i=f_{i+1} fi=fi+1,所以 f f f 序列必然是递增的。
4
11 5 2 7
观察样例,得到它的 f f f 序列:
11 5 2 7 11 5 2 7 11 5 2 7
2 2 6 6 6 7 10 10 10 11 11 11
观察发现,大部分的 f i f_i fi 是重复的,我们只要抓住转折点即可。
单调队列找最大值,如果当前元素的两倍小于队头,则队头标记并弹出。最后将剩余的位置填上即可。
代码如下:
#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE \
ios::sync_with_stdio(false); \
cin.tie(0); \
cout.tie(0)
using namespace std;
typedef long long ll;
const int mod = 1e9 + 7;
const int N = 1e5 + 10;
ll n, m, t;
deque<ll> q;
ll arr[N], ans[N], maxx = -1, minn = 1e9;
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> arr[i];
arr[i + n] = arr[i + 2 * n] = arr[i];
maxx = max(maxx, arr[i]);
minn = min(minn, arr[i]);
}
if ((minn * 2) >= maxx) {
for (int i = 1; i <= n; i++)
cout << "-1 ";
return 0;
}
for (int i = 1; i <= 3 * n; i++) {
while (!q.empty() && arr[q.back()] <= arr[i])
q.pop_back(); // 维护最大值,单调递减区间
q.push_back(i);
while (!q.empty() && arr[q.front()] > arr[i] * 2) {
ans[q.front()] = i - q.front();
q.pop_front();
}
}
for (int i = 3 * n; i >= 1; i--)
if (!ans[i])
ans[i] = ans[i + 1] + 1;
for (int i = 1; i <= n; i++)
cout << ans[i] << " ";
return 0;
}