题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N \times N\) 网格,Bessie 从 \((1,1)\) 出发,只能上下左右移动且只能在亮灯房间活动。每个亮灯房间可能有开关控制其他房间的灯。求最多能点亮多少房间。这是一个BFS + 动态点亮问题,关键在于Bessie 每到达一个新房间,可以点亮该房间开关控制的所有灯;若新点亮的灯与已访问区域相邻,则 Bessie 可以移动到该灯所在房间继续探索。
-
算法选择:
- BFS(广度优先搜索):模拟 Bessie 的移动过程,队列存储当前可到达且已访问的房间
- 动态点亮机制:从当前房间出发,用该房间的开关点亮其他房间;若新点亮的房间与已访问区域相邻,则将其加入BFS队列
- 访问标记分离:
lights[x][y]表示灯是否亮,vis[x][y]表示 Bessie 是否实际到达过该房间
-
关键步骤:
- 初始化:
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的数量
- 初始化:
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2 + M)\),每个房间最多被点亮一次、访问一次,每个开关最多被触发一次
- 空间复杂度:\(O(N^2 + M)\),网格状态、开关列表、队列
-
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
浙公网安备 33010602011771号