其他的题目题解都很好了,这里提供 F 题的一种我认为更好想到且更容易想明白的思路。

简单思考

首先我们可以考虑如下样例:

2 3 4
1 1 1
1 1 4
1
2
3
4

显然的,输出会是:

5
5
5
6

也就是说,当 的时候,查询的结果是一致的,更形象地说,此时的

一般情况

所以,对于任意一个矩阵 ,将其中所有元素去重并从小到大排序好,得到序列 (其中 是原矩阵当中不同数字的个数),也就是说,对于任意一个 的查询,都会有三个情况:

  • :直接输出
  • :直接输出
  • :必然存在一个 使得 ,这时直接输出 对应的结果就好了。

所以我们可以考虑预处理一个映射 ,里面存储所有 ,查询时我们只需要二分找到 就好了。

数据处理以及思路

接下来我们考虑怎么预处理这个表。

首先有一个简单的性质,就是 是递增的( 数组已经排序)。

所以我们可以考虑从小处理到大,也就是从 。使用另一个映射 来存储 的坐标,也就是 ,这里空间复杂度是 的,不用担心内存。接着我们建议读取的时候直接进行插入,这样可以确保 时的坐标始终在前(这点可以方便我们后续操作)。

在进行下一步之前,我们先实现一个用于模拟毒扩散的 函数,一个全局的 数组和一个 计数器(记录 增加情况)。

接着我们外层循环遍历 ,内层循环遍历 ,在遍历过程中,我们需要考虑两种情况:

  • :因为我们的毒在第一行释放,所以无论如何,只要当前坐标的 为否,就可以直接进行 BFS。

  • :这时我们就要考虑当前坐标的上下左右是否存在 为真的点,有的话就说明这一格是可以被感染到的,于是进行 BFS。

注意:第一行的 BFS 总是最优先的,因为我们的毒使用第一行释放的。所以这里就说明了为什么保证 在前会方便操作,这样我们就可以在一次循环内完成上述两种情况,而不必要遍历两次

正确性讨论

对于任意的 ,当我们释放大小为 的毒时,显然会覆盖到 的情况,所以我们可以在 已经覆盖的范围上进行扩展,扩展方式就是依照题意的:在第一行放毒,那我们就先对第一行的点直接进行 BFS,然后再对其他不在第一行的 进行 BFS(先判断四周的 ),所以我们使用全局的计数器和 数组,是因为这样就是自然的保留曾经的所有状态。

时间复杂度分析

注意,这样的双层循环也只是遍历了所有的坐标而已,复杂度是 的,我们主要分析 BFS。

因为我们的 数组是不断扩张的,并不会出现已经标记为 但是之后又回退 的现象。放到我们双重循环遍历坐标点里面来看,实际上就是类似于双指针,二者量级都是相同且同时增加的,只不过是增加的时刻和数量不一样,所以全部的 BFS 加起来的时间复杂度才是

综上,预处理时间复杂度是 或者 (带 是考虑到 C++ 可以使用 std::map,最坏时 ),查询一次只有二分的开销,于是总复杂度就是 ,在 下可以不卡常通过。

具体细节可以参考

Code

void solve() {
    int n, m, q;
    std::cin >> n >> m >> q;

    std::vector<std::vector<int>> a(n + 2, std::vector<int>(m + 2));
    std::map<int, std::vector<pii>> pos;	// 前面提到的存储坐标
    int min_v = INF, max_v = 0;	// 记录一下最值,查询的时候可以进行一点小优化
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= m; j++) {
            std::cin >> a[i][j];
            min_v = std::min(min_v, a[i][j]);
            max_v = std::max(max_v, a[i][j]);
            pos[a[i][j]].push_back({ i, j });	// 读取时直接插入,保证 x = 1 在前
        }
    }
    
    int cur = 0;	// 计数器
    std::map<int, int> cnt;	// 存储对应的数量
    
    std::queue<pii> que;	// BFS 队列,可以开在全局(BFS 的循环退出条件)
  	// vis 开大一格可以避免一些边界的问题(比如后面有 vis[x][y + 1] 之类的)
    std::vector<std::vector<int>> vis(n + 2, std::vector<int>(m + 2));
  	// BFS 不多赘述,就是多判断一步 a[next_x][next_y] > k 检查能否继续扩散
    const auto bfs = [&](const pii& st, int k) {
        vis[st.first][st.second] = 1;
        cur++;
        que.push(st);
        while (que.size()) {
            auto [x, y] = que.front();
            que.pop();

            for (int i = 0; i < 4; i++) {
                int nx = x + dx[i], ny = y + dy[i];
                if (nx < 1 || ny < 1 || nx > n || ny > m || vis[nx][ny] || a[nx][ny] > k) continue;
                vis[nx][ny] = 1;
                cur++;
                que.push({ nx, ny });
            }
        }
    };

  	// 主要循环逻辑,外层就是前文说到的,主要要注意内层的条件逻辑
    for (const auto& [num, v] : pos) {
        for (const auto& [x, y] : v) {
          	// 在任何时候,都不必对已经标记过的位置进行操作,否则会造成计数器重复计数
            if (!vis[x][y]) {
                if (x == 1) {	// 第一行:先放且可以直接放
                    bfs({ x, y }, num);
                } else {	// 其他:先要确定上下左右能否扩散过来
                    if (vis[x - 1][y] || vis[x + 1][y] || vis[x][y - 1] || vis[x][y + 1]) {
                        bfs({ x, y }, num);
                    }
                }
            }
        }
        cnt[num] = cur;	// 扩散完毕后直接记录存储
    }

    while (q--) {
        int x;
        std::cin >> x;

      	// 直接判断 x 和最值得关系就能获得两个答案,前文也有提到
        if (x >= max_v) {
            std::cout << n * m << "\n";
            continue;
        }

        if (x < min_v) {
            std::cout << "0\n";
            continue;;
        }

      	// 二分找到第一个小于 x 的位置,直接输出
        auto it = cnt.upper_bound(x);
        std::cout << std::prev(it)->second << "\n";
    }
}