[算法学习] 单调栈
单调栈在子矩阵方面的应用
大致的题型:
给定你一个\(n\times m\)的矩形,询问有多少个子矩阵,使得这个子矩阵满足一定的条件。
类型1
题目链接:洛谷P1950 长方形
Description
给定你一个\(n\times m\)的矩阵,询问有多少个内部全是\(1\)的子矩阵。
数据范围\(1\le n,m\le 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\n", ans);
return 0;
}
类型2
题目链接:ZJOI2007 棋盘制作
Description
给定你一个\(n\times m\)的矩阵,询问最大的全\(1\)矩形和正方形的面积。
数据范围\(1\le n,m\le 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)\times 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\n%lld\n", 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) \}\)。
数据范围\(1\le n\le 5\times 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), '\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\n", res);
return 0;
}
单调栈优化dp
题目链接:JSOI2011 柠檬
Description
将一个数列分成若干段,从每一段中选定一个数\(s_0\),假设这个数在此段有\(t\)个,那么这一段价值为\(s_0t^2\),数列的总价值为每一段的价值和。
你需要最大化总价值。
数据范围\(1\le n\le 100000,1\le s_i\le 10000\)。