单调队列
单调队列是一种特殊的双端队列,队列中的元素保持单调性(递增或递减)。它常用于解决滑动窗口问题,能够在O(1)时间内获取窗口内的最值,总体时间复杂度为O(n)。单调队列是动态规划优化和区间查询问题的重要工具。
单调队列基础
单调队列维护队列中元素的单调性,通过双端队列实现,支持队首队尾的插入和删除操作。
单调队列基础实现 - 滑动窗口最大值
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
using namespace std;
class MonotonicQueue {
private:
deque<int> dq; // 存储数组索引
vector<int> arr;
public:
MonotonicQueue(vector<int>& array) : arr(array) {}
// 维护单调递减队列(队首元素最大)
void push(int index) {
// 移除队尾所有小于等于当前元素的索引
while (!dq.empty() && arr[dq.back()] <= arr[index]) {
dq.pop_back();
}
dq.push_back(index);
}
// 如果队首元素已经不在窗口内,则移除
void pop(int index) {
if (!dq.empty() && dq.front() == index) {
dq.pop_front();
}
}
// 获取队列中的最大值
int getMax() {
return dq.empty() ? -1 : arr[dq.front()];
}
// 获取队列中的最小值(需要维护单调递增队列)
int getMin() {
return dq.empty() ? -1 : arr[dq.back()];
}
bool empty() {
return dq.empty();
}
};
// 滑动窗口最大值问题
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> dq;
for (int i = 0; i < nums.size(); i++) {
// 移除超出窗口的元素
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 维护单调性
while (!dq.empty() && nums[dq.back()] <= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
// 如果窗口大小达到k,记录最大值
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
// 滑动窗口最小值问题
vector<int> minSlidingWindow(vector<int>& nums, int k) {
vector<int> result;
deque<int> dq;
for (int i = 0; i < nums.size(); i++) {
// 移除超出窗口的元素
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
// 维护单调递增性
while (!dq.empty() && nums[dq.back()] >= nums[i]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) {
result.push_back(nums[dq.front()]);
}
}
return result;
}
// 双端单调队列 - 同时维护最大值和最小值
class DualMonotonicQueue {
private:
deque<int> maxDq; // 单调递减队列
deque<int> minDq; // 单调递增队列
vector<int> arr;
public:
DualMonotonicQueue(vector<int>& array) : arr(array) {}
void push(int index) {
// 维护最大值队列
while (!maxDq.empty() && arr[maxDq.back()] <= arr[index]) {
maxDq.pop_back();
}
maxDq.push_back(index);
// 维护最小值队列
while (!minDq.empty() && arr[minDq.back()] >= arr[index]) {
minDq.pop_back();
}
minDq.push_back(index);
}
void pop(int index) {
if (!maxDq.empty() && maxDq.front() == index) {
maxDq.pop_front();
}
if (!minDq.empty() && minDq.front() == index) {
minDq.pop_front();
}
}
int getMax() {
return maxDq.empty() ? -1 : arr[maxDq.front()];
}
int getMin() {
return minDq.empty() ? -1 : arr[minDq.front()];
}
int getRange() {
if (maxDq.empty() || minDq.empty()) return 0;
return arr[maxDq.front()] - arr[minDq.front()];
}
};
int main() {
vector<int> nums = {1, 3, -1, -3, 5, 3, 6, 7};
int k = 3;
cout << "原数组: ";
for (int x : nums) cout << x << " ";
cout << "\n窗口大小: " << k << endl;
// 测试滑动窗口最大值
vector<int> maxResult = maxSlidingWindow(nums, k);
cout << "\n滑动窗口最大值: ";
for (int x : maxResult) cout << x << " ";
cout << endl;
// 测试滑动窗口最小值
vector<int> minResult = minSlidingWindow(nums, k);
cout << "滑动窗口最小值: ";
for (int x : minResult) cout << x << " ";
cout << endl;
// 测试双端单调队列
cout << "\n双端单调队列测试:\n";
DualMonotonicQueue dmq(nums);
cout << "窗口\t最大值\t最小值\t极差" << endl;
for (int i = 0; i < nums.size(); i++) {
dmq.push(i);
// 移除超出窗口的元素
if (i >= k) {
dmq.pop(i - k);
}
if (i >= k - 1) {
cout << "[" << (i - k + 1) << "," << i << "]\t"
<< dmq.getMax() << "\t" << dmq.getMin() << "\t" << dmq.getRange() << endl;
}
}
return 0;
}单调队列通过维护队列中元素的单调性来快速获取区间最值。队列存储数组索引而非值,方便处理窗口边界。双端单调队列同时维护最大值和最小值队列。
单调队列优化动态规划
单调队列常用于优化动态规划中的状态转移,将O(nk)的复杂度优化到O(n)。
单调队列优化DP - 最大子序列和
#include <iostream>
#include <vector>
#include <deque>
#include <algorithm>
using namespace std;
// 长度限制的最大子数组和
class ConstrainedSubsequenceSum {
public:
int constrainedSubsetSum(vector<int>& nums, int k) {
int n = nums.size();
vector<int> dp(n);
deque<int> dq; // 按dp值递减顺序存储索引
for (int i = 0; i < n; i++) {
// 移除约束范围外的索引
while (!dq.empty() && dq.front() < i - k) {
dq.pop_front();
}
// dp[i] = nums[i] + max(0, max(dp[j] for j in [i-k, i-1]))
dp[i] = nums[i];
if (!dq.empty()) {
dp[i] = max(dp[i], nums[i] + dp[dq.front()]);
}
// 维护递减顺序,只保留正的dp值
while (!dq.empty() && dp[dq.back()] <= dp[i]) {
dq.pop_back();
}
if (dp[i] > 0) {
dq.push_back(i);
}
}
return *max_element(dp.begin(), dp.end());
}
};
// 和至少为K的最短子数组
class ShortestSubarrayWithSumK {
public:
int shortestSubarray(vector<int>& nums, int k) {
int n = nums.size();
vector<long long> prefixSum(n + 1, 0);
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
deque<int> dq; // 维护前缀和的递增顺序
int minLen = INT_MAX;
for (int i = 0; i <= n; i++) {
// 检查当前前缀和是否可以形成有效子数组
while (!dq.empty() && prefixSum[i] - prefixSum[dq.front()] >= k) {
minLen = min(minLen, i - dq.front());
dq.pop_front();
}
// 维护前缀和的递增顺序
while (!dq.empty() && prefixSum[dq.back()] >= prefixSum[i]) {
dq.pop_back();
}
dq.push_back(i);
}
return minLen == INT_MAX ? -1 : minLen;
}
};
// 跳跃游戏优化
class JumpGameOptimization {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if (n <= 1) return 0;
vector<int> dp(n, INT_MAX);
dp[0] = 0;
deque<int> dq;
dq.push_back(0);
for (int i = 1; i < n; i++) {
// 移除无法到达当前位置的位置
while (!dq.empty() && dq.front() + nums[dq.front()] < i) {
dq.pop_front();
}
if (!dq.empty()) {
dp[i] = dp[dq.front()] + 1;
}
// 按dp值顺序维护位置
while (!dq.empty() && dp[dq.back()] >= dp[i]) {
dq.pop_back();
}
dq.push_back(i);
}
return dp[n - 1];
}
};
// 使用单调队列的直方图最大矩形
class LargestRectangleHistogram {
public:
int largestRectangleArea(vector<int>& heights) {
deque<int> dq; // 维护递增高度
int maxArea = 0;
for (int i = 0; i <= heights.size(); i++) {
int currentHeight = (i == heights.size()) ? 0 : heights[i];
while (!dq.empty() && heights[dq.back()] > currentHeight) {
int height = heights[dq.back()];
dq.pop_back();
int width = dq.empty() ? i : i - dq.front() - 1;
maxArea = max(maxArea, height * width);
}
dq.push_back(i);
}
return maxArea;
}
};
int main() {
// 测试约束子序列和
cout << "约束子序列和:\n";
vector<int> nums1 = {10, 2, -10, 5, 20};
int k1 = 2;
ConstrainedSubsequenceSum css;
cout << "数组: ";
for (int x : nums1) cout << x << " ";
cout << "\n最大距离: " << k1 << endl;
cout << "最大和: " << css.constrainedSubsetSum(nums1, k1) << endl;
// 测试和为K的最短子数组
cout << "\n和至少为K的最短子数组:\n";
vector<int> nums2 = {2, -1, 2};
int k2 = 3;
ShortestSubarrayWithSumK ssk;
cout << "数组: ";
for (int x : nums2) cout << x << " ";
cout << "\n目标和: " << k2 << endl;
cout << "最短长度: " << ssk.shortestSubarray(nums2, k2) << endl;
// 测试跳跃游戏优化
cout << "\n跳跃游戏优化:\n";
vector<int> nums3 = {2, 3, 1, 1, 4};
JumpGameOptimization jgo;
cout << "数组: ";
for (int x : nums3) cout << x << " ";
cout << "\n最少跳跃次数: " << jgo.jump(nums3) << endl;
// 测试直方图最大矩形
cout << "\n直方图最大矩形:\n";
vector<int> heights = {2, 1, 5, 6, 2, 3};
LargestRectangleHistogram lrh;
cout << "高度: ";
for (int x : heights) cout << x << " ";
cout << "\n最大面积: " << lrh.largestRectangleArea(heights) << endl;
return 0;
}单调队列优化DP的核心是维护决策点的单调性,避免重复计算。在背包问题中按余数分组处理,在子数组问题中维护前缀和的单调性。
单调队列的高级应用
单调队列在复杂问题中的应用,包括多维优化、在线算法等。
单调队列的高级应用 - 在线算法和多维优化
#include <iostream>
#include <vector>
#include <deque>
#include <set>
#include <algorithm>
using namespace std;
// 在线维护滑动窗口的中位数
class SlidingWindowMedian {
private:
multiset<int> small, large; // small存储较小的一半,large存储较大的一半
int k;
void balance() {
if (small.size() > large.size() + 1) {
large.insert(*small.rbegin());
small.erase(small.find(*small.rbegin()));
} else if (large.size() > small.size() + 1) {
small.insert(*large.begin());
large.erase(large.begin());
}
}
double getMedian() {
if (k % 2 == 1) {
return small.size() > large.size() ? *small.rbegin() : *large.begin();
} else {
return (*small.rbegin() + *large.begin()) / 2.0;
}
}
public:
SlidingWindowMedian(int windowSize) : k(windowSize) {}
vector<double> medianSlidingWindow(vector<int>& nums) {
vector<double> result;
for (int i = 0; i < nums.size(); i++) {
// 添加新元素
if (small.empty() || nums[i] <= *small.rbegin()) {
small.insert(nums[i]);
} else {
large.insert(nums[i]);
}
// 移除超出窗口的元素
if (i >= k) {
int toRemove = nums[i - k];
if (small.find(toRemove) != small.end()) {
small.erase(small.find(toRemove));
} else {
large.erase(large.find(toRemove));
}
}
balance();
if (i >= k - 1) {
result.push_back(getMedian());
}
}
return result;
}
};
// 二维滑动窗口最大值
class Matrix2DSlidingWindow {
public:
vector<vector<int>> maxSlidingWindow2D(vector<vector<int>>& matrix, int k) {
int m = matrix.size(), n = matrix[0].size();
vector<vector<int>> result(m - k + 1, vector<int>(n - k + 1));
// 对每一行应用一维滑动窗口最大值
vector<vector<int>> rowMax(m, vector<int>(n - k + 1));
for (int i = 0; i < m; i++) {
deque<int> dq;
for (int j = 0; j < n; j++) {
while (!dq.empty() && dq.front() <= j - k) {
dq.pop_front();
}
while (!dq.empty() && matrix[i][dq.back()] <= matrix[i][j]) {
dq.pop_back();
}
dq.push_back(j);
if (j >= k - 1) {
rowMax[i][j - k + 1] = matrix[i][dq.front()];
}
}
}
// 对每一列应用一维滑动窗口最大值
for (int j = 0; j < n - k + 1; j++) {
deque<int> dq;
for (int i = 0; i < m; i++) {
while (!dq.empty() && dq.front() <= i - k) {
dq.pop_front();
}
while (!dq.empty() && rowMax[dq.back()][j] <= rowMax[i][j]) {
dq.pop_back();
}
dq.push_back(i);
if (i >= k - 1) {
result[i - k + 1][j] = rowMax[dq.front()][j];
}
}
}
return result;
}
};
// 约束条件下的最优决策
class ConstrainedDecision {
public:
int maxScore(vector<int>& nums, int maxJump) {
int n = nums.size();
vector<int> dp(n, INT_MIN);
dp[0] = nums[0];
deque<int> dq;
dq.push_back(0);
for (int i = 1; i < n; i++) {
// 移除超出范围的决策点
while (!dq.empty() && dq.front() < i - maxJump) {
dq.pop_front();
}
// 转移状态
if (!dq.empty()) {
dp[i] = dp[dq.front()] + nums[i];
}
// 维护单调性
while (!dq.empty() && dp[dq.back()] <= dp[i]) {
dq.pop_back();
}
dq.push_back(i);
}
return dp[n - 1];
}
};
int main() {
// 测试滑动窗口中位数
cout << "滑动窗口中位数:\n";
vector<int> nums1 = {1, 3, -1, -3, 5, 3, 6, 7};
SlidingWindowMedian swm(3);
vector<double> medians = swm.medianSlidingWindow(nums1);
cout << "数组: ";
for (int x : nums1) cout << x << " ";
cout << "\n窗口大小3的中位数: ";
for (double x : medians) cout << x << " ";
cout << endl;
// 测试二维滑动窗口
cout << "\n二维滑动窗口最大值:\n";
vector<vector<int>> matrix = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}
};
Matrix2DSlidingWindow m2d;
vector<vector<int>> result2d = m2d.maxSlidingWindow2D(matrix, 2);
cout << "原矩阵:\n";
for (auto& row : matrix) {
for (int x : row) cout << x << "\t";
cout << endl;
}
cout << "2x2窗口最大值:\n";
for (auto& row : result2d) {
for (int x : row) cout << x << "\t";
cout << endl;
}
// 测试约束条件下的最优决策
cout << "\n约束条件下的最大得分:\n";
vector<int> scores = {1, -1, -2, 4, -7, 3};
ConstrainedDecision cd;
cout << "分数数组: ";
for (int x : scores) cout << x << " ";
cout << "\n最大步长为2的最大得分: " << cd.maxScore(scores, 2) << endl;
return 0;
}高级应用展示了单调队列在多维问题、在线算法中的威力。滑动窗口中位数使用双set维护,二维滑动窗口分别对行列应用单调队列,约束决策问题通过维护可达状态的单调性优化。
竞赛技巧总结
单调队列的核心思想:
- 维护队列中元素的单调性(递增或递减)
- 通过双端队列支持队首队尾的高效操作
- 每个元素最多进出队列一次,保证O(n)复杂度
- 适用于滑动窗口和动态规划优化问题
常见应用场景:
- 滑动窗口最值问题:维护窗口内的最大值或最小值
- 动态规划优化:优化状态转移的时间复杂度
- 背包问题变形:多重背包的单调队列优化
- 子数组问题:最大子数组和的各种变形
- 决策优化:在约束条件下的最优决策
实现技巧:
- 存储索引而非值:便于处理窗口边界和元素位置
- 及时清理过期元素:保持队列的有效性
- 正确维护单调性:根据问题需求选择递增或递减
- 边界处理:注意窗口形成的时机和条件
调试要点:
- 检查单调性的维护逻辑
- 确认窗口边界的处理
- 验证队列清理的时机
- 测试边界情况(空队列、单元素等)
复杂度分析:
- 时间复杂度:每个元素最多进出队列一次,总体O(n)
- 空间复杂度:O(k),其中k是窗口大小或状态数
- 相比朴素算法的O(nk)复杂度有显著提升
扩展应用:
- 多维滑动窗口:分别对每个维度应用单调队列
- 在线算法:结合其他数据结构处理动态查询
- 并行优化:利用单调队列的性质进行并行处理
- 近似算法:在近似算法中维护候选解集合