1. 单调队列与单调栈基础概念
单调队列和单调栈是两种特殊的线性数据结构,它们通过维护元素的单调性来高效解决特定问题。单调队列主要用于维护滑动窗口内的极值,而单调栈则用于快速找到元素的前驱/后继极值关系。
单调队列的核心特征是队列中的元素保持严格的单调递增或递减顺序。以维护最小值为例,当新元素入队时,会从队尾移除所有大于等于它的元素,确保队列始终单调递增。这样,队首元素就是当前窗口的最小值。
单调栈则通过保持栈内元素的单调性,可以快速找到第一个比当前元素大或小的前驱/后继。例如,维护一个单调递增栈,当遇到比栈顶小的元素时,弹出栈顶直到满足单调性,此时栈顶就是当前元素的左侧第一个较小值。
2. 单调队列的实现与优化
2.1 基本实现模板
以滑动窗口最小值为例,C++实现如下:
cpp复制vector<int> minSlidingWindow(vector<int>& nums, int k) {
deque<int> q;
vector<int> res;
for(int i=0; i<nums.size(); ++i){
// 移除超出窗口范围的元素
while(!q.empty() && q.front() <= i-k) q.pop_front();
// 维护单调递增性
while(!q.empty() && nums[q.back()] >= nums[i]) q.pop_back();
q.push_back(i);
// 窗口形成后记录结果
if(i >= k-1) res.push_back(nums[q.front()]);
}
return res;
}
2.2 多重背包优化应用
单调队列可以优化多重背包问题的动态规划解法。传统多重背包的时间复杂度为O(NVK),使用单调队列可降为O(N*V):
cpp复制for(int i=1; i<=n; ++i){
for(int j=0; j<w[i]; ++j){
deque<int> q;
for(int k=0; k*w[i]+j <= V; ++k){
// 维护单调队列
while(!q.empty() && q.front() < k-c[i]) q.pop_front();
while(!q.empty() && dp[i-1][q.back()*w[i]+j]-q.back()*v[i]
<= dp[i-1][k*w[i]+j]-k*v[i]) q.pop_back();
q.push_back(k);
// 状态转移
dp[i][k*w[i]+j] = dp[i-1][q.front()*w[i]+j] + (k-q.front())*v[i];
}
}
}
3. 单调栈的典型应用场景
3.1 柱状图最大矩形
单调栈可以高效解决柱状图中最大矩形面积问题:
cpp复制int largestRectangleArea(vector<int>& heights) {
stack<int> s;
heights.push_back(0); // 哨兵
int max_area = 0;
for(int i=0; i<heights.size(); ){
if(s.empty() || heights[i] > heights[s.top()]){
s.push(i++);
}else{
int h = heights[s.top()]; s.pop();
int w = s.empty() ? i : i-s.top()-1;
max_area = max(max_area, h*w);
}
}
return max_area;
}
3.2 每日温度问题
单调栈还能解决"下一个更高温度"类问题:
cpp复制vector<int> dailyTemperatures(vector<int>& T) {
stack<int> s;
vector<int> res(T.size());
for(int i=0; i<T.size(); ++i){
while(!s.empty() && T[i] > T[s.top()]){
res[s.top()] = i - s.top();
s.pop();
}
s.push(i);
}
return res;
}
4. 常见问题与调试技巧
4.1 边界条件处理
处理单调数据结构时,边界条件容易出错:
- 窗口大小不足k时不应输出结果
- 栈空时需要特殊处理宽度计算
- 数组末尾需要添加哨兵元素
4.2 性能优化建议
- 预分配足够空间避免动态扩容
- 使用数组模拟栈/队列减少对象开销
- 在多重背包优化中,注意余数的分组处理
4.3 典型错误案例
错误示例:未正确处理相等元素
cpp复制// 错误:相等的元素也应该弹出
while(!q.empty() && nums[q.back()] > nums[i]) q.pop_back();
正确做法:
cpp复制// 正确:相等的元素也需要弹出以保持严格单调
while(!q.empty() && nums[q.back()] >= nums[i]) q.pop_back();
5. 高级应用与变种
5.1 二维单调栈问题
解决类似"最大全1子矩阵"问题时,可以逐行构建高度数组,然后对每行使用单调栈:
cpp复制int maximalRectangle(vector<vector<char>>& matrix) {
if(matrix.empty()) return 0;
int m = matrix.size(), n = matrix[0].size();
vector<int> heights(n+1, 0);
int max_area = 0;
for(int i=0; i<m; ++i){
stack<int> s;
for(int j=0; j<=n; ++j){
// 更新高度数组
if(j<n){
heights[j] = matrix[i][j]=='1' ? heights[j]+1 : 0;
}
// 单调栈处理
while(!s.empty() && heights[j] < heights[s.top()]){
int h = heights[s.top()]; s.pop();
int w = s.empty() ? j : j-s.top()-1;
max_area = max(max_area, h*w);
}
s.push(j);
}
}
return max_area;
}
5.2 单调队列维护凸包
在动态规划优化中,当状态转移方程满足特定形式时,可以用单调队列维护凸包:
cpp复制// 假设状态转移方程为 dp[i] = min(dp[j] + (a[i]-a[j])^2)
for(int i=1; i<=n; ++i){
// 维护下凸包
while(head+1<tail &&
(Y(q[head+1])-Y(q[head])) <= 2*a[i]*(X(q[head+1])-X(q[head]))){
head++;
}
dp[i] = dp[q[head]] + (a[i]-a[q[head]])*(a[i]-a[q[head]]);
// 将当前点加入凸包
while(head+1<tail &&
(Y(i)-Y(q[tail-1]))*(X(q[tail-1])-X(q[tail-2])) <=
(Y(q[tail-1])-Y(q[tail-2]))*(X(i)-X(q[tail-1]))){
tail--;
}
q[tail++] = i;
}
6. 实战经验分享
在实际编码比赛中,我有以下几点深刻体会:
- 单调栈判断条件容易混淆,建议统一使用"小于等于"或"大于等于"的标准,避免混用
- 处理环形数组时,可以破环成链(将数组复制一份连接在末尾)
- 在Python中,使用collections.deque比list模拟队列性能更好
- 调试时可以打印出栈/队列的内容,可视化检查单调性是否保持
一个常见的坑点是忘记处理剩余元素。例如在柱状图问题中,遍历结束后栈中可能还有元素,这时需要在数组末尾添加一个哨兵值(如0)来强制清空栈。
