单调栈在子矩阵方面的应用
大致的题型:
给定你一个(n imes m)的矩形,询问有多少个子矩阵,使得这个子矩阵满足一定的条件。
类型1
题目链接:洛谷P1950 长方形
Description
给定你一个(n imes m)的矩阵,询问有多少个内部全是(1)的子矩阵。
数据范围(1le n,mle 1000)。
Solution
先想如何暴力。
枚举这个子矩阵的两长和两宽,在暴力(check)该子矩阵内的所有点是否为(1)。
复杂度:(O(n^6)),期望得分:(10)分。
我们发现,这个(check)部分可以利用二维前缀和来优化,因此可以预处理(cnt[i][j]=sum_{p=1}^{i}sum_{q=1}^{j}a_{i,j})。
复杂度:(O(n^4)),期望得分:(30)分。
考虑框定了该子矩形的左端和右端,那么我们可以扫行,并进行简单计数即可。
复杂度:(O(n^3)),期望得分:(30)~(100)分。
接下来,我们用:
(h_{i,j})表示((i,j))这个点往上连续(1)的最长长度。
(l_{i,j})表示从((i,j))开始第一个满足(h_{i,l_{i,j}}le h(i,j))的点,如果不存在,令(l_{i,j}=0)。
(r_{i,j})表示从((i,j))开始第一个满足(h_{i,r_{i,j}}le h(i,j))的点,如果不存在,令(r_{i,j}=m+1)。
那么,对于((i,j))为矩形底的贡献,就是(val=(j-l_{i,j})*(r_{i,j}-j)*h_{i,j})。
考虑一下,这样做如何保证答案不重不漏。
不重:当且仅当在同一行存在两个数(l_{i,j_1}=l_{i,j_2})并且(r_{i,j_1}=r_{i,j_2})的时候,才有可能算重矩形。
但是这种情况是不存在的,因为(l_{i,j})满足了左边第一个小于等于的,右边第一个小于的,显然无法构造出这种情况。
不漏:对于一个矩形,总有一个(l_{i,j},r_{i,j})能框住一个矩形的两边,故这个矩形一定能被计算到。
我们可以通过一个单调栈来计算(l_{i,j})和(r_{i,j}),复杂度(O(n^2))。
Code
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define rint register int
const int N = 1005;
bool a[N][N];
int h[N][N], l[N][N], r[N][N], n, m;
stack <int> st;
void push_l(int i, int j) {
while (!st.empty() && h[i][st.top()] > h[i][j]) r[i][st.top()] = j, st.pop();
st.push(j);
}
void push_r(int i, int j) {
while (!st.empty() && h[i][st.top()] >= h[i][j]) l[i][st.top()] = j, st.pop();
st.push(j);
}
int main() {
scanf("%d%d", &n, &m);
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= m; j++) {
char x = getchar();
while (x != '.' && x != '*') {
x = getchar();
}
a[i][j] = x == '.';
}
}
for (rint j = 1; j <= m; j++) {
for (rint i = 1; i <= n; i++) {
if (a[i][j]) h[i][j] = h[i - 1][j] + 1;
else h[i][j] = 0;
}
}
long long ans = 0ll;
for (rint i = 1; i <= n; i++) {
while (!st.empty()) st.pop();
for (rint j = 1; j <= m; j++) {
push_l(i, j);
}
while (!st.empty()) r[i][st.top()] = m + 1, st.pop();
for (rint j = m; j >= 1; j--) {
push_r(i, j);
}
while (!st.empty()) l[i][st.top()] = 0, st.pop();
for (rint j = 1; j <= m; j++) {
ans += 1ll * (j - l[i][j]) * (r[i][j] - j) * h[i][j];
}
}
printf("%lld
", ans);
return 0;
}
类型2
题目链接:ZJOI2007 棋盘制作
Description
给定你一个(n imes m)的矩阵,询问最大的全(1)矩形和正方形的面积。
数据范围(1le n,mle 2000)。
Solution
跟上一题类似,我们用:
(h_{i,j})表示((i,j))这个点往上连续(1)的最长长度。
(l_{i,j})表示从((i,j))开始第一个满足(h_{i,l_{i,j}}le h(i,j))的点,如果不存在,令(l_{i,j}=0)。
(r_{i,j})表示从((i,j))开始第一个满足(h_{i,r_{i,j}}le h(i,j))的点,如果不存在,令(r_{i,j}=m+1)。
那么,对于((i,j))为矩形底的贡献,我们需要求的是 (max{(r_{i,j}-l_{i,j}-1) imes h_{i,j}}) 。
正方形的话,只需要求(max^2 {min{r_{i,j}-l_{i,j}-1,h_{i,j}}})。
Code
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define rint register int
#define ll long long
const int N = 2105;
bool a[N][N];
int h[N][N], l[N][N], r[N][N], n, m;
stack <int> st;
void push_l(int i, int j) {
while (!st.empty() && h[i][st.top()] > h[i][j]) r[i][st.top()] = j, st.pop();
st.push(j);
}
void push_r(int i, int j) {
while (!st.empty() && h[i][st.top()] >= h[i][j]) l[i][st.top()] = j, st.pop();
st.push(j);
}
pair <ll, ll> solve() {
for (rint j = 1; j <= m; j++) {
for (rint i = 1; i <= n; i++) {
if (a[i][j]) h[i][j] = h[i - 1][j] + 1;
else h[i][j] = 0;
l[i][j] = r[i][j] = 0;
}
}
long long ans1 = 0ll, ans2 = 0ll;
for (rint i = 1; i <= n; i++) {
while (!st.empty()) st.pop();
for (rint j = 1; j <= m; j++) {
push_l(i, j);
}
while (!st.empty()) r[i][st.top()] = m + 1, st.pop();
for (rint j = m; j >= 1; j--) {
push_r(i, j);
}
while (!st.empty()) l[i][st.top()] = 0, st.pop();
for (rint j = 1; j <= m; j++) {
ans1 = max(ans1, 1ll * (r[i][j] - l[i][j] - 1) * h[i][j]);
ans2 = max(ans2, (long long)min(r[i][j] - l[i][j] - 1, h[i][j]));
}
}
return make_pair(ans1, ans2 * ans2);
}
int main() {
scanf("%d%d", &n, &m);
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= m; j++) {
scanf("%d", &a[i][j]);
if ((i + j) & 1) {
a[i][j] ^= 1;
}
}
}
pair <ll, ll> ans1 = solve();
for (rint i = 1; i <= n; i++) {
for (rint j = 1; j <= m; j++) {
a[i][j] ^= 1;
}
}
pair <ll, ll> ans2 = solve();
printf("%lld
%lld
", max(ans1.second, ans2.second), max(ans1.first, ans2.first));
return 0;
}
类型3
题目链接:区间max
Description
给定一个序列a,求(max { min(a[l],a[l+1],a[l+2]…a[r]) * (r-l+1) })。
数据范围(1le nle 5 imes 10^5)。
Solution
在暴力扫的过程中记录当前区间最小值。
复杂度:(O(n^2)),期望得分:(80)分。
考虑分治进行该过程,假设我们想要知道([l,r])的答案,可以把它拆分成([l,mid]),([mid+1,r])和两者合并的答案。
所以可以在分治后随便合并一下,即可通过此题。
复杂度:(O(nlogn)),期望得分:(100)分。
我们转换思路,考虑(a_i)成为(min)时,左右最远能拓展到的距离。
显然,这可以用两个单调栈解决。
复杂度:(O(n)),期望得分:(100)分。
话说这数据造不了太大,该怎么卡(O(nlogn))啊。
Code1(分治)
const int N = 500005;
ll a[N];
int n;
ll dfs(int l, int r) {
if (l == r) return a[l];
int mid = (l + r) >> 1;
ll ans = max(dfs(l, mid), dfs(mid + 1, r));
ll x = mid, y = mid + 1, h = min(a[x], a[y]);
ans = max(ans, h << 1);
while (x > l && y < r) {
if (a[x - 1] < a[y + 1]) h = min(h, a[++y]);
else h = min(h, a[--x]);
ans = max(ans, (ll)(y - x + 1) * h);
}
while (x > l) {
h = min(h, a[--x]);
ans = max(ans, (ll)(y - x + 1) * h);
}
while (y < r) {
h = min(h, a[++y]);
ans = max(ans, (ll)(y - x + 1) * h);
}
return ans;
}
int main() {
read(n);
rep(i, 1, n) scanf("%lld", &a[i]);
print(dfs(1, n), '
');
return 0;
}
Code2(单调栈)
#include <bits/stdc++.h>
using namespace std;
const int N = 500005;
int a[N], L[N], R[N];
int st[N], tp = 0;
int n;
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
}
for (int i = 1; i <= n; i++) {
while (tp > 0 && a[st[tp]] >= a[i]) tp--;
L[i] = st[tp] + 1;
st[++tp] = i;
}
tp = 0; st[tp] = n + 1;
for (int i = n; i >= 1; i--) {
while (tp > 0 && a[st[tp]] >= a[i]) tp--;
R[i] = st[tp] - 1;
st[++tp] = i;
}
long long res = 0;
for (int i = 1; i <= n; i++) {
res = max(res, (long long)a[i] * (R[i] - L[i] + 1));
}
printf("%lld
", res);
return 0;
}
单调栈优化dp
题目链接:JSOI2011 柠檬
Description
将一个数列分成若干段,从每一段中选定一个数(s_0),假设这个数在此段有(t)个,那么这一段价值为(s_0t^2),数列的总价值为每一段的价值和。
你需要最大化总价值。
数据范围(1le nle 100000,1le s_ile 10000)。