把每块元件的对角线看成格点之间权为 0 或 1 的边,答案即电源格点到灯泡格点的最短路,用 0-1 BFS 在 O(NM) 内求出。
OJ: roj
题目 ID: 1448
难度:提高+/省选-
标签:0-1 BFS最短路图论建模
创建: 2026-09-30 11:11
更新: 2026-10-07 11:01
形式化题目
棋盘上有
\连通左上顶点与右下顶点 ; /连通右上顶点与左下顶点 。
每个元件可以旋转
把电路板的顶点看成
正解
思路
从「转元件」到「选边」
连通性发生在格点上:两块相邻元件的导线要在它们共享的那个格点处相遇,通路才能延续。 所以图的点应该是格点,而不是元件。
抓住这一点,注意一个事实:任意一对对角格点,由唯一一块元件决定它们能否直连。
例如格点
- 元件是
\→ 它正好连着这一对,不用旋转,代价 0; - 元件是
/→ 它连的是另一对,要让这一对导通必须转一次,代价 1。
同样的,格点 / 时代价 0,\ 时代价 1。
于是每块元件相当于给「某一条对角线」上的一条边赋了权 0 或 1:
| 格点对 | 由哪块元件决定 | 代价 0 的朝向 | 代价 1 的朝向 |
|---|---|---|---|
| 元件 |
\ |
/ |
|
| 元件 |
/ |
\ |
这一步就是整道题的枢纽:原本「选一个旋转方案」是
为什么每块元件最多只贡献一次旋转
有人会担心一条路径反复经过同一块元件的两个方向,把一次旋转算成两次。 不会发生:一块元件在同一时刻只能提供一条对角线(朝向二选一)。 路径一旦用了某个方向,用到的就是该元件当时的朝向;另一条对角线在该元件旋转后就不存在了。 一条最短路不会重复走同一条边,所以每块元件的旋转被统计且仅被统计一次。
为什么用 0-1 BFS 而不是普通 BFS 或 Dijkstra
-
普通 BFS 要求所有边等权,这里边权有 0 有 1,直接用会错;
-
Dijkstra 配堆能做,但多了
的常数; -
0-1 BFS 正合适:维护一个双端队列,队列里始终按
dist非递减排列——- 走权 0 的边:新点距离不变,塞进队头;
- 走权 1 的边:新点距离 +1,塞进队尾。
这样每个格点第一次被弹出时,它的距离就已经是最终最短路,可以直接返回。 总复杂度
。
算法流程
dist[r][c] = -1表示未访问,dist[0][0] = 0,队列放。 - 弹出队头
:如果它就是 ,直接返回 dist[r][c]。 - 对四个对角方向
: - 目标格点
越界就跳过; - 跨过的那块元件位于行列各取较小的位置,即
; - 权值
; - 若
dist[r][c] + w更优就更新,并按是 0 还是 1 决定塞队头还是队尾。
- 目标格点
- 队列空了还没到
,说明无解。
「行列各取较小值」这个写法值得留意:往左上走时元件在当前格点的上一行, 往右下走时元件在当前行,统一写成取 min 就没有分支,也不会越界或取错格子。
无解怎么判
无解的来源有两类,但不需要分别特判:
- 奇偶性:对角移动让
的奇偶性不变(改变量是 或 ), 起点 是偶数,所以 为奇数时终点必然不可达; - 真实阻隔:
为偶数时也可能因为朝向分布而不可达,例如 的 \\。
两者都表现为「队列空掉后 dist[N][M] 仍然是 -1」,统一从同一个出口输出
NO SOLUTION。
样例推演
样例是
\\/\\
\\///
/\\\\下面是 0-1 BFS 求出的格点距离表,行列分别对应格点坐标 -1 表示不可达:
| 0 | 1 | 2 | 3 | 4 | 5 | |
|---|---|---|---|---|---|---|
| 0 | 0 | -1 | 1 | -1 | 1 | -1 |
| 1 | -1 | 0 | -1 | 0 | -1 | 1 |
| 2 | 1 | -1 | 0 | -1 | 1 | -1 |
| 3 | -1 | 1 | -1 | 0 | -1 | 1 |
可以看出两点。第一,所有可达格点都满足 -1 恰好填满另一半,
这正是奇偶性观察的直接体现。第二,沿主对角线
\;
从 / 而实际是 \,
于是花了唯一的一次旋转,之后
这恰好对应题面那句「把右边数第二列的任意一块元件转
代码
Python 版:
#!/usr/bin/env python3
# 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-09-30 11:11
# update_at: 2026-09-30 11:32
import sys
from collections import deque
BACKSLASH = ord('\\') # 反斜杠元件:导线连通「左上—右下」一对顶点
SLASH = ord('/') # 斜杠元件:导线连通「右上—左下」一对顶点
# 从格点斜着跨过一块元件到达对角格点,需要该元件是哪种方向。
# 每项 = (行偏移, 列偏移, 直通所需的方向);跨过的元件总是位于 (min(行), min(列))。
STEPS = (
(-1, -1, BACKSLASH), # 左上:走元件的「右下」半条线
(1, 1, BACKSLASH), # 右下:走元件的「左上」半条线
(-1, 1, SLASH), # 右上:走元件的「左下」半条线
(1, -1, SLASH), # 左下:走元件的「右上」半条线
)
def light_up(grid: list[bytes], n: int, m: int) -> int:
"""返回点亮灯泡所需的最少旋转次数,无解返回 -1。
图上的点 = 电路板的 (N+1)×(M+1) 个格点,边 = 斜着跨过一块元件到对角格点。
若元件方向正好能穿过,边权 0;否则要把它转 90°,边权 1。于是问题变成 0-1 BFS。
"""
dist = [[-1] * (m + 1) for _ in range(n + 1)] # -1 表示尚未到达该格点
dist[0][0] = 0
queue = deque([(0, 0)])
while queue:
r, c = queue.popleft()
d = dist[r][c]
if (r, c) == (n, m): # 0-1 BFS 出队即最短路,第一次弹出终点就是答案
return d
for dr, dc, need in STEPS:
nr, nc = r + dr, c + dc
if not (0 <= nr <= n and 0 <= nc <= m):
continue # 目标格点越界
# 跨过的元件就是两个格点围成的那个格子:行列各取小的那个
cost = d + (grid[min(r, nr)][min(c, nc)] != need) # 方向不符要转一次
if dist[nr][nc] < 0 or cost < dist[nr][nc]:
dist[nr][nc] = cost
# 边权 0 走队头、边权 1 走队尾,队列始终按 dist 非递减排列
(queue.appendleft if cost == d else queue.append)((nr, nc))
return -1 # 队列空了还没弹出终点,说明电源与灯泡不连通
def solve() -> None:
data = iter(sys.stdin.buffer.read().split())
n = int(next(data))
m = int(next(data))
grid = [next(data) for _ in range(n)] # 每行一个 bytes,逐字节比较方向更快
ans = light_up(grid, n, m)
print('NO SOLUTION' if ans < 0 else ans)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* 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-10-05 02:25
* update_at: 2026-10-05 02:25
*/
#include <cstdio>
#include <deque>
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
const int MAXN = 505; // N, M <= 500,格点数为 (N+1)*(M+1)
int n; // 元件行数
int m; // 元件列数
string grid[MAXN]; // grid[r][c] 是元件 (r,c) 的导线方向,'\\' 或 '/'
int dist[MAXN][MAXN]; // dist[r][c] = 从电源格点 (0,0) 到格点 (r,c) 的最少旋转次数,-1 表示不可达
// 从当前格点斜着跨过一块元件走到对角格点。
// 每项 = (行偏移, 列偏移, 直通所需方向):跨过的元件总是位于 (min(行), min(列))。
const int STEP_CNT = 4;
const int step_r[STEP_CNT] = {-1, 1, -1, 1};
const int step_c[STEP_CNT] = {-1, 1, 1, -1};
const char step_need[STEP_CNT] = {'\\', '\\', '/', '/'}; // 方向相符则不用旋转,边权 0
// 0-1 BFS:图上的点是 (N+1)*(M+1) 个格点,边是斜跨一块元件。
// 边权 0 的邻点塞队头,边权 1 的塞队尾,保证出队时距离已是最短路。
ll zero_one_bfs() {
for (int r = 0; r <= n; r++) {
for (int c = 0; c <= m; c++) {
dist[r][c] = -1;
}
}
deque<pair<int, int> > dq; // 队列中每个元素是一个格点 (r,c)
dist[0][0] = 0;
dq.push_back(make_pair(0, 0));
while (!dq.empty()) {
int r = dq.front().first;
int c = dq.front().second;
dq.pop_front();
int d = dist[r][c];
if (r == n && c == m) {
return d; // 第一次弹出终点即为答案
}
for (int k = 0; k < STEP_CNT; k++) {
int nr = r + step_r[k];
int nc = c + step_c[k];
if (nr < 0 || nr > n || nc < 0 || nc > m) {
continue; // 目标格点越界
}
// 跨过的元件就是两个格点围成的那一格:行列各取较小的那个
int pr = r < nr ? r : nr;
int pc = c < nc ? c : nc;
int w = 0; // 元件方向正好能穿过时不用旋转
if (grid[pr][pc] != step_need[k]) {
w = 1; // 方向不符,要转 90°,代价 1
}
int nd = d + w;
if (dist[nr][nc] == -1 || nd < dist[nr][nc]) {
dist[nr][nc] = nd;
if (w == 0) {
dq.push_front(make_pair(nr, nc));
} else {
dq.push_back(make_pair(nr, nc));
}
}
}
}
return -1; // 队列空掉仍未到终点,说明无法连通
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int r = 0; r < n; r++) {
cin >> grid[r];
}
ll ans = zero_one_bfs();
if (ans < 0) {
cout << "NO SOLUTION" << '\n';
} else {
cout << ans << '\n';
}
return 0;
}复杂度
- 时间:
。格点数为 ,每个格点最多入队常数次。 - 空间:
。一张 的距离表和双端队列。
在
总结
本题的全部难度在建模,不在算法:
- 连通性属于格点,不属于元件——所以要建
个点的图; - 每对对角格点由唯一一块元件决定——于是每块元件退化成一条边, 权值就是「要不要转这一下」,只有 0 和 1;
- 0/1 权图的最短路用 0-1 BFS——双端队列保证出队即定稿,
收工。
一旦完成第 2 步,问题就从
