题解:AtCoder AT_awc0130_d Lighting Panels and Picture Frames

【题目来源】

AtCoder:D - Lighting Panels and Picture Frames

【题目描述】

Takahashi has a lighting panel consisting of a grid with \(H\) rows and \(W\) columns. The cell at the \(r\)-th row from the top and the \(c\)-th column from the left is called cell \((r, c)\).

Initially, all cells are turned off. Takahashi performs \(N\) operations in order.

In the \(i\)-th operation, he toggles the state of all cells whose row is between \(A_i\) and \(B_i\) (inclusive) and whose column is between \(C_i\) and \(D_i\) (inclusive). That is, cells that are on are turned off, and cells that are off are turned on.

The state of the lighting panel after all operations are completed is called the final state.

In the final state, each connected component of lit cells under the 4-directional (up, down, left, right) adjacency relation (sharing an edge) is called an island. If there are no lit cells at all, the number of islands is \(0\).

Aoki asks \(M\) questions about the final state. Takahashi must answer each question.

In the \(j\)-th question, a rectangular region is specified where the row is between \(P_j\) and \(Q_j\) (inclusive) and the column is between \(R_j\) and \(S_j\) (inclusive).

For each question, find the following two values:

  1. The number of lit cells within the specified region.
  2. The number of islands in the final state that are completely contained within the specified region.

Here, an island is completely contained within a region if all cells belonging to that island are within the specified region.

Note: Islands are defined with respect to the entire panel in the final state. We do NOT recompute connected components within only the region specified by each question.

高橋有一个由 \(H\)\(W\) 列的网格组成的照明面板。从上往下第 \(r\) 行、从左往右第 \(c\) 列的格子称为格子 \((r, c)\)

最初,所有格子都处于关闭状态。高橋按顺序执行 \(N\) 次操作。

在第 \(i\) 次操作中,他将行在 \(A_i\)\(B_i\) 之间(含)且列在 \(C_i\)\(D_i\) 之间(含)的所有格子的状态切换。也就是说,开启的格子变为关闭,关闭的格子变为开启。

所有操作完成后的照明面板状态称为最终状态

在最终状态中,在上下左右四方向相邻关系(共享边)下,每个由点亮格子组成的连通分量称为一个岛屿。如果完全没有点亮的格子,则岛屿数量为 \(0\)

青木就最终状态提出了 \(M\) 个问题。高橋必须回答每个问题。

在第 \(j\) 个问题中,指定了一个矩形区域,其中行在 \(P_j\)\(Q_j\) 之间(含),列在 \(R_j\)\(S_j\) 之间(含)。

对于每个问题,求以下两个值:

  1. 指定区域内点亮的格子数量。
  2. 最终状态中完全包含在指定区域内的岛屿数量。

这里,如果一个岛屿的所有格子都在指定区域内,则称该岛屿完全包含在该区域内。

注意: 岛屿是相对于整个面板的最终状态定义的。我们不会针对每个问题只在指定区域内重新计算连通分量。

【输入】

The input is given from standard input in the following format:

\(H\) \(W\) \(N\)
\(A_1\) \(B_1\) \(C_1\) \(D_1\)
\(A_2\) \(B_2\) \(C_2\) \(D_2\)
\(\vdots\)
\(A_N\) \(B_N\) \(C_N\) \(D_N\)
\(M\)
\(P_1\) \(Q_1\) \(R_1\) \(S_1\)
\(P_2\) \(Q_2\) \(R_2\) \(S_2\)
\(\vdots\)
\(P_M\) \(Q_M\) \(R_M\) \(S_M\)

【输出】

Output \(M\) lines.

On the \(j\)-th line, print the number of lit cells within the region and the number of islands completely contained within the region for the \(j\)-th question, separated by a space, in this order.

【输入样例】

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

【输出样例】

11 3
4 0
5 1
1 0

