单调栈、最大子矩形、最大子矩阵问题
Ⅰ 单调栈
可以线性预处理:序列前/后缀最大/小值的位置,或者是第 \(i\) 个数下一个更小/大数的位置。
B3666 求数列所有后缀最大值的位置 https://www.luogu.com.cn/problem/B3666
题意:
给一个初始为空的数列 \(a\) ,共 \(n\) 次操作,第 \(i(1 \leq i \leq n)\) 次操作会在 \(a\) 的末尾加入一个正整数 \(x\) 。
每次操作结束后,找到当前 \(a\) 的所有后缀最大值的下标(下标从 \(1\) 开始)。一个下标 \(i\) 是当前 \(a\) 的后缀最大值下标 \(iff\) :\(\forall j, i < j \leq |a|\) ,都有 \(a_i > a_j\) 。
每次操作结束输出当前数列所有后缀最大值的下标的按位异或和。
\(1 \leq n \leq 10^{6}, 1 \leq a_i < 2^{64}\)
题解:
对于某个数列,显然任意元素 \(a_x\) 满足 \(\forall i > x, a_i < x\) ,则是后缀最大值。设所有 \(x\) 得到的序列为 \(\mathbb{P}\) 。
于是 \(\forall x < y \in \mathbb{P}\) ,有 \(a_x > a_y\) 。至少 \(\forall x \in \mathbb{P}\) 属于解集。
否则,至少存在 \(a_y \geq a_x \ s.t.\ y > x\) 。于是 \(\forall x \not \in \mathbb{P}\) 不属于解集。
于是 \(\forall x \in \mathbb{P}\) 为解集。
于是对于滑动的窗口,可以维护严格递减的单调栈,即动态维护序列 \(\mathbb{P}\) 。
现在答案貌似成为了:对每个滑动窗口的 \(x \in \mathbb{P}\) ,计算一遍按位异或和。但这显然是时间不允许的。
注意到按位异或的重要性质: \(x \oplus x = 0\) 。
考虑正难则反,区间的所有下标去除非法下标则是答案。要去除的即当前窗口下,单调栈中会被弹出的下标。维护历史弹出值的按位异或和即可。
窗口是维护前缀 \(pre(1, r)\) 的滑动,所以设 \(ans = \bigoplus_{i = 1}^{r} a_i\) 。然后 \(ans \oplus x \ s.t.\ x \in \mathbb{P}, a_x \leq a_r\) 。
前缀异或可以在线处理 \(ans \oplus r\) 。也存在
考虑证明,设 \(a = ???00, b = ???01, c = ???10, d = ???11\) 。
int n; std::cin >> n;
std::vector<u64> a(n + 1);
std::stack<u64> stk;
auto calc = [&] (int X) -> u64 {
if (X % 4 == 0) return X;
if (X % 4 == 1) return 1;
if (X % 4 == 2) return X + 1;
if (X % 4 == 3) return 0;
return -1;
};
u64 remove = 0;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
u64 res = 0;
while (!stk.empty() && a[i] >= a[stk.top()]) {
remove ^= stk.top();
stk.pop();
}
stk.push(i);
std::cout << (remove ^ calc(i)) << "\n";
}
时间复杂度 \(O(n)\) 。
P2866 [USACO06NOV] Bad Hair Day S https://www.luogu.com.cn/problem/P2866
题意:
有 \(N\) 头奶牛,每一头奶牛都站在同一排,面朝右,他们被从左到右依次编号为 \(1, 2, 3, \cdots, N\) 。编号为 \(i(1 \leq i \leq N)\) 的奶牛身高为 \(h_i\) 。
对于第 \(i\) 头奶牛前面的第 \(j\) 头牛,如果 \(h_i > h_{i+ 1 \sim j}\) 。那么认为第 \(i\) 头牛可以看见第 \(i + 1 \sim j\) 头牛。
定义 \(C_i\) 为第 \(i\) 头牛能看到的牛的数量,回答 \(\sum_{i = 1}^{N} C_i\) 。
\(1 \leq 8 \times 10^{4}, 1 \leq h_i \leq 10^{9}\)
题解:
通过算贡献可以用普通栈解决。
最裸的做法是单调栈,很显然预处理出每个位置 \(i\) 右一个非严格更大值位置 \(p_i\) 。 \(C_i = p_i - i - 1\) 。
\(a_i\) 右一个非严格更大值为 \(a_p\) ,本质是:单调严格递减栈中,\(i\) 满足 \(a_i \leq a_p\) ,被 \(p\) 踢出。
最后单调栈中属于的元素不具有 \(p_i\) 。让 \(p_i = n + 1\) 可以保证答案正确。
然后对 \(C_i\) 线性求和。
int n; std::cin >> n;
std::vector<int> a(n + 1);
std::vector<int> p(n + 1, n + 1);
std::stack<int> stk;
for (int i = 1; i <= n; i++) {
std::cin >> a[i];
while (!stk.empty() && a[i] >= a[stk.top()]) {
p[stk.top()] = i;
stk.pop();
}
stk.push(i);
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
ans += p[i] - i - 1;
}
std::cout << ans << "\n";
时间复杂度 \(O(n)\) 。
P1901 发射站 https://www.luogu.com.cn/problem/P1901
题意:
某地 \(N\) 个能量发射站顺序排成一行,第 \(i(1 \leq i \leq N)\) 个发射站的高度位 \(H_i\) ,并能向两边同时发射能量值为 \(V_i\) 的能量,发出的能量只能被两边离它最近且更高的发射站接受。
询问接受能量最多的发射站所接受的能量是多少?
\(1 \leq N \leq 10^{6}, 1 \leq H_i \leq 2 \times 10^{9}, 1 \leq V_i \leq 10^{4}\)
题解:
经典的单调栈预处理出下一个更大/小值。
正向逆向跑一个单调递减栈处理两侧的下一个更大值。预处理成 \(0\) 和 \(n + 1\) 肯定没错。
对于 \(H_{i}\) ,左一个更大值和右一个更大值为 \(H_{l_i}, H_{r_i}\) 。则 \(val_{l_i} += V_i, val_{r_i} += V_i\) 。
弹出逻辑为 \(a_i > a_{stk.top()}\) 。
统计完贡献后可以回答。
int n; std::cin >> n;
std::vector<int> H(n + 1), V(n + 1);
std::vector<int> l(n + 1, 0);
std::vector<int> r(n + 1, n + 1);
std::vector<ll> val(n + 1);
for (int i = 1; i <= n; i++) {
std::cin >> H[i] >> V[i];
}
std::stack<int> stk;
for (int i = 1; i <= n; i++) {
while (!stk.empty() && H[i] > H[stk.top()]) {
r[stk.top()] = i;
stk.pop();
}
stk.push(i);
}
while (!stk.empty()) stk.pop();
for (int i = n; i; --i) {
while (!stk.empty() && H[i] > H[stk.top()]) {
l[stk.top()] = i;
stk.pop();
}
stk.push(i);
}
for (int i = 1; i <= n; i++) {
if (l[i] >= 1) val[l[i]] += V[i];
if (r[i] <= n) val[r[i]] += V[i];
}
std::cout << *max_element(val.begin() + 1, val.end()) << "\n";
时间复杂度 \(O(n)\) 。
P9461 「EZEC-14」众数 II https://www.luogu.com.cn/problem/P9461
P9607 [CERC2019] Be Geeks! https://www.luogu.com.cn/problem/P9607
P10169 [DTCPC 2024] mex,min,max https://www.luogu.com.cn/problem/P10169
Ⅱ 论文:wzk 浅谈极大化思想——如在最大子矩形中的应用
具体还是参考论文原文。
极大化思想解决最大子矩阵问题
把这题放到这里的原因是想说:子矩阵问题别用单调栈写。不仅二维不方便,甚至一维问题都很多用 笛卡尔 树代替。
也不要直接用 \(O(n^{3})\) 的 DP 写。
一 问题:
在一个给定的矩形网络中有一些障碍点,要找出网格内部不包含任何障碍点,且边界与坐标轴平行的最大子矩形。
二 定义:
有效子矩形
内部不包含任何障碍点,且边界与坐标轴平行的子矩形。
极大有效子矩形
一个有效子矩形,如果不存在包含它且比它大的有效子矩形,则它是极大有效子矩形。
最大有效子矩形
所有有效子矩形中最大的一个。
三 极大化思想
定理 1 : 在一个有障碍点的矩形中,最大有效子矩形一定是极大有效子矩形。
证明: 如果最大有效子矩形不是一个极大有效子矩形,根据极大有效子矩形的定义,存在一个有效子矩形更大。与最大有效子矩形的最大性矛盾。\(\square\)
四 两种算法
定理 1 得到算法的思路:枚举所有的极大有效子矩形,就可以得到最大有效子矩形。
约定: 为了方便描述,定义整个矩形的大小为 \(n \times m\) ,其中障碍点个数为 \(s\) 。
算法 1
根据枚举所有极大有效子矩形的思路,如果某次枚举的矩形不是有效子矩形,那么这个算法是不优的。
定理 2 : 一个有效子矩形是极大有效子矩形当前仅当这个子矩形的每条边覆盖了一个障碍点或与整个矩形的边界重合。
充分性显然,必要性显然。
根据定理 2 ,可以得到一个算法:首先不妨在障碍点的集合中加上整个矩形的四个角点作为约束边界的障碍点,称作点集 \(\mathbb{S}\) 。然后枚举四个障碍点约束矩形的四个边界,然后检查矩形内部是否有障碍点。时间复杂度 \(O(s^{5})\) 。
显然直接枚举四个边界会包含大量无效子矩形。
考虑将 \(\mathbb{S}\) 中的点按纵坐标排序,只枚举左右边界,然后竖直枚举确定出水平边界。时间复杂度 \(O(s^{3})\) 。
现在可以确定一定能枚举到所有极大有效子矩形,过程中枚举到的子矩形也是有效的。
同时这也意味着枚举到的每个子矩形是有效的,但不是极大的。更进一步地意味着算法还有优化空间。
接下来是 算法 1 。在 \(\mathbb{S}\) 中选择一个点约束出左边界,默认上下边界为整个矩形的上下边界,将点按横坐标排序后,从左往右扫描。
对遇到的每个障碍点更新上下边界的约束,并确定右边界。
以每个障碍点为左边界约束的极大子矩形都能被得到,是否所有极大有效子矩形都被得到了?还没有考虑边界与上下边界重合的情况。
只需要将点重新按照纵坐标排序,从下往上扫描。即可保证所有极大有效子矩形都被得到。
包含所有极大有效子矩形的集合的最大矩形一定是最大有效子矩形。因为不存在一个有效子矩形不是极大有效子矩形而比最大子矩形大。
时间复杂度 \(O(s^{2})\) ,空间瓶颈最少是 \(O(s)\) 。
只依赖障碍点,不依赖矩形大小。称作“障碍点算法”。
P1578 奶牛浴场 https://www.luogu.com.cn/problem/P1578
题意:
此处要求的是最大子正方形而不是最大子矩形。
复用有效子矩形的定义到有效子正方形上,于是有。
给一个 \(L \times W\) 的矩形牛场,有 \(n\) 个点表示奶牛的产奶点,第 \(i(1 \leq i \leq n)\) 个产奶点的坐标为 \((x_i, y_i)\) 。询问能够在牛场中建造一个浴场的最大面积是多大。要求浴场是与牛场边界平行的矩形,且不能覆盖产奶点。
\(0 \leq n \leq 5 \times 10^{3}, 1 \leq L, W \leq 3 \times 10^{4}, 0 \leq x_i \leq L, 0 \leq y_i \leq W\)
题解:
\(L \times W\) 很大,但是障碍点 \(n\) 不多。于是可以用 \(O(n^{2})\) 的障碍点算法求最大子矩形。
一般按照笛卡尔坐标系建系,手动重载运算符排序。
int L, W, n; std::cin >> L >> W >> n;
std::vector<std::array<int, 2> > p(n + 1 + 4); // (x, y)
for (int i = 1; i <= n; i++) {
int x, y; std::cin >> x >> y;
p[i] = {x, y};
}
p[n + 1] = {0, 0};
p[n + 2] = {L, W};
p[n + 3] = {0, W};
p[n + 4] = {L, 0};
n += 4;
ll ans = 0;
std::sort(p.begin() + 1, p.end());
for (int i = 1; i <= n; i++) {
int up = 0, down = W;
for (int j = i + 1; j <= n; j++) {
ans = std::max(ans, 1LL * (down - up) * (p[j][0] - p[i][0]));
if (p[j][1] < p[i][1]) up = std::max(up, p[j][1]);
else down = std::min(down, p[j][1]);
}
}
std::sort(p.begin() + 1, p.end(), [&](std::array<int, 2> x, std::array<int, 2> y){
if (x[1] == y[1]) return x[0] < y[0];
else return x[1] < y[1];
});
for (int i = 1; i <= n; i++) {
int left = 0, right = L;
for (int j = i + 1; j <= n; j++) {
ans = std::max(ans, 1LL * (right - left) * (p[j][1] - p[i][1]));
if (p[j][0] < p[i][0]) left = std::max(left, p[j][0]);
else right = std::min(right, p[j][0]);
}
}
std::cout << ans << "\n";
算法 2
极大化思想依然可以推出一种不依赖障碍点,只依赖矩形大小的算法。称作“悬线法”。
定义
有效竖线
只允许两个端点存在障碍点的竖线
悬线
上端点覆盖了一个障碍点或达到整个矩形上端的有效竖线
将极大有效子矩形按照 \(x\) 坐标不同分类,可以分成无数条与 \(y\) 轴平行的竖线。这些竖线中一定存在悬线。
对于一条悬线,尽可能向左右扩展,一定可以得到一个有效子矩形(但不一定是极大有效子矩形)。
定理 3 : 所有悬线向左右扩展得到的有效子矩形集合,一定包含所有极大有效子矩形。
证明:
显然一个极大有效子矩形可以由悬线扩展得到。且悬线扩展出的一定是有效子矩形。
\(\square\)
根据悬线的定义,每条悬线一定与底部的点一一对应,所以悬线最多有 \(O(n m)\) 条。如果能做到 \(O(1)\) 对悬线进行扩展,则能得到一个包含所有极大有效子矩形的有效子矩形集合。
对每条悬线根据底部 \((i, j)\) 分类。每个悬线会有三个属性:高度 \(h_{i, j}\) 、左右能扩展到的最远位置 \(left_{i, j}, right_{i, j}\) 。
这三个属性是经典的可以递推的函数!
具体为何可以递推参考论文原文,这里做简单解释:
若 \((i - 1, j)\) 是障碍点。
显然有 \(h_{i, j} = 1, left_{i, j} = 0, right_{i, j} = m\) 。
若 \((i - 1, j)\) 不是障碍点。
\(h_{i, j} = h_{i - 1, j} + 1\)
\(left_{i, j} = max(left_{i - 1, j}, (i - 1, j) 左边第一个障碍点的位置)\) 。
\(right_{i, j} = min(right_{i - 1, j}, (i - 1, j) 右边第一个障碍点的位置)\) 。
不妨设 \((i, j)\) 左边第一个障碍点的位置为 \(obstacle\_L_(i, j)\) 。
- 若 \((i, j)\) 是障碍点。\(obstacle\_L_{i, j} = j\) 。(边界是障碍点)
- 若 \((i, j)\) 不是障碍点。\(obstacle\_L_{i, j} = obstacle\_L_{i, j - 1}\) 。
设 \((i, j)\) 右边第一个障碍点的位置为 \(abstacle\_R_(i, j)\) 。
- 若 \((i, j)\) 是障碍点。\(obstacle\_R_{i, j} = j\) 。(边界是障碍点)
- 若 \((i, j)\) 不是障碍点。\(obstacle\_R_{i, j} = obstacle\_R_{i, j + 1}\) 。
答案询问
\(ans = max(ans, (right_{i, j} - left_{i, j}) \times h_{i, j})\) 。
推广 1 最大权值子矩阵问题
模型: 在一个非负权矩阵中有一些障碍点,找出一个不包含障碍点的最大权值子矩形。
分析: 在一个非负权矩阵矩阵中,一个最大权值子矩阵一定是一个极大有效子矩阵。正确性可以反证。
类型转换: 矩形的点是两条垂直线段的交点,有效矩形允许与障碍点非严格相交。矩阵的点是单位方格,有效矩阵不能包含障碍点。处理方法具体分析。
细节处理递推式,前缀和,可以解决类似问题。
更新定义:
有效子矩阵: 矩阵内严格不包含障碍点的子矩阵。
1. 如果使用悬线法
更新定义:
悬线: 悬线上端点的上一个点是障碍点或矩阵上边界。
有效竖线: 只允许上端点的上一个点是障碍点,下端点的下一个点是障碍点的悬线
重新处理递推式
若 \((i - 1, j)\) 是障碍点。
显然有 \(h_{i, j} = 0, left_{i, j} = 0, right_{i, j} = m + 1\) 。
若 \((i - 1, j)\) 不是障碍点。
\(h_{i, j} = h_{i - 1, j} + 1\)
\(left_{i, j} = max(left_{i - 1, j}, (i, j) 左边第一个障碍点的位置 + 1\) 。
\(right_{i, j} = min(right_{i - 1, j}, (i, j) 右边第一个障碍点的位置 - 1\) 。
答案询问
\(x2 = i, x1 = i - h_{i, j} + 1, y1 = left_{i, j} + 1, y2 = right_{i, j} - 1\) 。(矩阵可能为空矩阵)
\(ans = max(ans, pre[x2][y2] - pre[x2][y1 - 1] - pre[x1 - 1][y2] + pre[x1 - 1][y1 - 1])\) 。
不妨让障碍点为 \(-1\) ,不影响前缀和处理。
2. 如果使用障碍点法
大多情况下带非负权的矩阵已经可以使用悬线法了,否则不能读入权。
少数情况下依旧需要使用障碍点法做。
P4147 玉蟾宫 https://www.luogu.com.cn/problem/P4147
题意:
给一个 \(N \times M\) 矩阵,单元格要么是障碍点,要么有 \(1\) 的权值。询问权值最大的子矩阵,输出这个权值 \(\times 3\)。
\(1 \leq N, M \leq 1000\)
障碍点 \(s\) 最多会达到 \(O(N M)\) 级别,所以适用于悬线法。可以直接计算面积,但不妨处理前缀和。
int n, m; std::cin >> n >> m;
std::vector<std::vector<int> > G(n + 1, std::vector<int>(m + 2, -1));
std::vector<std::vector<ll> > S(n + 1, std::vector<ll>(m + 2));
std::vector<std::vector<int> > h(n + 1, std::vector<int>(m + 2, 0));
std::vector<std::vector<int> > left(n + 1, std::vector<int>(m + 2, 0));
std::vector<std::vector<int> > right(n + 1, std::vector<int>(m + 2, m + 1));
std::vector<std::vector<int> > obstacle_L(n + 1, std::vector<int>(m + 2, 0));
std::vector<std::vector<int> > obstacle_R(n + 1, std::vector<int>(m + 2, m + 1));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
char c; std::cin >> c;
if (c == 'F') G[i][j] = 1;
else G[i][j] = -1;
S[i][j] = G[i][j];
}
}
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
S[i][j] += S[i - 1][j];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
S[i][j] += S[i][j - 1];
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++) {
if (G[i][j] == -1) h[i][j] = 0, left[i][j] = 0, right[i][j] = m + 1;
else h[i][j] = h[i - 1][j] + 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (G[i][j] == -1) obstacle_L[i][j] = j;
else obstacle_L[i][j] = obstacle_L[i][j - 1];
}
for (int j = m; j; --j) {
if (G[i][j] == -1) obstacle_R[i][j] = j;
else obstacle_R[i][j] = obstacle_R[i][j + 1];
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (G[i][j] != -1) left[i][j] = std::max(left[i - 1][j], obstacle_L[i][j]);
}
for (int j = m; j >= 1; --j) {
if (G[i][j] != -1) right[i][j] = std::min(right[i - 1][j], obstacle_R[i][j]);
}
}
ll ans = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int x1 = i - h[i][j] + 1, x2 = i, y1 = left[i][j] + 1, y2 = right[i][j] - 1;
if (x1 <= x2 && y1 <= y2)
ans = std::max<ll>(ans, S[x2][y2] + S[x1 - 1][y1 - 1] - S[x1 - 1][y2] - S[x2][y1 - 1]);
}
}
std::cout << ans * 3 << "\n";
推广 2 最大子正方形问题
模型: 在一个矩形中存在 \(s\) 个障碍点,要找出最大的不包含障碍点的正方形。
分析: 在一个有障碍点的矩形中,最大有效子正方形一定是极大有效子正方形。
定理: 一个极大有效子正方形至少被一个极大有效子矩形包含。且这个包含它的极大有效子矩形至少有两条边与它重合(否则该有效子正方形不是极大)。并且边长是矩形的短边长(否则该有效子正方形不是极大)。
由上述定理可以得到解决方法: 枚举每个极大有效子矩形,可以找到所有极大有效子正方形。
P2701 [USACO5.3] 巨大的牛棚Big Barn https://www.luogu.com.cn/problem/P2701
这题如果带权, DP 会达到 \(O(n^{3})\) 。
这里视作带权但为 \(1\) 的正方形矩阵。可以 \(O(n^{2})\) 的悬线法或 \(O(t^{2})\) 的障碍点法解决。
此题显然使用悬线法更优,但是这里使用障碍点法处理矩阵(因为恰好还缺少这个处理方法的实现)。
题意:
有一个 \(n \times n\) 的农场,需要在上面修建一个和农场边界平行的正方形牛棚。农场中有 \(t\) 棵果树,第 \(i(1 \leq i \leq t)\) 棵树的坐标为 \((x_i, y_i), (1 \leq x_i, y_i \leq n)\) 牛棚不能建造在果树上。询问牛棚最大能覆盖多少面积。
\(1 \leq n \leq 1000, 1 \leq t \leq 10000\)
题解:
Ⅲ 带负权矩阵的最大权子矩阵
如果带负权矩阵中存在障碍点,那么是不太可解决的问题。
否则是 DP 问题。