洛谷 P7715 Shape
1 P7715 Shape
2 题目描述
时间限制 \(500ms - 1s\) | 空间限制 \(128M\)
小 \(A\) 有一个 \(n\times m\) 的网格,一些为白色格子,剩余为黑色格子。
小 A 选择四个整数 \(x_1,x_2,y_1,y_2\),满足如下条件:
-
1、$ 1\le x_1<x_2\le n$ 且 $ 1\le y_1<y_2\le m$。
-
2、$ x_1+x_2$ 为偶数。
若 \((x_1,y_1)\to (x_2,y_1),(x_1,y_2)\to (x_2,y_2),(\frac{x_1+x_2}{2},y_1)\to (\frac{x_1+x_2}{2},y_2)\) 这三段中的格子均为白色,则称这三段构成的图形为 \(H\) 形。
小 \(A\) 想知道,这个网格中存在多少不同的 \(H\) 形。
两个 \(H\) 形相同,当且仅当两个 \(H\) 形的 \(x_1,x_2,y_1,y_2\) 均相同。
数据范围:\(2≤n,m≤2×10^3\)
3 题解
先考虑一个纯粹的暴力:看到题目给你的那个 \(x_1, x_2, y_1, y_2\) 了吗?直接枚举它。看到那个全部是白色了吗?直接 \(O(n)\) 枚举它。这样我们就得到了一个时间复杂度 \(O(n^5)\) 的优异算法。可惜这道题的 \(n\) 有点大,我们只能拿个 \(10\) 分。
我们考虑如何优化:全是白色显然可以用二维前缀和 \(O(n^2)\) 预处理,\(O(1)\) 查询。这样我们只需要枚举 \(x_1, x_2, y_1, y_2\) 即可,时间复杂度是 \(O(n^4)\)。
显然,这样的时间复杂度不够优秀,我们只能拿到 \(20\) 分。好像优化不太动了,我们换个思路。
我们选择枚举中间的那个横杠,这只需要 \(O(n^3)\) 的时间复杂度。然后我们对于当前横杠的两端,可以直接用二分搞。具体来说,二分查找两端一起向上下能延伸(上、下均不碰到黑色块)是多少,设左端点向上向下一起延伸的距离为 \(lans\),右端点向上向下一起延伸的距离为 \(rans\),包含这一段横杠的 \(H\) 形个数就是 \(min(lans, rans)\)。这样,我们就得到了一个预处理 \(O(n^2)\),找 \(H\) 形 \(O(n^3 log_2n)\) 时间复杂度的算法。由于时间复杂度确实比较劣,所以还是 \(20\) 分。(剩下两个点都是卡线没过,过掉就 \(50\) 分了)
我们仔细想想,就发现根本没有必要把向上向下延伸的距离那一部分放在找 \(H\) 的时候处理。我们完全可以在前面直接对于每一个点二分找出它向上向下能延伸多少。这样,时间复杂度就是 \(O(n^2log_2n)\) 预处理,\(O(n^3)\) 找 \(H\) 形了。于是我们就顺利拿到了 \(50\) 分。
再下手就必须在这个 \(O(n^3)\) 找横杠上下手了。容易发现,我们只需要固定左端点,然后向右二分查找最后一个白色的位置即可。如果左端点本来就是黑色块,我们就往右一直移动到白色块为止。找到右端点之后,就固定右端点不动,只让左端点向右移动,这样就一定可以覆盖到所有的横杠。
找到左右端点之后,我们又该如何算出这一段的贡献呢?容易发现,这一段横杠在左端点固定的时候,设左端点的位置为 \(i\),右端点的位置为 \(j\),\(a_k\) 表示在 \(k\) 位置向上向下的延伸距离,答案就是:\(\sum_{k = i+1}^j \limits min(a_k, a_i)\)。
我们发现这是个区间查询,于是考虑使用高级数据结构来维护一下。我们想到,如果没有 \(min\),这就是一个简单的区间和。但是有了 \(min\) 之后,我们就不能单纯地用线段树直接维护了,我们考虑使用权值线段树。这是因为权值线段树有一个好处:可以分出比 \(a_i\) 大的数和比 \(a_i\) 小的数。这里的区间和,其实就可以看成每一个数出现的次数乘以那个数本身。至于大于 \(a_i\) 的数,我们数出他们的个数,然后直接用这个个数乘上 \(a_i\) 加到答案中即可。
具体地,我们维护两个值,一个是每一个位置上数的出现次数乘以那个数本身的值,一个是那个数出现的次数。容易发现,第一个信息就相当于在修改时直接将某个数对应的数的位置上直接加上那个数。并且,我们发现,我们每次固定右端点之后,可以先把左端点到右端点之间所有的数都推到权值线段树中。容易发现,每一个数都只会被推入权值线段树一次,所以这块的时间复杂度就是 \(O(n^2log_2n)\),可以接受。而我们的左端点的延伸长度由于不能看做右端点的延伸长度(\(H\) 形的左右竖杠不能重合),可以在每次查找 \(H\) 形之前将现在的左端点的信息从权值线段数中去除。这样,我们将这个右端点的所有横杠都走完后,权值线段树正好清空。
这样,我们的算法就是 \(O(n^2log_2n)\) 预处理,\(O(n^2log_2n)\) 找 \(H\) 形了。但是,我们仍然不能拿到 \(100\) 分,因为常数太大了。我们首先考虑把权值线段树改为树状数组,发现仍然被卡常。
我们此时再次观察预处理,发现这里面有很多向上向下延伸部分重复了。于是我们重新设计这一部分:设 \(up_{i, j}\) 为向上可以延伸的最远距离,\(down_{i, j}\) 为向下可以延伸的最远距离。我们先考虑如何计算出 \(up\) 数组:我们从上到下整张图,保证当我们计算 \(up_{i, j}\) 的时候,\(up_{i-1, j}\) 已经被计算出来了。此时,如果当前位置上面的位置的数为 \(1\),那么我们最多就可以向上延伸 \(0\) 格;否则我们可以向上延伸 \(up_{i-1, j}+1\)格。在计算 \(down\) 数组时同理,只需要将遍历顺序改为从下到上即可。最后,\((i, j)\) 可以向上向下延伸的最大长度就是 \(min(up_{i, j}, down_{i, j})\)。
这样,我们的算法就变成了 \(O(n^2)\) 预处理,\(O(n^2log_2n)\) 求 \(H\) 形的时间复杂度,终于可以通过了!我们这个时候再看看,发现由于我们的预处理改掉了,所以二维前缀和也就不需要了:我们只在计算右端点的时候需要横着的一行的一部分的和,只用维护一个一位前缀和记录每一行的前 \(i\) 个数的和即可。
4 代码(空格警告):
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 2e3+10;
typedef long long ll;
int n, m, l, r, mid, flag;
ll ans, cnt;
int Map[N][N], sum[N][N], len[N][N], c1[N], c2[N];
int up[N][N], down[N][N];
ll query1(int x)
{
ll res = 0;
while (x)
{
res += c1[x];
x -= (x & (-x));
}
return res;
}
void add1(int x, int d)
{
while (x <= n)
{
c1[x] += d;
x += (x & (-x));
}
}
ll query2(int x)
{
ll res = 0;
while (x)
{
res += c2[x];
x -= (x & (-x));
}
return res;
}
void add2(int x, int d)
{
while (x <= n)
{
c2[x] += d;
x += (x & (-x));
}
}
int read()
{
int x = 0, f = 1;
char c = getchar();
while (c < '0' || c > '9')
{
if (c == '-') f = -1;
c = getchar();
}
while (c >= '0' && c <= '9')
{
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
void write(ll x)
{
if(x<0)
putchar('-'),x=-x;
if(x>9)
write(x/10);
putchar(x%10+'0');
return;
}
int Min(int x, int y)
{
if (x < y) return x;
return y;
}
int main()
{
n = read(), m = read();
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
Map[i][j] = read();
sum[i][j] = sum[i][j-1] + Map[i][j];
}
}
for (int i = 2; i < n; i++)
{
for (int j = 1; j <= m; j++)
{
if (Map[i-1][j]) up[i][j] = 0;
else up[i][j] = up[i-1][j] + 1;
}
}
for (int i = n-1; i >= 2; i--)
{
for (int j = 1; j <= m; j++)
{
if (Map[i+1][j]) down[i][j] = 0;
else down[i][j] = down[i+1][j] + 1;
}
}
for (int i = 2; i < n; i++)
for (int j = 1; j <= m; j++)
len[i][j] = Min(up[i][j], down[i][j]);
for (int i = 2; i < n; i++)
{
flag = 1;
for (int j = 1; j <= m; j++)
{
if (Map[i][j])
{
flag = 1;
continue;
}
if (!Map[i][j] && flag)
{
flag = 0;
l = j;
r = m;
mid = (l + r) >> 1;
while (l <= r)
{
if (sum[i][mid] - sum[i][j-1] == 0)
{
l = mid + 1;
ans = mid;
}
else r = mid - 1;
mid = (l + r) >> 1;
}
for (int k = j; k <= ans; k++)
{
if (!len[i][k]) continue;
add1(len[i][k], 1);
add2(len[i][k], len[i][k]);
}
}
if (!len[i][j]) continue;
add1(len[i][j], -1);
add2(len[i][j], -len[i][j]);
cnt += (query1(n) - query1(len[i][j])) * (len[i][j]) + query2(len[i][j]);
}
}
write(cnt);
return 0;
}