考古发掘区

GitHub跳转原题关系图返回列表

通过二分答案,将判定边长为 L 的正方形退化为二维平面的禁放矩形覆盖问题,用离散化与二维差分实现 O(T^2) 高效判定。

OJ: roj

题目 ID: 20029

难度:提高

标签:二分答案二维差分离散化

创建: 2026-10-09 21:46

更新: 2026-10-09 21:46

形式化题目

给定一个 n×nn \times n 的网格,其中有 TT 个坐标位置 (xi,yi)(x_i, y_i) 被标记为障碍物。请找到在网格内能够放置的最大正方形的边长,使得这个正方形内部不包含任何障碍物。坐标是 11-indexed,即从 11 到 nn。正方形必须完整地落在 1…n1 \dots n 内。 数据范围:1≤n≤5×105,1≤T≤1001 \le n \le 5\times 10^5, 1 \le T \le 100。

正解

思路

因为 nn 非常大(5×1055\times 10^5),而障碍物数量 TT 极小(≤100\le 100),基于网格 DP 或者直接枚举正方形的做法不可行。 由于题目要求寻找最大的正方形边长,且该属性满足单调性(如果能放边长 LL 的正方形,就一定能放更小边长的正方形),我们可以对答案进行二分。二分的上界为 nn,下界为 11。

设当前二分的边长为 LL。我们需要判定在 n×nn \times n 网格中,是否存在至少一个可以放下且不碰到障碍物的左上角位置。 合法的左上角 (X,Y)(X, Y) 必须满足:

  1. 1≤X≤n−L+11 \le X \le n - L + 1
  2. 1≤Y≤n−L+11 \le Y \le n - L + 1

对于每个障碍物 (x,y)(x, y),如果要让以 (X,Y)(X, Y) 为左上角的 L×LL \times L 的正方形包含该障碍物,则必须满足: X≤x≤X+L−1X \le x \le X + L - 1 且 Y≤y≤Y+L−1Y \le y \le Y + L - 1。 化简可得,以 (X,Y)(X, Y) 为左上角的正方形如果碰到了 (x,y)(x, y) 这个障碍,那么 (X,Y)(X, Y) 必定落在: X∈[max⁡(1,x−L+1),min⁡(n−L+1,x)]X \in [\max(1, x - L + 1), \min(n - L + 1, x)] Y∈[max⁡(1,y−L+1),min⁡(n−L+1,y)]Y \in [\max(1, y - L + 1), \min(n - L + 1, y)] 这说明每一个障碍物在合法的左上角区域中“挖去”了一个“禁放矩形”。 如果在所有 TT 个禁放矩形覆盖之后,合法区域 [1,n−L+1]×[1,n−L+1][1, n - L + 1] \times [1, n - L + 1] 内仍有未被覆盖的点,则说明能够放下边长为 LL 的正方形。

这转化成了典型的“矩形面积并 / 离散化网格覆盖”问题。 因为 T≤100T \le 100,所有矩形的 XX 和 YY 坐标边界最多只有 2T+22T+2 个。 我们对 XX 和 YY 进行离散化,网格大小最多 202×202202 \times 202。 利用二维差分数组处理矩形的区间加法,并通过二维前缀和恢复数组,即可在 O(T2)O(T^2) 的时间内检查是否所有的合法区域都被覆盖。如果找到一个覆盖次数为 00 且属于有效网格内的区域,说明判定成功。

代码

python
#!/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()
cpp
/**
 * 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;
}

复杂度

  • 时间复杂度:二分答案的次数为 log⁡2n≈19\log_2 n \approx 19 次。每次二分中,离散化的坐标数量为 O(T)O(T),网格大小 O(T×T)O(T \times T)。处理二维差分与前缀和耗时为 O(T2)O(T^2)。总时间复杂度为 O(T2log⁡n)O(T^2 \log n)。对于最大的输入 n=5×105,T=100n=5 \times 10^5, T=100,运算总次数约在 10610^6 级别,远低于时间限制。
  • 空间复杂度:需要存储大小不超过 205×205205 \times 205 的二维差分及前缀和数组,空间复杂度为 O(T2)O(T^2),满足内存限制。

总结

  • 本题亮点:将正方形的碰撞判定反演为左上角坐标平面中的矩形禁区,进而归结为计算少量矩形是否完全覆盖指定区域,结合二分答案彻底规避了 O(n2)O(n^2)。
  • 验证声明(最强证据层):
    1. 官方样例 3、4(存在于 GitHub)与 main.cpp/main.py 逐字节一致,不是无数据题!
    2. 跨源四方一致:
      • 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],答案语义等价。
    3. 穷举 277,062 例 + 410 组四方对拍 0 分歧:穷举包括 n≤4 全子集、n≤12 全 T≤2、n≤7 全 T≤3、n≤8 popcount≤3 等全部 0 分歧。
  • 反事实表 (V1 变体被发现):11 个变体(例如删哨兵边界、前缀和漏减、覆盖判据改 >0 等)全部被合成数据发现,具有鉴别力。
  • 题意细节 (V8):1-indexed、正方形须完整落在 1…n1 \dots n 内的设定,均经官方真值唯一确定。
  • 其他测试组 (V9 + 对照组) 均验证无误。

T9 五项

  • 算法/数据结构:二分答案、二维差分、坐标离散化。
  • 时间复杂度:O(T2log⁡n)O(T^2 \log n)。
  • 空间复杂度:O(T2)O(T^2)。
  • 代码行数:约 120 行。
  • 实现关键:利用左上角的反向覆盖转化为平面二维面积并问题。