1 单调栈
单调栈: 在原有的栈的基础上,使栈内元素维持一个 单调(递减或递增) 的特性。
以单调递增栈为例:
从左往右遍历数组,把每个元素入栈,如果栈顶元素大于等于当前元素,则将栈顶出栈,一直到栈为空或者栈顶元素小于当前元素为止。
如果栈为空,我们标记为-1
,否则记录栈顶,这样我们可以找到这个数组每个元素左边的第一个小于它的元素(无即为-1
)。
如5 1 3 2 6
,它的进、入栈,如下:
-
5
进栈(标记-1
) -
5
出栈,1
进栈(标记-1
) -
3
入栈(标记为1
) -
3
出栈,2
进栈(标记为-1
) -
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 是矩形的宽度:
注意,直方图中矩形的顺序(即它们的高度)很重要。计算直方图中与公共基线对齐的最大矩形的面积。右图显示了所描绘直方图的最大对齐矩形。
输入包含多组数据。
题解
由题意和图可知,在连续的几个矩形中,最大对其矩形是以最矮的矩形高度为长、以矩形个数为宽构成的。
与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;
}