单调结构 / 基础
单调队列:滑动窗口最值
用双端队列只保留仍在窗口内、且可能成为答案的候选下标,在线性时间内求出所有滑动窗口最大值。
- 复杂度
- 时间 O(n),空间 O(k)
- 阅读
- 2 分钟
- 前置知识
- 队列与双端队列、滑动窗口、单调栈
- 单调队列
- 滑动窗口
- 区间最值
- C++17
问题#
给定长度为 的数组和窗口长度 。窗口从左到右每次移动一格,求每个长度为 的窗口中的最大值。
逐个窗口扫描需要 时间。我们真正需要保存的并不是窗口中的所有元素,而是 仍有机会成为当前或未来最大值的候选。
核心思想#
双端队列中保存候选元素的下标。处理位置 时依次完成:
- 从队首删除已经离开窗口的下标。
- 从队尾删除值小于等于 的下标。
- 把 放到队尾。
- 当第一个完整窗口形成后,队首就是窗口最大值的下标。
第二步为何安全?若旧下标 且 ,那么 更晚离开窗口,值又 不小于 。从现在开始, 永远不可能比 更适合作为最大值。
不变量#
- 队列中的下标严格递增。
- 队列中的下标都位于当前窗口。
- 队列中对应的值严格递减,因此队首始终是最大值。
模板在值相等时删除旧下标、保留新下标。这样不会改变最大值,并能让候选存活更久。
复杂度#
每个下标最多从队尾进入一次,并从队首或队尾离开一次:
- 时间:
- 额外空间:
可独立编译的 C++17 模板#
输入第一行是 ,第二行是数组。约定 ;不满足时程序不输出结果。
#include <cstddef>
#include <deque>
#include <iostream>
#include <vector>
std::vector<long long> sliding_window_maximum(
const std::vector<long long>& values,
int window_size
) {
const int n = static_cast<int>(values.size());
if (window_size <= 0 || window_size > n) {
return {};
}
std::deque<int> candidates;
std::vector<long long> answer;
answer.reserve(
static_cast<std::size_t>(n - window_size + 1)
);
for (int i = 0; i < n; ++i) {
const int first_valid = i - window_size + 1;
while (
!candidates.empty() &&
candidates.front() < first_valid
) {
candidates.pop_front();
}
while (
!candidates.empty() &&
values[static_cast<std::size_t>(candidates.back())] <=
values[static_cast<std::size_t>(i)]
) {
candidates.pop_back();
}
candidates.push_back(i);
if (i + 1 >= window_size) {
answer.push_back(
values[static_cast<std::size_t>(candidates.front())]
);
}
}
return answer;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
int n = 0;
int window_size = 0;
std::cin >> n >> window_size;
if (n < 0) {
return 0;
}
std::vector<long long> values(static_cast<std::size_t>(n));
for (long long& value : values) {
std::cin >> value;
}
const std::vector<long long> answer =
sliding_window_maximum(values, window_size);
for (std::size_t i = 0; i < answer.size(); ++i) {
if (i != 0) {
std::cout << ' ';
}
std::cout << answer[i];
}
std::cout << '\n';
}
示例#
输入:
8 3
1 3 -1 -3 5 3 6 7
输出:
3 3 5 5 6 7
例如窗口 形成时,旧候选 都会从队尾删除,队首直接变成 5。
易错点#
- 队列中保存值:仅保存值无法判断它何时离开窗口,必须保存下标。
- 过期条件差一位:对当前右端 ,合法窗口左端是 ,下标小于它才 过期。
- 过早输出:只有 时才形成完整窗口。
- 重复值处理不一致:弹出小于等于当前值会保留更新的下标;只弹出小于也正确,但 队列可能保留更多相等元素。
- 最大值与最小值方向写反:求最大值维护递减队列,求最小值维护递增队列。
- 把双端队列误写成普通队列:候选既可能从队首过期,也可能从队尾被淘汰。
边界测试#
- :答案应与原数组相同。
- :只输出全局最大值。
- 严格递增、严格递减、全部相等。
- 最大值重复出现,旧最大值恰好离开窗口。
- 全负数与单元素数组。
- 非法 或 是否按接口约定处理。
- 随机短数组,与每个窗口直接调用最大值函数的 实现对拍。
变体#
- 滑动窗口最小值:把队尾比较改为大于等于当前值。
- 同时求最大和最小:分别维护一个递减队列与一个递增队列。
- 不定长窗口:只要窗口左端单调右移,仍可按相同规则删除过期下标。
- DP 优化:当转移只依赖一个滑动区间内的最优值时,单调队列可把一次转移从 降到均摊 。
- 延伸阅读(非课程主线):二维滑动窗口:先对每行做一次,再对中间结果的每列 做一次。