单调队列
算法笔记:单调队列与滑动窗口进阶
单调队列的核心在于“如果在你之后入队的人比你强(或者更符合条件),那你就永远没机会了”。通过维护一个双端队列(deque),存储原数组的下标,并保持队列内部对应元素值的单调性,可以实现 $O(n)$ 复杂度下的区间最值查询。
级别一:定长滑动窗口(基础模板)
对应文件:P_1886_模板_单调队列_滑动窗口.cpp
-
🏷️ 题面抽象:给定一个长度为 $n$ 的数组和一个固定大小为 $k$ 的滑动窗口。窗口从左向右滑动,求每次滑动时窗口内的最大值和最小值。
-
💡 核心思路:
-
维护单调性:以求最小值为例,维护一个单调递增队列。遍历时,若队尾元素 $\ge$ 当前元素,则不断弹出队尾,然后将当前元素的下标入队。
-
剔除过期元素:若队头下标 $< i - k + 1$,说明队头元素已经离开窗口,从队头弹出。
-
记录答案:当遍历索引 $i \ge k$ 时,队头元素对应的就是当前窗口的最值。
-
⚠️ 细节提醒:队列中务必存下标而不是具体的值,否则无法判断元素是否已经滑出窗口边界。
点击查看代码
void solve()
{
int n, k;
cin >> n >> k;
deque<int> q; // 存的下标
vi a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
deque<int> mnq;
for (int i = 1; i <= n; ++i)
{
while (mnq.size() && a[mnq.back()] > a[i])
{
mnq.pop_back();
}
mnq.push_back(i);
if (!mnq.empty() && mnq.front() < i - k + 1)
mnq.pop_front();
int front = a[mnq.front()];
if (i >= k)
cout << front << ' ';
}
cout << endl;
for (int i = 1; i <= n; ++i)
{
while (q.size() && a[q.back()] < a[i])
{
q.pop_back();
}
q.push_back(i);
if (!q.empty() && q.front() < i - k + 1) // 注意判断 q 不为空
q.pop_front();
int front = a[q.front()];
if (i >= k)
cout << front << ' ';
}
cout << endl;
}
级别二:定长滑动窗口 + 前缀和转化
对应文件:U_192528_最大子序和.cpp
-
🏷️ 题面抽象:在长度为 $n$ 的序列中,找出一个长度不超过 $m$ 的连续子序列,使其元素总和最大。
-
💡 核心思路:
-
区间和问题优先转化为前缀和:子序列 $[j, i]$ 的和为 $S_i - S_{j-1}$。
-
要使 $S_i - S_{j-1}$ 最大,对于固定的右端点 $i$,我们需要在合法的窗口范围 $[i-m, i-1]$ 内,找到一个最小的 $S_{j-1}$。
-
这本质上退化成了级别一的问题:用单调递增队列维护前缀和数组在长度为 $m$ 的滑动窗口内的最小值。
-
⚠️ 细节提醒:
-
入队时机:当遍历到 $i$ 时,准备入队的是 $i-1$ 的状态。
-
越界判断:合法区间是 $[i-m, i-1]$,因此过期的队头条件是下标 $< i-m$。
点击查看代码
// 输入一个长度为 n 的整数序列,
// 从中找出一段长度不超过 m 的连续子序列,使得子序列中所有数的和
void solve() {
int n, m;
cin >> n >> m;
vi a(n + 1);
for (int i = 1; i <= n; i++) {
cin >> a[i];
a[i] += a[i - 1];
}
// 对每个 i 找到 (i - m) 到 (i - 1) 这个滑动窗口的最小值
int mx = -1e18;
deque<int> q; // 单调递增序列
for (int i = 1; i <= n; i++) {
while (!q.empty() && a[q.back()] > a[i - 1]) {
q.pop_back();
}
q.push_back(i - 1);
if (!q.empty() && q.front() < i - m) {
q.pop_front();
}
int sj = a[q.front()];
int si = a[i];
// cerr << 1 << endl;
mx = max(si - sj, mx);
}
cout << mx << endl;
}
级别三:动态窗口求“最长”满足条件的区间
对应文件:P_3512_POI_2010_PIL_Pilots.cpp
- 🏷️ 题面抽象:求最长的连续子段,使得该子段内的“最大值与最小值之差”不超过 $k$。
- 💡 核心思路:
- 窗口大小不再固定,属于动态滑动窗口(尺取法)。
- 需要两个单调队列同时维护当前窗口内的最大值和最小值。
- 扩张与收缩:维护左端点 $l$。右端点 $i$ 不断向右扩张并入队;如果此时刻极差
max_val - min_val > k,说明当前窗口不合法,必须将左端点 $l$ 向右收缩(l++),同时弹出所有下标 $< l$ 的队头元素,直到窗口重新合法。 - 在每次确认窗口合法后,使用 $i - l + 1$ 更新最长长度答案。
点击查看代码
//题意 给定 n,k 和一个长度为 n 的序列,求最长的最大值最小值相差不超过 k 的子段
//求窗口长度的最大值
void solve()
{
int k, n;
cin >> k >> n;
vi a(n + 1);
for (int i = 1; i <= n; i++)
cin >> a[i];
deque<int> mxq;
deque<int> mnq;
int ans = -1;
int l = 1;//要维护左端点
for (int i = 1; i <= n; i++)
{
while (mnq.size() && a[mnq.back()] > a[i])
mnq.pop_back();
mnq.push_back(i);
while (mxq.size() && a[mxq.back()] < a[i])
mxq.pop_back();
mxq.push_back(i);
// 动态区间窗口 要用 while维护
//必须要写在 i 加入后我们模拟时就是加入之后
//直接判断的要符合模拟的
while (mxq.size() && mnq.size() && a[mxq.front()] - a[mnq.front()] > k)
{
l++;
while (mxq.size() && mxq.front() < l)
mxq.pop_front();
while (mnq.size() && mnq.front() < l)
mnq.pop_front();
}
ans = max(ans, i - l + 1);
}
cout << ans << endl;
}
级别四:动态窗口求“最短”满足条件的区间(二维坐标轴)
对应文件:P_2698_USACO_12_MAR_Flowerpot_S.cpp
-
🏷️ 题面抽象:给定二维平面上的 $n$ 个点 $(x, y)$,求一个最小的宽度(即两点 $x$ 坐标的差值),使得在该宽度范围内的点,其 $y$ 坐标的极差 $\ge d$。
-
💡 核心思路:
-
降维排序:先将所有点按照 $x$ 坐标升序排序,将问题转化为一维序列上的滑动窗口。
-
依然使用双单调队列维护窗口内 $y$ 坐标的最大值和最小值。
-
满足条件即收缩:与级别三逻辑相反。当 $i$ 向右扩张且发现极差 $\ge d$ 时,说明已经找到了一个合法解。此时记录 $x_i - x_l$ 更新最小宽度答案,然后尝试将左端点 $l$ 右移(收缩窗口)来寻找看看有没有更短的合法宽度,并同步清理过期队头。
-
⚠️ 细节提醒:
-
判断合法并收缩的操作一定要写在
while循环里,因为左端点 $l$ 可能可以连续向右移动多次且依然满足极差 $\ge d$。 -
如果没有找到任何合法解,注意根据初始化的边界值(如
2e18)输出-1。
点击查看代码
// 滑动窗口求最值 单调队列
//求窗口长度的最小值
//把判断写在while里面
void solve()
{
int n, d;
cin >> n >> d;
vector<pair<int, int>> a(n + 1);
for (int i = 1; i <= n; i++)
{
cin >> a[i].first >> a[i].second;
}
sort(a.begin() + 1, a.end());
deque<int> mxq; // 存的是下标
deque<int> mnq;
int l = 1;
int ans = 2e18;
for (int i = 1; i <= n; i++)
{
while (mxq.size() && a[i].second < a[mxq.back()].second)
{
mxq.pop_back();
}
mxq.push_back(i);
while (mnq.size() && a[i].second > a[mnq.back()].second)
mnq.pop_back();
mnq.push_back(i);
//窗口的len 最小值这样写没问题
while (mxq.size() && mnq.size() && a[mnq.front()].second - a[mxq.front()].second >= d)
{
int len = abs(a[i].first - a[l].first);
ans = min(len, ans);
l++;
while (mxq.size() && mxq.front() < l)
mxq.pop_front();
while (mnq.size() && mnq.front() < l)
mnq.pop_front();
}
}
if (ans == 2e18)
{
cout << -1 << endl;
}
else
cout << ans;
}

浙公网安备 33010602011771号