【核心思想】

  1. 问题分析:给定 \(H \times W\) 网格,初始全灭。执行 \(N\) 次矩形区域切换操作(开变关、关变开)。求最终状态后,回答 \(M\) 个查询:每个查询矩形区域内(1)点亮格子数量;(2)完全包含在该区域内的岛屿数量(四连通连通块)。这是一个二维差分 + 前缀和 + BFS问题,关键在于高效处理矩形切换、快速查询区域亮格数,以及预处理所有岛屿的边界信息。

  2. 算法选择

    • 二维差分:将 \(N\) 个矩形切换操作转化为 \(4N\) 个角点标记,\(O(1)\) 单次操作
    • 二维前缀和:还原差分数组得到每个格子的切换次数,再构建亮格前缀和实现 \(O(1)\) 区域查询
    • BFS 找连通块:遍历最终状态网格,对每个未访问的亮格进行 BFS,记录每个岛屿的边界坐标(最小/最大行列)
  3. 关键步骤

    • 二维差分标记\(N\) 次操作):
      • 对于矩形 \([A_i, B_i] \times [C_i, D_i]\)
        • diff[A_i][C_i]++diff[A_i][D_i+1]--
        • diff[B_i+1][C_i]--diff[B_i+1][D_i+1]++
    • 前缀和还原diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1]
    • 确定最终状态g[i][j] = diff[i][j] % 2(奇数次为亮)
    • 亮格前缀和pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + g[i][j]
    • BFS 找岛屿:遍历网格,对每个未访问亮格 BFS,记录岛屿边界 minr, maxr, minc, maxc
    • 回答查询\(M\) 次):
      • 亮格数:pre[b][d] - pre[a-1][d] - pre[b][c-1] + pre[a-1][c-1]
      • 完全包含岛屿数:遍历所有岛屿,检查边界是否完全在查询区域内
  4. 时间/空间复杂度

    • 时间复杂度:\(O(HW + N + M \cdot I)\),其中 \(I\) 为岛屿数量。差分 \(O(N)\),还原 \(O(HW)\),BFS \(O(HW)\),每次查询 \(O(I)\)
    • 空间复杂度:\(O(HW)\),差分数组、最终状态、前缀和、访问标记
  5. 多维离线处理的核心思想

    • 差分降维:二维差分将矩形区域操作从 \(O(HW)\) 降为 \(O(1)\),是处理网格区间更新的标准技巧
    • 前缀和加速查询:两次前缀和(一次还原差分、一次统计亮格)将区域查询从 \(O(HW)\) 降为 \(O(1)\)
    • 边界盒简化包含判断:记录每个岛屿的最小/最大行列作为边界盒,判断"完全包含"只需比较四个边界值,无需遍历岛屿内所有格子
    • 离线预处理策略:先完成所有操作得到最终状态,再统一处理所有查询,避免每个查询重复计算
    • 适用于"网格批量更新 + 多查询"问题,核心在于差分前缀和的高效性与连通块边界的预处理

【算法标签】

BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; // 定义坐标对类型
const int N = 1005, INF = 1e9; // 最大网格尺寸和无穷大常量
int h, w, n, m; // h:行数, w:列数, n:操作次数, m:询问次数
int diff[N][N], g[N][N], pre[N][N]; // 差分数组、最终状态网格、二维前缀和数组
int island_cnt; // 岛屿计数器
struct Node
{
    int minr, maxr, minc, maxc; // 记录岛屿的边界坐标:最小/最大行、最小/最大列
};
vector<Node> islands; // 存储所有岛屿的边界信息
int dx[4] = {-1, 1, 0, 0}; // 四方向移动的行偏移量(上、下、左、右)
int dy[4] = {0, 0, -1, 1}; // 四方向移动的列偏移量
bool vis[N][N]; // BFS访问标记数组

