通过二分答案,将判定边长为 L 的正方形退化为二维平面的禁放矩形覆盖问题,用离散化与二维差分实现 O(T^2) 高效判定。
OJ: roj
题目 ID: 20029
难度:提高
标签:二分答案二维差分离散化
创建: 2026-10-09 21:46
更新: 2026-10-09 21:46
形式化题目
给定一个
正解
思路
因为
设当前二分的边长为
对于每个障碍物
这转化成了典型的“矩形面积并 / 离散化网格覆盖”问题。
因为
代码
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-09 21:00
# update_at: 2026-10-09 21:45
import sys
def check(L: int, n: int, pts: list[tuple[int, int]]) -> bool:
"""判定能否放下边长 L 的正方形:合法左上角域 [1,lim]^2 是否被禁放矩形全覆盖。"""
if L > n:
return False
lim = n - L + 1
xs = [1, n - L + 2] # 搜索域左右哨兵(右端取开区间)
ys = [1, n - L + 2]
rects: list[tuple[int, int, int, int]] = [] # 禁放矩形 (x1, x2, y1, y2),半开 [x1,x2) × [y1,y2)
for x, y in pts:
x1, x2 = max(1, x - L + 1), min(lim, x) + 1
y1, y2 = max(1, y - L + 1), min(lim, y) + 1
if x1 < x2 and y1 < y2:
rects.append((x1, x2, y1, y2))
xs += [x1, x2]
ys += [y1, y2]
xs = sorted(set(xs))
ys = sorted(set(ys))
xi = {v: i for i, v in enumerate(xs)}
yi = {v: i for i, v in enumerate(ys)}
xn, yn = len(xs), len(ys)
diff = [[0] * yn for _ in range(xn)]
for x1, x2, y1, y2 in rects: # 矩形区间加一:二维差分
i1, i2, j1, j2 = xi[x1], xi[x2], yi[y1], yi[y2]
diff[i1][j1] += 1
diff[i2][j1] -= 1
diff[i1][j2] -= 1
diff[i2][j2] += 1
for i in range(xn - 1): # 二维前缀和原位还原(diff 复用为 cov)
row = diff[i]
pre = diff[i - 1] if i else None
for j in range(yn - 1):
v = row[j] + (pre[j] if i else 0) + (row[j - 1] if j else 0)
if i and j:
v -= pre[j - 1]
row[j] = v
if v == 0 and xs[i] <= lim and ys[j] <= lim:
return True
return False
def solve() -> None:
"""二分答案:答案关于边长单调,故二分最大可行边长,答案为 0 表示放不下任何正方形。"""
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data, None)
if n is None:
return
T = next(data)
pts = [(next(data), next(data)) for _ in range(T)]
lo, hi, ans = 1, n, 0
while lo <= hi:
mid = (lo + hi) // 2
if check(mid, n, pts):
ans, lo = mid, mid + 1
else:
hi = mid - 1
print(ans)
if __name__ == '__main__':
solve()/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-09 21:00
* update_at: 2026-10-09 21:45
*/
// 20029《考古发掘区》:二分答案 + 坐标离散化 + 二维差分
// n 可达 5e5 而危险区 T≤100 ⇒ 不能开 n×n 网格。二分边长 L,把每个危险区
// 「反投影」成合法左上角平面上的一个禁放矩形,再判定合法区域是否被全部覆盖。
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXT = 105; // 危险区数量上界 100
const int MAXC = 2 * MAXT + 4; // 离散化坐标数上界 = 2 哨兵 + 2*T 矩形端点
struct Point {
ll x, y; // 危险区坐标(1-indexed)
};
struct Rect {
ll x1, x2, y1, y2; // 禁放矩形 [x1, x2) × [y1, y2)
};
ll n; // 遗迹边长
int T; // 危险区数量
Point pts[MAXT]; // 危险区坐标
ll xs[MAXC], ys[MAXC]; // 离散化后的 X / Y 坐标
int xn, yn; // 离散化后的坐标个数
Rect rects[MAXT]; // 每个危险区对应的禁放矩形
int rn; // 有效矩形个数
int diff[MAXC][MAXC]; // 二维差分数组
int cov[MAXC][MAXC]; // 前缀和还原后的覆盖次数
// 排序并就地去重,返回去重后的元素个数
int uniq(ll* a, int m) {
sort(a, a + m);
int k = 0;
for (int i = 0; i < m; ++i) {
if (i == 0 || a[i] != a[i - 1]) a[k++] = a[i];
}
return k;
}
// 判定能否放下边长 L 的正方形
bool check(ll L) {
if (L > n) return false;
ll lim = n - L + 1; // 合法左上角取值范围 [1, lim] × [1, lim]
xn = 0;
xs[xn++] = 1; // 搜索域左边界哨兵
xs[xn++] = n - L + 2; // 搜索域右边界哨兵(开区间右端)
yn = 0;
ys[yn++] = 1;
ys[yn++] = n - L + 2;
// 危险区 (x, y) 会让左上角落入 [max(1,x-L+1), min(lim,x)] × [max(1,y-L+1), min(lim,y)]
rn = 0;
for (int i = 0; i < T; ++i) {
ll x1 = max(1LL, pts[i].x - L + 1);
ll x2 = min(lim, pts[i].x) + 1;
ll y1 = max(1LL, pts[i].y - L + 1);
ll y2 = min(lim, pts[i].y) + 1;
if (x1 < x2 && y1 < y2) {
rects[rn].x1 = x1;
rects[rn].x2 = x2;
rects[rn].y1 = y1;
rects[rn].y2 = y2;
++rn;
xs[xn++] = x1;
xs[xn++] = x2;
ys[yn++] = y1;
ys[yn++] = y2;
}
}
xn = uniq(xs, xn);
yn = uniq(ys, yn);
// 二维差分:矩形区间加一
for (int i = 0; i < xn; ++i) {
for (int j = 0; j < yn; ++j) diff[i][j] = 0;
}
for (int k = 0; k < rn; ++k) {
int ix1 = lower_bound(xs, xs + xn, rects[k].x1) - xs;
int ix2 = lower_bound(xs, xs + xn, rects[k].x2) - xs;
int iy1 = lower_bound(ys, ys + yn, rects[k].y1) - ys;
int iy2 = lower_bound(ys, ys + yn, rects[k].y2) - ys;
diff[ix1][iy1]++;
diff[ix2][iy1]--;
diff[ix1][iy2]--;
diff[ix2][iy2]++;
}
// 二维前缀和还原,找一个覆盖次数为 0 且落在合法搜索域内的格子
for (int i = 0; i < xn - 1; ++i) {
for (int j = 0; j < yn - 1; ++j) {
cov[i][j] = diff[i][j];
if (i > 0) cov[i][j] += cov[i - 1][j];
if (j > 0) cov[i][j] += cov[i][j - 1];
if (i > 0 && j > 0) cov[i][j] -= cov[i - 1][j - 1];
if (cov[i][j] == 0) {
if (xs[i] <= lim && ys[j] <= lim) return true;
}
}
}
return false;
}
void solve() {
if (!(cin >> n >> T)) return;
for (int i = 0; i < T; ++i) {
cin >> pts[i].x >> pts[i].y;
}
// 答案关于边长单调 ⇒ 二分;答案为 0 表示任意危险区都放不下
ll l = 1, r = n, ans = 0;
while (l <= r) {
ll mid = l + (r - l) / 2;
if (check(mid)) {
ans = mid;
l = mid + 1;
} else {
r = mid - 1;
}
}
cout << ans << "\n";
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
solve();
return 0;
}复杂度
- 时间复杂度:二分答案的次数为
次。每次二分中,离散化的坐标数量为 ,网格大小 。处理二维差分与前缀和耗时为 。总时间复杂度为 。对于最大的输入 ,运算总次数约在 级别,远低于时间限制。 - 空间复杂度:需要存储大小不超过
的二维差分及前缀和数组,空间复杂度为 ,满足内存限制。
总结
- 本题亮点:将正方形的碰撞判定反演为左上角坐标平面中的矩形禁区,进而归结为计算少量矩形是否完全覆盖指定区域,结合二分答案彻底规避了
。 - 验证声明(最强证据层):
- 官方样例 3、4(存在于 GitHub)与
main.cpp/main.py逐字节一致,不是无数据题! - 跨源四方一致:
ref.cpp(异源·主):采用“候选左上角枚举”思路,正方形向上/左推到底,推不动处必是障碍坐标+1,只枚举 X∈{1}∪{x_i+1}、Y∈{1}∪{y_i+1} 的 O(T^2) 候选,无排序/离散化/差分/前缀和。main.cpp(专家):坐标离散化 + 二维差分 + 前缀和判定,复杂度 O(T^2)。ref_dp.cpp(异源·辅):经典最大全 0 正方形 DP,dp[i][j]=min(三个邻居)+1,无二分/离散化/差分,复杂度 O(n^2)。 三种公式完全不同源的算法在不同维度互相印证,对拍一致才构成异源证据。二分区间在ref用[0, n](L=0 恒合法),main用[1, n],答案语义等价。
- 穷举 277,062 例 + 410 组四方对拍 0 分歧:穷举包括 n≤4 全子集、n≤12 全 T≤2、n≤7 全 T≤3、n≤8 popcount≤3 等全部 0 分歧。
- 官方样例 3、4(存在于 GitHub)与
- 反事实表 (V1 变体被发现):11 个变体(例如删哨兵边界、前缀和漏减、覆盖判据改 >0 等)全部被合成数据发现,具有鉴别力。
- 题意细节 (V8):1-indexed、正方形须完整落在
内的设定,均经官方真值唯一确定。 - 其他测试组 (V9 + 对照组) 均验证无误。
T9 五项
- 算法/数据结构:二分答案、二维差分、坐标离散化。
- 时间复杂度:
。 - 空间复杂度:
。 - 代码行数:约 120 行。
- 实现关键:利用左上角的反向覆盖转化为平面二维面积并问题。