题解:P3271 [JLOI2016] 方

· · 题解

入手

对于这种计数题,可以先去尝试枚举所有可行的正方形,但是本题中点的个数极多,不好计数。
考虑正难则反,既然合法正方形计数难,就枚举非法正方形,可这么做又容易重复。

处理重复可以使用容斥原理

解法

下文讨论的“正方形”的顶点都在格点上。

S_t 表示:枚举所有大小为 t 的被删点集合,对每个集合计算四个顶点包含该集合中全部 t 个被删点的正方形个数,再把这些个数加起来。

根据容斥原理,答案即为 $S_0-S_1+S_2-S_3+S_4$(由于正方形只有四个顶点 $S_5,S_6,\ldots$ 均为 $0$,无需考虑)。 在本题中,分别计算 $S_0,S_1,S_2,S_3,S_4$ 即可,建议自己思考这五种情况,但在这边也提供一下大致解法。 ::::info[$S_0$] 一个正方形(红色)可以视作嵌在某个大正方形内。 ![](https://cdn.luogu.com.cn/upload/image_hosting/qt0t9nv2.png) 枚举大正方形边长,即可知道大正方形的数量,再乘上一个大正方形内小正方形的数量。 $$S_0=\sum\limits_{d=2}^{\min(n,m)}(n-d+1)(m-d+1)(d-1)$$ :::: ::::info[$S_1$] 枚举一个被删点,计数正方形。 ![](https://cdn.luogu.com.cn/upload/image_hosting/aslu5mzn.png) 假设枚举到这个红点,然后计数右边绿色正方形可能的数量。 绿色正方形可以视作嵌在一个大正方形内,可以计数大正方形的数量。 本题解代码中对四个方向的大正方形分开计数,且绿色正方形和大正方形重合的情况也是分开的。 :::: ::::info[$S_2$] $O(K^2)$ 枚举每对被删点,计算它们连边作为正方形对角线以及边的情况数。 :::: ::::info[$S_3$] $O(K^2)$ 枚举每对被删点,把它们连边作为正方形对角线,此时使用 `set` 等方式判断该正方形另外两个顶点是否有被删点即可。 :::: ::::info[$S_4$] $O(K^2)$ 枚举每对被删点,把它们连边作为正方形对角线,此时使用 `set` 等方式判断该正方形另外两个顶点是否全为被删点即可。 :::: # 代码 ```cpp #include <bits/stdc++.h> using namespace std; typedef long long ll; const int MOD = 1e8 + 7; const int N = 2009; int n, m, K; int x[N], y[N]; ll S0, S1, S2, S3, S4; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> m >> K; // 转化为格点数,坐标变为 1‑based n++; m++; for (int i = 1; i <= K; i++) { cin >> x[i] >> y[i]; x[i]++; y[i]++; } // 用 set 快速判断一个点是否是被删掉的坏点 set<pair<int, int>> bad; for (int i = 1; i <= K; i++) { bad.emplace(x[i], y[i]); } // ========== S0 : 所有正方形的总数 ========== for (int d = 2; d <= min(n, m); d++) { S0 = (S0 + 1LL * (n - d + 1) * (m - d + 1) % MOD * (d - 1)) % MOD; } // ========== S1 : 包含某个被删点的正方形个数之和 ========== // 对于一个点 x,其垂直方向上方还有 a 格空间,垂直下方还有 b 格空间,水平方向上右边还有 c 格空间,在这个 (a+1+b)*(c+1) 的空间内,calc(a,b,c) 表示有多少一条边包含 x 的大正方形(不包括顶点在 x 上的正方形)(“大正方形”可以见题解S1的大致讲解)。 auto calc = [&](ll a, ll b, ll c) -> ll { if (c <= 1) return 0; a = min(a, c - 1); b = min(b, c - 1); ll ans = a * b; ll t = max(b - (c - a), 0LL); ans -= t * (t + 1) / 2; return ans; }; for (int i = 1; i <= K; i++) { ll cnt = 0; // 斜正方形:大正方形的四个方向 cnt += calc(x[i] - 1, n - x[i], m - y[i]); cnt += calc(x[i] - 1, n - x[i], y[i] - 1); cnt += calc(y[i] - 1, m - y[i], n - x[i]); cnt += calc(y[i] - 1, m - y[i], x[i] - 1); // 正放正方形:i号点是正方形顶点 cnt += min(x[i] - 1, y[i] - 1); cnt += min(x[i] - 1, m - y[i]); cnt += min(n - x[i], y[i] - 1); cnt += min(n - x[i], m - y[i]); cnt %= MOD; S1 = (S1 + cnt) % MOD; } // ========== S2, S3, S4 : 枚举坏点对生成正方形 ========== // 情形 1 : 以 (x[i],y[i]) 和 (x[j],y[j]) 为正方形的一条边 for (int i = 1; i <= K; i++) { for (int j = 1; j <= K; j++) { if (i == j) continue; int dx = x[j] - x[i]; int dy = y[j] - y[i]; int px1 = x[i] + dy; int py1 = y[i] - dx; int px2 = x[j] + dy; int py2 = y[j] - dx; if (px1 < 1 || px1 > n) continue; if (px2 < 1 || px2 > n) continue; if (py1 < 1 || py1 > m) continue; if (py2 < 1 || py2 > m) continue; S2++; } } // 情形 2 : 以 (x[i],y[i]) 和 (x[j],y[j]) 为对角线 for (int i = 1; i <= K; i++) { for (int j = i + 1; j <= K; j++) { int sumx = x[i] + x[j]; int sumy = y[i] + y[j]; int dx = x[i] - x[j]; int dy = y[i] - y[j]; // 另外两个顶点的 2 倍坐标 int px1_2 = sumx - dy; int py1_2 = sumy + dx; int px2_2 = sumx + dy; int py2_2 = sumy - dx; if (px1_2 & 1 || py1_2 & 1 || px2_2 & 1 || py2_2 & 1) continue; int px1 = px1_2 / 2, py1 = py1_2 / 2; int px2 = px2_2 / 2, py2 = py2_2 / 2; if (px1 < 1 || px1 > n) continue; if (px2 < 1 || px2 > n) continue; if (py1 < 1 || py1 > m) continue; if (py2 < 1 || py2 > m) continue; S2++; bool A = bad.count({px1, py1}); bool B = bad.count({px2, py2}); S3 += A + B; S4 += (A && B); } } S4 /= 2; // 每个四坏点正方形被两条对角线重复计算了一次 // ========== 容斥原理 ========== S0 %= MOD; S1 %= MOD; S2 %= MOD; S3 %= MOD; S4 %= MOD; ll ans = (S0 - S1 + S2 - S3 + S4) % MOD; ans = (ans + MOD) % MOD; cout << ans << "\n"; return 0; } ``` ::::info[AI 使用声明] 代码由 Deepseek 优化过码风,部分注释也由其生成。 ::::