单调队列及应用

1 单调队列

单调队列: 在原有的队列的基础上,使队列内元素维持一个 单调(递减或递增) 的特性。

单调队列的实现与单调栈类似,在一些情况下,也可以互相替换。但单调队列在维护单调性的同时,也可以维护区间长度

单调队列需要头尾都可进可出,使用双端队列(deque)

单调递增队列为例:

从左往右遍历数组,如果队尾元素大于等于当前元素,则将队尾元素出队,知道队列为空或小于为止。

判断队头元素是否要更新,如果已超出区间长度,队头出队。

把当前元素从队尾入队。

此时队头元素即为以当前元素为右端点的区间内最小值。

注意:入队的是元素索引而非元素值。

5 1 3 2 6,区间长度为 2 2 2,它的操作过程如下:

  1. 5队尾入队(区间长度不足 2 2 2,标记0
  2. 5队尾出队,1队尾入队(标记队头1的索引2
  3. 3队尾入队(标记队头1的索引2
  4. 3队尾出队,1过期队头出队,2队尾入队(标记队头2的索引4
  5. 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 滑动窗口

P1886 滑动窗口 /【模板】单调队列 - 洛谷

题目描述

有一个长为 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] 理想的正方形

P2216 [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
以样例求最小值为例:

  1. 每一行从左向右进行单调队列(区间长度为 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

  1. 再对每一列从上往下进行单调队列(区间长度为 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 na 行、 n ∼ b n\sim b nb 列,因此,遍历这个范围内的数,找到最大值和最小值差最小的数即可。

代码如下:

#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(1in) 的完整听歌数量(若不会停止,输出 − 1 -1 1)。

题解

遇到环的问题,一般拆解成链,通常是环的 2 2 2 倍。但在本题中,不难发现,需要重复 3 3 3 次。

显然,当歌曲里的最大值仍比最小值的两倍小时,无论从哪首歌开始,都不会停止。

f i f_i fi 表示从第 i i i 首歌开始听,还可以听几首。则 f i − i f_i - i fii 即为所求的结果。

f i ≤ f i + 1 f_i\le f_{i+1} fifi+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;
}
  • 18
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值