算法系列:单调栈 单调队列


单调栈

首先,单调栈本身也属于栈,属于一个 “先进后出” 的数据结构。单调栈的话,就是需要栈内元素满足单调性的数据结构,可以是单调递增的,也可以是单调递减的。

对于数组 $[3,1,4,5,2,7]$ 为例,单调递增栈的过程如下:

单调栈
  1. acwing 131 直方图中最大矩形

​ 这道题的思路其实并不简单,我们要维护一个单调递增的栈。我们求解的过程是:遍历每个长方形的高,如果当前的遍历高度大于栈顶的高度,那就直接入栈。如果小于栈顶的高度,然后就不断的出栈,直到当前遍历高度大于栈顶的高度或者栈已经空了,在每次出栈的过程中,都会计算当前构成的矩形面积的值,并不断的更新最大值。

​ 注意:这道题还需要存储每个高度对应的矩形的高度,所以用数组模拟栈比较方便操作,就不在使用STL中的stack数据结构。

#include <bits/stdc++.h> 
using namespace std; 
const int maxn = 1e5 + 1000; 
typedef long long ll; 
int n, cnt; 
ll ans; 
ll a[maxn], s[maxn], w[maxn]; 

int main() {
    while (scanf("%d", &n) && n != 0) { 
        a[n + 1] = 0; 
        cnt = 0; 
        ans = 0;
        // 用数组s模拟栈的情况 
        memset(s, 0, sizeof(s)); 
        memset(w, 0, sizeof(w)); 
        for (int i = 1; i <= n; ++i) cin >> a[i]; 
        // 这里n + 1的原因是为了让最后一个入栈的元素能够计算
        for (int i = 1; i <= n + 1; ++i) {
            if (a[i] > s[cnt]) {
                s[++cnt] = a[i]; 
                w[cnt] = 1; 
            } else {
                int width = 0; 
                while (s[cnt] > a[i]) {
                    // 不断更新该状态下对应的矩形面积
                    width += w[cnt]; 
                    ans = max(ans, (ll) width * s[cnt]); 
                    cnt--; 
                }
                s[++cnt] = a[i]; 
                w[cnt] = width + 1; 
            }
        }
        printf("%lld\n", ans); 
    }
    return 0; 
}
  1. LeetCode 503 下一个更大元素

​ 题目要求:给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。

​ 把两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。

// 版本一
class Solution {
public:
    vector<int> nextGreaterElements(vector<int>& nums) {
        // 拼接一个新的nums
        vector<int> nums1(nums.begin(), nums.end());
        nums.insert(nums.end(), nums1.begin(), nums1.end());
        // 用新的nums大小来初始化result
        vector<int> result(nums.size(), -1);
        if (nums.size() == 0) return result;
        // 开始单调栈
        stack<int> st;
        for (int i = 0; i < nums.size(); i++) {
            while (!st.empty() && nums[i] > nums[st.top()]) {
                result[st.top()] = nums[i];
                st.pop();
            }
            st.push(i);
        }
        // 最后再把结果集即result数组resize到原数组大小
        result.resize(nums.size() / 2);
        return result;
    }
};

单调队列

单调队列与单调栈相似,他们的区别在于单调栈只能从一端进行元素的添加和删除,而单调队列可以维护数组两端元素的添加和删除。

c++ STL中有一个数据结构 $deque$,可以实现双端队列的效果。即可以在队列的两端进行元素的添加和删除,用来维护单调队列比较方便。(当然也可以直接通过数组进行维护)。

关于单调队列,最经典的就是滑动窗口模型。

  1. acwing 154 滑动窗口

​ 题目:给定一个大小为 $n <= 10^6$ 的数组。有一个大小为 $k$ 的滑动窗口,它从数组的最左边移动到最右边。你只能在窗口中看到 $k$ 个数字。你的任务是确定滑动窗口位于每个位置时,窗口中的最大值和最小值。

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N];
deque<int> q;

int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    // 单调队列维护窗口最小值
    for(int i = 1; i <= n; i++) {
        // 如果当前队列尾部的值大于当前遍历的值,就不断的弹出尾部的值
        while(!q.empty() && a[q.back()] >= a[i]) {
            q.pop_back();
        }
        // 如果当前队列头部的索引已经超过了滑动窗口的限制,弹出头部的值
        while(q.front() <= i - m) {
            q.pop_front();
        }
        q.push_back(i);
        // 输出每个窗口对应的最小值
        if(i >= m) cout << a[q.front()] << " "; 
    }
    cout << "\n";
    q.clear();
    for(int i = 1; i <= n; i++) {
        while(!q.empty() && a[q.back()] <= a[i]) {
            q.pop_back();
        }
        while(q.front() <= i - m) {
            q.pop_front();
        }
        q.push_back(i);
        if(i >= m) cout << a[q.front()] << " "; 
    }
    return 0;
}
  1. acwing 135 最大子序和

题目:输入一个长度为 $n$ 的整数序列,从中找出一段长度不超过 $m$ 的连续子序列,使得子序列中所有数的和最大。

最大子序和题解

#include <bits/stdc++.h>
using namespace std;
const int N = 1005;
int a[N], sum[N];
deque<int> q;
int res;

// 单调队列优化算法,算法复杂度为o(n)
int main()
{
    int n, m;
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    q.push_back(0);
    for(int i = 1; i <= n; i++) {
        // 如果子序列的长度超过了m, 弹出队列头部的值
        while(!q.empty() && q.front() < i - m) {
            q.pop_front();
        }
        res = max(res, sum[i] - sum[q.front()]);
        // 如果当前遍历的值的前缀和小于队列尾部的前缀和,弹出尾部的值
        // 原因是区间和是s[R] - s[L],所以s[L]肯定要越小越好。
        while(!q.empty() && sum[q.back()] >= sum[i]) {
            q.pop_back();
        }
        q.push_back(i);
    }
    cout << res << endl;
    return 0;
}

文章作者: Fu-Kang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fu-Kang !
  目录