void bfs(int sx, int sy) // 从起点(sx, sy)开始BFS搜索整个岛屿
{
    queue<PII> q; // BFS队列
    q.push({sx, sy}); // 起点入队
    vis[sx][sy] = 1; // 标记起点已访问
    while (!q.empty()) // 队列非空时继续搜索
    {
        auto t = q.front(); // 取出队首元素
        q.pop(); // 队首出队
        int x = t.first, y = t.second; // 当前格子的坐标

        // 更新当前岛屿的边界范围
        islands[island_cnt-1].minr = min(islands[island_cnt-1].minr, x);
        islands[island_cnt-1].maxr = max(islands[island_cnt-1].maxr, x);
        islands[island_cnt-1].minc = min(islands[island_cnt-1].minc, y);
        islands[island_cnt-1].maxc = max(islands[island_cnt-1].maxc, y);

        for (int i=0; i<4; i++) // 遍历四个相邻方向
        {
            int nx = x + dx[i]; // 计算相邻格子的行坐标
            int ny = y + dy[i]; // 计算相邻格子的列坐标
            if (nx<1 || nx>h || ny<1 || ny>w) continue; // 边界检查:越界则跳过
            if (g[nx][ny] == 0) continue; // 状态检查:未点亮的格子跳过
            if (vis[nx][ny]) continue; // 访问检查:已访问的格子跳过
            vis[nx][ny] = 1; // 标记相邻格子已访问
            q.push({nx, ny}); // 相邻格子入队
        }

    }
}

int main()
{
    cin >> h >> w >> n; // 读入网格尺寸和操作次数
    for (int i=1; i<=n; i++) // 处理n次矩形区域切换操作
    {
        int a, b, c, d; // a,b:行范围, c,d:列范围
        cin >> a >> b >> c >> d;
        // 二维差分:在矩形四个角进行标记
        diff[a][c]++;       // 左上角+1
        diff[a][d+1]--;     // 右上角-1
        diff[b+1][c]--;     // 左下角-1
        diff[b+1][d+1]++;   // 右下角+1(容斥原理恢复)
    }

    // 通过前缀和将差分数组还原为每个格子的切换次数
    for (int i=1; i<=h; i++)
        for (int j=1; j<=w; j++)
            diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];

    // 根据切换次数的奇偶性确定最终状态:奇数次为1(亮),偶数次为0(灭)
    for (int i=1; i<=h; i++)
        for (int j=1; j<=w; j++)
            g[i][j] = diff[i][j] % 2;

    // 构建点亮格子的二维前缀和数组,用于快速查询矩形区域内亮格数量
    for (int i=1; i<=h; i++)
        for (int j=1; j<=w; j++)
            pre[i][j] = pre[i-1][j] + pre[i][j-1] - pre[i-1][j-1] + g[i][j];

    // 遍历整个网格,对每个未访问的亮格进行BFS,找出所有岛屿
    for (int i=1; i<=h; i++)
        for (int j=1; j<=w; j++)
        {
            if (g[i][j] && !vis[i][j]) // 当前格子亮且未访问,发现新岛屿
            {
                island_cnt++; // 岛屿计数加1
                islands.push_back({INF, 0, INF, 0}); // 初始化该岛屿的边界为极端值
                bfs(i, j); // BFS遍历整个岛屿并记录边界
            }
        }

    cin >> m; // 读入询问次数
    for (int i=1; i<=m; i++) // 处理每个询问
    {
        int a, b, c, d; // a,b:询问区域的行范围, c,d:列范围
        cin >> a >> b >> c >> d;
        // 利用二维前缀和计算矩形区域内亮格数量(容斥原理)
        int res = pre[b][d] - pre[a-1][d] - pre[b][c-1] + pre[a-1][c-1];
        cout << res << " "; // 输出亮格数量
        int cnt = 0; // 统计完全包含在询问区域内的岛屿数量
        // 遍历所有岛屿,检查其边界是否完全在询问区域内
        for (int j=0; j<island_cnt; j++)
        {
            if (islands[j].minr>=a && islands[j].maxr<=b && islands[j].minc>=c && islands[j].maxc<=d)
                cnt++; // 岛屿完全包含,计数加1
        }
        cout << cnt << endl; // 输出完全包含的岛屿数量
    }
    return 0;
}

【运行结果】

4 5 3
1 2 1 3
2 4 2 5
3 3 1 5
4
1 4 1 5
1 2 1 3
3 4 1 5
2 3 2 4
11 3
4 0
5 1
1 0
posted @ 2026-08-10 12:24  团爸讲算法  阅读(6)  评论(0)    收藏  举报