单调栈及应用

本文介绍了如何运用单调栈解决四个编程问题:查找数组中每个元素左边的第一个小于它的元素、区间最小值问题、直方图中最大矩形的面积以及1的最大矩阵。通过递增栈实现元素的单调性操作,有效地找到区间边界并计算相关面积。
摘要由CSDN通过智能技术生成

1 单调栈

单调栈: 在原有的栈的基础上,使栈内元素维持一个 单调(递减或递增) 的特性。

单调递增栈为例:

从左往右遍历数组,把每个元素入栈,如果栈顶元素大于等于当前元素,则将栈顶出栈,一直到栈为空或者栈顶元素小于当前元素为止。

如果栈为空,我们标记为-1,否则记录栈顶,这样我们可以找到这个数组每个元素左边的第一个小于它的元素(无即为-1)。

5 1 3 2 6,它的进、入栈,如下:

  1. 5进栈(标记-1

  2. 5出栈,1进栈(标记-1

  3. 3入栈(标记为1

  4. 3出栈,2进栈(标记为-1

  5. 6入栈(标记为2

由此可以得到数组-1 -1 1 -1 2

代码如下:

for (int i = 1; i <= n; i++) {
    while (!s.empty() && s.top() >= a[i])
    	s.pop();
    ans[i] = (s.empty() ? -1 : s.top());
    s.push(a[i]);
}

如果要找右边第一个最小,从右往左遍历;如果找第一个最大,改变符号即可。

2 例题

2.1 找小的数字

题目描述

给定一个 N N N,表示一个序列的长度,然后输入这个序列,输出序列每个元素的左边最近的比它小的数字,如果不存在则输出 − 1 -1 1

题解

经典模板。代码如下:

for (int i = 1; i <= n; i++) {
    cin >> a[i];
    while (!s.empty() && s.top() >= a[i])
        s.pop();
    ans[i] = (s.empty() ? -1 : s.top());
    s.push(a[i]);
    cout << ans[i] << " ";
}

2.2 区间最小值问题

题目描述

给出正整数 n n n 和一个长度为 n n n 的数列,要求找出一个子区间,使这个子区间的数字之和乘上子区间中的最小值最大。

题解

以每个元素作为所在数列的最小值,因此要尽可能扩大数列的长度,并且要保证数列内没有元素小于被选中的这个元素。

用单调栈找出这个元素左边和右边第一个比它小的元素所在位置,左边的加 1 1 1 即为最靠左的合法左端点,右边的减 1 1 1 即为最靠右的合法右端点。

注意,单调栈模板中,比较的始终是数值而非索引,但我们可以用 ans数组和栈内元素保存对应的索引

以寻找左端点为例:

while (!s.empty() && a[s.top()] >= a[i])  //注意是a[s.top()]
    s.pop();
S[i] = (s.empty() ? 1 : s.top() + 1);  //如果左边没有比它小的,则左端点到1即可;如果有,左端点为这个位置+1
s.push(i);  //入栈当前索引

求出左右端点后,利用点缀和求出对应每个数列的值,比较即可。

注意数据范围过大,需要long long型。

代码如下:

#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
    freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE                    \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
using namespace std;
typedef long long ll;
const ll N = 1e6 + 10;
ll n, a[N], S[N], T[N], ans[N], sum[N];
stack<ll> s, t;
int main() {
    CLOSE;
    cin >> n;
    for (ll i = 1; i <= n; i++) {
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i];
    }
    for (ll i = 1; i <= n; i++) { //单调栈找左端点
        while (!s.empty() && a[s.top()] >= a[i])
            s.pop();
        S[i] = (s.empty() ? 1 : s.top() + 1);
        s.push(i);
    }
    for (ll i = n; i >= 1; i--) {  //单调栈找右端点
        while (!t.empty() && a[t.top()] >= a[i])
            t.pop();
        T[i] = (t.empty() ? n : t.top() - 1);
        t.push(i);
    }
    ll res = -1, s, e;
    for (ll i = 1; i <= n; i++) {  //求面积最大值
        ans[i] = sum[T[i]] - sum[S[i] - 1];
        ans[i] *= a[i];
        if (ans[i] > res) {
            res = ans[i];
            e = T[i];
            s = S[i];
        }
    }
    cout << res << endl << s << " " << e;
    return 0;
}

2.3 直方图中最大的矩形

题目描述

直方图是由在公共基线对齐的一系列矩形组成的多边形。矩形具有相同的宽度,但可能具有不同的高度。

例如,左图显示了由高度为 2、1、4、5、1、3、3 的矩形组成的直方图,以单位测量,其中 1 是矩形的宽度:

img

注意,直方图中矩形的顺序(即它们的高度)很重要。计算直方图中与公共基线对齐的最大矩形的面积。右图显示了所描绘直方图的最大对齐矩形。

输入包含多组数据。

题解

由题意和图可知,在连续的几个矩形中,最大对其矩形是以最矮的矩形高度为长、以矩形个数为宽构成的。

2.2 区间最小值问题类似地,我们可以找到左右端点(方法完全相同),然后计算出每组矩形构成的大矩形的面积,求出最大值。

求最大值部分代码:

ll res = -1;
for (ll i = 1; i <= n; i++) {
    ans[i] = a[i] * (T[i] - S[i] + 1);
    res = max(res, ans[i]);
}

2.4 所有1的最大矩阵

题目描述

给定一个 m × n ( 0 , 1 ) m\times n(0,1) m×n(0,1) 矩阵,找出最大的子矩阵 p \mathbf p p p \mathbf p p 矩阵里只含有 1 1 1 。请问 p \mathbf p p 包含多少个元素。

题解

传统的思考方式似乎行不通,但我们可以换一种方法:

把矩阵从第 1 1 1 行开始遍历到最后一行,将 1 1 1 ~ i i i 行的 0 , 1 0,1 0,1 看做2.3 直方图中最大的矩阵的直方图。即把第 i i i 行看做公共基线,上面的 1 1 1 表示矩形。利用数组a,如果是 1 1 1,累加;如果是 0 0 0,清空,从而求出“矩形”的高度。

例如:

1 0 1 1
0 0 1 0
1 1 1 0
0 0 0 1

以第 3 3 3 行为基线,此时 a[] = {1, 1, 3, 0}

对于每一行,以相同的方法求出a[]的左右端点,和“矩形”面积。

代码如下:

#include <bits/stdc++.h>
#define endl '\n'
#define file(FILENAME) \
    freopen(FILENAME ".in", "r", stdin), freopen(FILENAME ".out", "w", stdout)
#define CLOSE                    \
    ios::sync_with_stdio(false); \
    cin.tie(0);                  \
    cout.tie(0)
using namespace std;
typedef long long ll;
const ll mod = 1e9 + 7;
const ll N = 2010;
int n, m, p[N][N], a[N], S[N], T[N], ans[N];
int res;
stack<int> s, t;
void solve() {
    memset(S, 0, sizeof S);
    memset(T, 0, sizeof T);
    while (!s.empty())
        s.pop();
    while (!t.empty())
        t.pop();
    for (int i = 1; i <= m; i++) {
        while (!s.empty() && a[s.top()] >= a[i])
            s.pop();
        S[i] = (s.empty() ? 1 : s.top() + 1);
        s.push(i);
    }
    for (int i = m; i >= 1; i--) {
        while (!t.empty() && a[t.top()] >= a[i])
            t.pop();
        T[i] = (t.empty() ? m : t.top() - 1);
        t.push(i);
    }
    for (int i = 1; i <= m; i++)
        res = max(res, a[i] * (T[i] - S[i] + 1));
}
int main() {
    CLOSE;
    while (cin >> n >> m) {
        res = -1;
        memset(a, 0, sizeof a);
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++)
                cin >> p[i][j];
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++)
                a[j] = (p[i][j] ? (a[j] + 1) : 0);
            solve();
        }
        cout << res << endl;
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值