题解:洛谷 P2845 Switching on the Lights

【题目来源】

洛谷:P2845 [USACO15DEC] Switching on the Lights S - 洛谷

【题目描述】

\(N \times N\) 个房间,组成了一张 \(N \times N\) 的网格图,Bessie 一开始位于左上角 \((1,1)\),并且只能上下左右行走。

一开始,只有 \((1,1)\) 这个房间的灯是亮着的,Bessie 只能在亮着灯的房间里活动。

有另外 \(M\) 条信息,每条信息包含四个数 \(a,b,c,d\),表示房间 \((a,b)\) 里有房间 \((c,d)\) 的灯的开关。

请计算出最多有多少个房间的灯可以被打开。

【输入】

第一行输入两个整数 \(N,M(1 < N \leq 100,1 < M < 2 \times 10 ^ 5)\)

\(2 \sim M + 1\) 行,每行输入四个整数 \((x_1,y_1),(x_2,y_2)\),代表房间的坐标 \((x_1,y_1)\) 可以点亮房间的坐标 \((x_2,y_2)\)

【输出】

一个数,最多可以点亮的房间数。

【输入样例】

3 6
1 1 1 2
2 1 2 2
1 1 1 3
2 3 3 1
1 3 1 2
1 3 2 1

【输出样例】

5

【核心思想】

  1. 问题分析:给定 \(N \times N\) 网格,Bessie 从 \((1,1)\) 出发,只能上下左右移动且只能在亮灯房间活动。每个亮灯房间可能有开关控制其他房间的灯。求最多能点亮多少房间。这是一个BFS + 动态点亮问题,关键在于Bessie 每到达一个新房间,可以点亮该房间开关控制的所有灯;若新点亮的灯与已访问区域相邻,则 Bessie 可以移动到该灯所在房间继续探索。

  2. 算法选择

    • BFS(广度优先搜索):模拟 Bessie 的移动过程,队列存储当前可到达且已访问的房间
    • 动态点亮机制:从当前房间出发,用该房间的开关点亮其他房间;若新点亮的房间与已访问区域相邻,则将其加入BFS队列
    • 访问标记分离lights[x][y] 表示灯是否亮,vis[x][y] 表示 Bessie 是否实际到达过该房间
  3. 关键步骤

    • 初始化lights[1][1] = true, vis[1][1] = true\((1,1)\) 入队
    • BFS 循环(队列非空时):
      • 取出队首 \((x, y)\)
      • 点亮阶段:遍历 sw[x][y] 中所有被 \((x,y)\) 控制的灯 \((nx, ny)\)
        • !lights[nx][ny],点亮它
        • 检查 \((nx, ny)\) 的四邻域是否有 vis = true 的房间:
          • 若有,则 vis[nx][ny] = true\((nx, ny)\) 入队(Bessie 可以到达)
      • 移动阶段:遍历 \((x, y)\) 的四邻域 \((nx, ny)\)
        • 若未越界、未访问、且灯亮着,则 vis[nx][ny] = true,入队
    • 统计答案:遍历所有格子,统计 lights[i][j] == true 的数量
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2 + M)\),每个房间最多被点亮一次、访问一次,每个开关最多被触发一次
    • 空间复杂度:\(O(N^2 + M)\),网格状态、开关列表、队列
  5. BFS 动态点亮的核心思想

    • 点亮与到达的分离:灯被点亮不代表 Bessie 能到达,只有点亮的灯与已访问区域相邻时,Bessie 才能走过去
    • 连锁反应:Bessie 到达房间 A → 点亮房间 B → B 与已访问区域相邻 → Bessie 到达 B → 点亮房间 C → ...,形成连锁点亮和移动
    • 延迟入队策略:新点亮的灯不立即入队,而是先检查是否与已访问区域相邻。这避免了 Bessie 在无法到达的亮灯房间进行无效搜索
    • 访问标记的双重作用vis 既标记 Bessie 的物理位置,也作为"已探索区域"的边界,用于判断新点亮的灯是否可达
    • 适用于"探索-触发-再探索"的连锁反应问题,核心在于状态分离和条件入队

【算法标签】

普及+ #BFS-二维

【代码详解】

#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII;
const int N = 105, M = 200005;
int n, m, ans;  // n: 网格大小,m: 开关数量,ans: 亮灯数量
vector<PII> sw[N][N];  // sw[x][y]: 位于(x,y)的开关控制的灯的位置列表
bool lights[N][N], vis[N][N];  // lights: 灯是否亮着,vis: BFS访问标记
int dx[4] = {-1, 1, 0, 0};  // 上下左右四个方向的x偏移
int dy[4] = {0, 0, -1, 1};  // 上下左右四个方向的y偏移
queue<PII> q;  // BFS队列

// 广度优先搜索,模拟灯的连锁点亮过程
void bfs()
{
    q.push({1, 1});  // 从(1,1)开始
    lights[1][1] = true;  // (1,1)的灯初始亮着
    vis[1][1] = true;  // 标记(1,1)已访问

    while (!q.empty())
    {
        auto [x, y] = q.front();  // 取出队首位置
        q.pop();

        // 遍历当前位置开关控制的所有灯
        for (auto [nx, ny] : sw[x][y])
        {
            if (!lights[nx][ny])  // 如果灯没亮
            {
                lights[nx][ny] = true;  // 点亮它
                // 检查这个新亮的灯周围是否有已访问过的灯
                for (int j = 0; j < 4; j++)
                {
                    int nnx = nx + dx[j], nny = ny + dy[j];  // 相邻位置
                    // 检查边界
                    if (nnx < 1 || nnx > n || nny < 1 || nny > n)
                        continue;
                    // 如果相邻位置已访问过,则将新亮的灯加入队列
                    if (vis[nnx][nny])
                    {
                        vis[nx][ny] = 1;  // 标记新亮的灯已访问
                        q.push({nx, ny});  // 入队
                        break;  // 找到一个即可
                    }
                }
            }
        }

        // 遍历当前位置的四个相邻位置
        for (int i = 0; i < 4; i++)
        {
            int nx = x + dx[i], ny = y + dy[i];  // 相邻位置
            // 检查边界、是否访问过、灯是否亮着
            if (nx < 1 || nx > n || ny < 1 || ny > n ||
                vis[nx][ny] || !lights[nx][ny])
                continue;
            vis[nx][ny] = true;  // 标记已访问
            q.push({nx, ny});  // 入队
        }
    }
}

int main()
{
    cin >> n >> m;  // 输入网格大小和开关数量

    while (m--)  // 读入所有开关信息
    {
        int a, b, c, d;
        cin >> a >> b >> c >> d;  // (a,b)处的开关控制(c,d)处的灯
        sw[a][b].push_back({c, d});
    }

    bfs();  // 执行BFS

    // 统计亮着的灯的数量
    for (int i = 1; i <= n; i++)
        for (int j = 1; j <= n; j++)
            if (lights[i][j])
                ans++;

    cout << ans << endl;  // 输出结果
    return 0;
}

【运行结果】

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