寻宝!大冒险!

枚举树作为左下角,检查平移后的藏宝图 1 集合与窗口内树集合完全相同。

OJ: shumeng

题目 ID: CSP202206B

难度:普及-

标签:模拟集合网格

日期: 2026-07-31 16:21

形式化题目

有一张大小为 (L+1)×(L+1)(L+1)\times(L+1) 的稀疏 01 网格,只给出其中 nn 个值为 1 的位置(树)。另有一张大小为 (S+1)×(S+1)(S+1)\times(S+1) 的完整 01 藏宝图 BB,且 B0,0=1B_{0,0}=1

问有多少个左下角 (x,y)(x,y),满足把 BB 平移到 [x,x+S]×[y,y+S][x,x+S]\times[y,y+S] 后,与绿化图的对应区域完全相同。

思路

藏宝图很小(S50S\le 50),绿化图很大(L109L\le 10^9)但只有 n1000n\le 1000 棵树。直接对每个候选左下角展开整个窗口逐格比较,是理解题意最直接的朴素做法:

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-07-31 16:21
 * update_at: 2026-08-17 22:40
 */
// brute.cpp:小数据暴力解,枚举每个候选左下角并逐格比较完整藏宝图。
#include <bits/stdc++.h>
using namespace std;

int n;
long long limit;
int s;
vector<pair<long long, long long> > tree;
set<pair<long long, long long> > exists; // 用集合记录每棵树的位置
vector<vector<int> > map_value;
int answer;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> limit >> s;
    tree.resize(n);
    for (int i = 0; i < n; i++) {
        cin >> tree[i].first >> tree[i].second;
        exists.insert(tree[i]);
    }
    map_value.assign(s + 1, vector<int>(s + 1, 0));
    for (int row = s; row >= 0; row--) {
        for (int col = 0; col <= s; col++) {
            cin >> map_value[row][col];
        }
    }

    // 对每个树作为左下角,展开整个 (S+1)*(S+1) 窗口逐格比较
    for (int i = 0; i < n; i++) {
        long long x = tree[i].first, y = tree[i].second;
        if (x + s > limit || y + s > limit) continue;
        bool valid = true;
        for (int dx = 0; dx <= s && valid; dx++) {
            for (int dy = 0; dy <= s; dy++) {
                bool has_tree = exists.count({x + dx, y + dy}) != 0;
                if (has_tree != (map_value[dx][dy] != 0)) {
                    valid = false;
                    break;
                }
            }
        }
        if (valid) answer++;
    }

    cout << answer << '\n';
    return 0;
}

这个做法逐格比较需要 O(nS2)O(nS^2) 次集合查询,已经可以通过。下面再给出一个只用树集合、不展开窗口的写法。

关键观察

  • 藏宝图左下角一定是树,所以候选左下角只可能是 nn 棵树之一。
  • 对候选 (x,y)(x,y),窗口内实际存在的树必须是藏宝图中为 1 的位置。
  • 反之,藏宝图中为 1 的位置也必须在窗口内有树。

第二个方向只需检查数量:窗口内的树总数等于藏宝图中 1 的总数,即可保证 1 的位置都有树。

算法步骤

  1. 读入 nn 棵树的位置。
  2. 读入藏宝图,注意输入按从上到下给出,实际存储时把行号倒置,让 map[dx][dy] 与坐标偏移 (x+dx,y+dy)(x+dx,y+dy) 对应;同时统计藏宝图中 1 的个数 tree_count
  3. 枚举每棵树作为候选左下角,先排除窗口越界的候选。
  4. 遍历所有树,统计落在窗口内的树,并检查每棵窗口内的树对应藏宝图位置是否为 1。
  5. 若窗口内树数等于 tree_count,则该候选匹配,答案加一。

代码

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-07-31 16:21
 * update_at: 2026-08-17 22:40
 */
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1005;

int n;          // 树的数量
long long limit; // 绿化图大小 L
int s;       // 藏宝图大小 S
pair<long long, long long> tree[MAXN]; // 每棵树的坐标 (x, y)
bool map_value[55][55]; // 藏宝图:map[dx][dy] 表示相对左下角的偏移位置是否有树
int tree_count;  // 藏宝图中 1 的总数
int answer;      // 匹配的左下角候选个数

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n >> limit >> s;
    for (int i = 0; i < n; i++) {
        cin >> tree[i].first >> tree[i].second;
    }

    // 藏宝图按从下往上逐行读入,所以先读最上面一行,再填到 map[0..s][0..s]
    // 这里把输入的第 s 行放到 map 的第 s 行,保证 map[dx][dy] 与坐标偏移一致
    for (int row = s; row >= 0; row--) {
        for (int col = 0; col <= s; col++) {
            cin >> map_value[row][col];
            if (map_value[row][col]) tree_count++;
        }
    }

    // 枚举每个树作为藏宝图左下角,检查该窗口内的树是否与藏宝图一致
    for (int i = 0; i < n; i++) {
        long long x = tree[i].first, y = tree[i].second;
        if (x + s > limit || y + s > limit) continue; // 窗口越出绿化图边界

        bool valid = true;
        int inside = 0; // 落在窗口内的树的数量
        for (int j = 0; j < n; j++) {
            long long tx = tree[j].first, ty = tree[j].second;
            if (tx < x || tx > x + s || ty < y || ty > y + s) continue;
            inside++;
            if (!map_value[tx - x][ty - y]) { // 窗口内有树但藏宝图对应位置为 0
                valid = false;
                break;
            }
        }
        // 窗口内树的数量必须恰好等于藏宝图中 1 的数量,
        // 这样藏宝图为 1 的位置也都一定有树。
        if (valid && inside == tree_count) answer++;
    }

    cout << answer << '\n';
    return 0;
}

复杂度

枚举 nn 个候选,每个候选扫描全部 nn 棵树,时间复杂度为 O(n2)O(n^2);空间复杂度为 O(n+S2)O(n+S^2),用于存树和藏宝图。

总结

稀疏地图不需要展开到 L2L^2 的大小,候选左下角枚举和窗口内树计数分别保证了藏宝图 0、1 两种格子都被检查;这也是用树集合维护稀疏信息的典型思路。