(低技术力预警)
有时我们希望对 01 矩阵中,每个全 1 子矩阵做一些统计。
一种通用的方法是单调栈。我们从上到下枚举行,然后考察以该行为底边的所有全 1 子矩阵。利用单调栈,我们可以得到以该行为底边的所有极大子矩阵。
假设对一个高为 h h h、宽为 w w w 的矩形,其权值为 f ( h , w ) f(h, w) f(h,w),那么对一个高为 h h h、宽为 w w w 的极大子矩阵而言,其包含的所有矩形的贡献和为
s u m ( h , w ) = ∑ h ′ = 1 h ∑ w ′ = 1 w ( w − w ′ + 1 ) f ( h ′ , w ′ ) sum(h, w) = \sum_{h'=1}^{h}\sum_{w' = 1}^{w} (w-w'+1)f(h', w') sum(h,w)=h′=1∑hw′=1∑w(w−w′+1)f(h′,w′)
设 g ( h , w ′ ) = ∑ h ′ = 1 h f ( h ′ , w ′ ) g(h, w') = \sum_{h'=1}^{h}f(h', w') g(h,w′)=∑h′=1hf(h′,w′),有
s u m ( h , w ) = ∑ w ′ = 1 w ( w − w ′ + 1 ) g ( h , w ′ ) = w ∑ w ′ = 1 w g ( h , w ′ ) − ∑ w ′ = 1 w ( w ′ − 1 ) g ( h , w ′ ) sum(h, w) = \sum_{w' = 1}^{w} (w-w'+1)g(h, w') = w \sum_{w' = 1}^{w} g(h, w') - \sum_{w' = 1}^{w} (w'-1)g(h, w') sum(h,w)=w′=1∑w(w−w′+1)g(h,w′)=ww′=1∑wg(h,w′)−w′=1∑w(w′−1)g(h,w′)
如果我们能快速计算出 f f f 和 g g g,就能通过预处理前缀和,从而在 O ( 1 ) O(1) O(1) 内计算出 s u m ( h , w ) sum(h, w) sum(h,w)。
然而我们不能简单的把所有极大子矩阵的贡献全部加起来,因为可能会算重。例如:
1 1
1 1
1 1 1
1 1 1
1 1 1 1
1 1 1 1
在单调栈出栈的时候,三个极大子矩阵相互有重合的部分。因此还需要在出栈时通过容斥去掉这些算重的部分。
具体怎么容斥,看代码比较方便:
#define REP(temp, init_val, end_val) for (int temp = init_val; temp <= end_val; ++temp)
#define REPR(temp, init_val, end_val) for (int temp = init_val; temp >= end_val; --temp)
stack<pair<int, int> > st;
// height[j] 是第 j 列的高
height[n + 1] = 0;
int ans = 0;
REP(i, 1, n){
REP(j, 1, n){
if (s[i][j] == '0') height[j] = 0;
else ++height[j];
}
while (!st.empty()) st.pop();
REP(j, 1, n + 1){
int curw = 0;
while (!st.empty() && st.top().first > height[j]){
if (curw > 0)
ans -= sum[st.top().first][curw];
curw += st.top().second;
ans += sum[st.top().first][curw];
st.pop();
}
if (curw > 0)
ans -= sum[height[j]][curw];
if (!st.empty() && st.top().first == height[j]){
st.top().second += curw + 1;
} else if (height[j] > 0){
st.emplace(height[j], curw + 1);
}
}
}
例 1
题意:给定一个 01 矩阵,问有多少个全 1 子矩阵满足一条边的边长是另一条边边长的倍数。(来源:2020 计蒜之道线上决赛 E)
令 f ( h , w ′ ) = ∑ h ′ = 1 h [ h ′ ∣ w ′ ∨ w ′ ∣ h ′ ] f(h, w') = \sum_{h' = 1}^{h} [h' | w' \vee w' | h'] f(h,w′)=∑h′=1h[h′∣w′∨w′∣h′] 即可套用上述框架。
例 2
题意:给出一个 R × C R\times C R×C 的网格图,其中有些格子被涂成黑色。多次询问,给定宽 w w w 和高 h h h,问网格图中有多少个位置可以放置 w × h w \times h w×h 的矩形(不可旋转),满足矩形内没有黑色的格子,且边与网格线重合。(来源:SWERC 2017 B 题)
询问 w × h w\times h w×h 时,令 f ( x , y ) = [ x ≥ h ∧ y ≥ w ] f(x, y) = [x \ge h \wedge y \ge w] f(x,y)=[x≥h∧y≥w] 即可套用上述框架。
由于有很多组询问,因此需要预处理每一组 ( w , h ) (w, h) (w,h) 的答案,故在累积答案时需要用到前缀和的技巧。具体细节可以参考下面的代码。
#include <bits/stdc++.h>
using namespace std;
stack<pair<int, int> > st;
int X, Y, N, D;
int block[2005][2005] = {
0}, hh[2005] = {
0};
int ans[2005][2005] = {
0};
int main(){
scanf("%d%d%d%d", &X, &Y, &N, &D);
// 处理出黑色区域
for (int i = 1, xx1, xx2, yy1, yy2; i <= N; ++i){
scanf("%d%d%d%d", &xx1, &xx2, &yy1, &yy2);
++block[xx1 + 1][yy1 + 1];
--block[xx1 + 1][yy2 + 1];
--block[xx2 + 1][yy1 + 1];
++block[xx2 + 1][yy2 + 1];
}
for (int i = 1; i <= X; ++i)
for (int j = 1; j <= Y; ++j)
block[i][j] += block[i][j - 1];
for (int i = 1; i <= X; ++i)
for (int j = 1; j <= Y; ++j)
block[i][j] += block[i - 1][j];
// 应用单调栈计算答案
hh[X + 1] = 0;
for (int j = Y; j >= 1; --j){
for (int i = 1; i <= X; ++i){
if (block[i][j]) hh[i] = 0;
else ++hh[i];
}
while (!st.empty()) st.pop();
for (int i = 1; i <= X + 1; ++i){
int curw = 0;
while (!st.empty() && st.top().first > hh[i]){
if (curw > 0)
ans[st.top().first][curw] -= 1;
curw += st.top().second;
ans[st.top().first][curw] += 1;
st.pop();
}
if (curw > 0)
ans[hh[i]][curw] -= 1;
if (!st.empty() && st.top().first == hh[i]){
st.top().second += curw + 1;
} else if (hh[i] > 0){
st.emplace(hh[i], curw + 1);
}
}
}
// 做两次后缀和,再对 h 这一维做一次后缀和,方便回答
for (int i = 1; i <= Y; ++i)
for (int j = X - 1; j >= 1; --j)
ans[i][j] += ans[i][j + 1];
for (int i = 1; i <= Y; ++i)
for (int j = X - 1; j >= 1; --j)
ans[i][j] += ans[i][j + 1];
for (int j = 1; j <= X; ++j)
for (int i = Y - 1; i >= 1; --i)
ans[i][j] += ans[i + 1][j];
// 回答询问
for (int i = 1, qw, qh; i <= D; ++i){
scanf("%d%d", &qw, &qh);
printf("%d\n", ans[qh][qw]);
}
return 0;
}