洛谷好题推荐 2026年10月24日

P4667 BalticOI 2011 Switch the Lamp On (Day1)

题目描述

Casper 在一个 N×MN\times MN×M 的电路板上设计电路。有一种正方形的电路元件,在它的两组相对顶点中,有一组会用导线连接起来,另一组则不会。有 N×MN\times MN×M 个这样的元件,你想将其排列成 NNN 行,每行 MMM 个。 电源连接到板的左上角。灯连接到板的右下角。只有在电源和灯之间有一条电线连接的情况下,灯才会亮着。为了打开灯,任何数量的电路元件都可以转动 90°(两个方向)。

在上面的图片中,灯是关着的。如果从右往左数第二列的任何一个电路元件被旋转 90°,电源和灯都会连接,灯被打开。现在请你编写一个程序,求出最小需要旋转多少电路元件。

输入格式

输入的第一行包含两个整数 NNN 和 MMM,表示电路板的尺寸。 在以下 NNN 行中,每一行有 MMM 个符号 \ 或 /,表示连接对应电路元件对角线的导线的方向。

输出格式

如果可以打开灯,那么输出只包含一个整数,表示最少转动电路元件的数量。

如果不可能打开灯,输出 NO SOLUTION。

输入输出样例 #1

输入 #1

复制代码
3 5
\\/\\
\\///
/\\\\

输出 #1

复制代码
1

说明/提示

对于 40%40\%40% 的数据,1≤N≤41 \le N \le 41≤N≤4,1≤M≤51 \le M \le 51≤M≤5。

对于所有数据,1≤N,M≤5001 \le N,M \le 5001≤N,M≤500。


P4667 详细讲解(面向算法小白)

一、先理解题目在说什么

想象你有一块 N 行 M 列 的电路板,每个小方格里有 一根斜着的导线 ,可能是 \ 也可能是 /。

  • 电源接在左上角
  • 灯接在右下角
  • 每个方格都可以单独旋转 90° (\ 变 /,/ 变 \)
  • 问:最少旋转多少个方格,能让电从左上角一路通到右下角?

可以把它想象成"拼水管"游戏:每个格子是一根斜着的水管,你要让水从左上角流到右下角。不通就把某些管子转一下,求最少转几次。


二、关键洞察:把它变成"图"

1. 顶点在哪里?

每个小方格有 4 个角 。相邻方格的角是重合的。所以整块 N × M 的板子,其实有:

复制代码
(N+1) 行 × (M+1) 列 个"角点"

我们把每个角点看成一个 图上的顶点。

用 (i, j) 表示第 i 行、第 j 列的角点(从 0 开始数):

复制代码
(0,0)   (0,1)   (0,2)   ...  (0,M)
(1,0)   (1,1)   (1,2)   ...  (1,M)
  ...
(N,0)   (N,1)   (N,2)   ...  (N,M)   ← 终点(右下角)
 ↑
 起点(左上角)

2. 边在哪里?

每个小方格里有一根斜线,它把 两个对角 连起来------这条线就是图的一条边。

假设现在是第 i 行第 j 列的小方格(0 ≤ i < N, 0 ≤ j < M),它的四个角是:

  • 左上角 (i, j)
  • 右上角 (i, j+1)
  • 左下角 (i+1, j)
  • 右下角 (i+1, j+1)

那么:

方格内容 导线连接的两个角 图上的边
\ 左上 ↔ 右下 (i,j) ------ (i+1,j+1)
/ 右上 ↔ 左下 (i,j+1) ------ (i+1,j)

3. 旋转代价是什么?

一个格子有两种可能状态:

  • 状态 A:连接 左上-右下 (即 \)
  • 状态 B:连接 右上-左下 (即 /)

每种格子永远只有这两条对角线中的一条。 想让另一条出现,就必须旋转它(代价 1)。

所以对于每个格子,从图的角度看:

  • 想要的那条对角线 出现 → 代价 0
  • 另一条对角线 才出现 → 代价 1

4. 题目变成图论问题

从左上角顶点 (0,0) 走到右下角顶点 (N,M),每条边有一个代价 0 或 1,求最小总代价。

这就是一个 最短路问题,只不过边权只有 0 和 1。


三、建图的具体例子

假设一个 1 × 1 的板子,内容是 \:

复制代码
(0,0)    (0,1)
  +--------+
  | \      |
  +--------+
(1,0)    (1,1)
  • 这个格子是 \,所以它 本来就有 连接 (0,0) 和 (1,1) 的边 → 代价 0
  • 如果我想让 (0,1) 和 (1,0) 相连,就需要旋转它 → 代价 1

所以图上:

复制代码
(0,0) ───0─── (1,1)
(0,1) ───1─── (1,0)

从 (0,0) 到 (1,1) 的最短路是 0(不用旋转),灯就亮。

再假设内容是 /:

复制代码
(0,0)    (0,1)
  +--------+
  | /      |
  +--------+
(1,0)    (1,1)
  • 本来就有 (0,1) ------ (1,0) 的边,代价 0
  • 想要 (0,0) ------ (1,1) 的边,要转一格,代价 1

从 (0,0) 到 (1,1) 最短距离是 1,转一次就亮。

思路通了。 接下来的任务就是在这个大图上跑最短路。


四、为什么用 0-1 BFS?

最短路算法挺多,为什么不用 Dijkstra?

因为 边权只有 0 和 1 。这种特殊情况下,有一个更快的算法叫 0-1 BFS:

  • 用 双端队列 deque(两头都能进出的队列)
  • 从队首取点,看它的邻居
  • 如果走这条边代价是 0 → 新点 插入队首
  • 如果代价是 1 → 新点 插入队尾

为什么这样对?

关键是不变式:队列里点的距离永远是"非递减"的(前面小,后面大)。这和 Dijkstra 里"优先队列按距离出队"是一个道理。

类比一下:

  • Dijkstra :用优先队列,每次取最小距离的点。代价 O(E log V)。
  • 0-1 BFS :因为距离只有两种增长方式(+0 或 +1),用双端队列就够了。代价 O(E),更快。

代码结构:

cpp 复制代码
deque<int> dq;
dist[start] = 0;
dq.push_front(start);

while (!dq.empty()) {
    int u = dq.front(); dq.pop_front();
    if (visited[u]) continue;
    visited[u] = true;

    for (每条从 u 出发的边 e) {
        if (dist[e.to] > dist[u] + e.w) {
            dist[e.to] = dist[u] + e.w;
            if (e.w == 0) dq.push_front(e.to);  // 0 边 → 放前面
            else          dq.push_back(e.to);   // 1 边 → 放后面
        }
    }
}

关键就是这个 push_front / push_back。


五、代码逐行讲解

1. 读入

cpp 复制代码
int N, M;
cin >> N >> M;
vector<string> board(N);
for (int i = 0; i < N; i++) cin >> board[i];

board[i][j] 表示第 i 行第 j 列格子的字符(\ 或 /)。

⚠️ 注意转义 :C++ 里 '\' 表示"转义字符",如果你真的要表示一个反斜杠,要写 '\\'。输入进去后 board[i][j] 存的确实就是单个字符 \,代码里比较要用 '\\'。

2. 给顶点编号

每行有 M + 1 个角点,所以:

cpp 复制代码
int C = M + 1;   // 每行的角点数
auto id = [&](int x, int y) { return x * C + y; };

这样 id(i, j) 就把二维坐标 (i, j) 映射成一维编号(0 到 (N+1)*(M+1)-1)。

举例 N=3, M=5:

  • (0,0) → 0
  • (0,1) → 1
  • (0,6) → 6 (下一行第一个)
  • (3,5) → 3*6 + 5 = 23(右下角)

3. 建图

cpp 复制代码
for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        int u1 = id(i, j);         // 左上
        int v1 = id(i + 1, j + 1); // 右下
        int u2 = id(i, j + 1);     // 右上
        int v2 = id(i + 1, j);     // 左下

        if (board[i][j] == '\\') {
            addEdge(u1, v1, 0);   // \ 本来就连左上-右下
            addEdge(u2, v2, 1);   // 想要右上-左下?转一次
        } else { // '/'
            addEdge(u2, v2, 0);   // / 本来就连右上-左下
            addEdge(u1, v1, 1);   // 想要左上-右下?转一次
        }
    }
}

addEdge(u, v, w) 加一条无向边 (两个方向都能走),代价 w:

cpp 复制代码
auto addEdge = [&](int u, int v, int w) {
    graph[u].push_back({v, w});
    graph[v].push_back({u, w});   // 无向边
};

为什么要加两条边? 因为电可以在一条导线上"双向流动",从 u 走到 v 也行,从 v 走到 u 也行。

注意:每个格子只有一条实际的导线。但为了让 BFS 探索方便,我们把它写成两条(一条代表"无需旋转的直接通路",一条代表"旋转后打通的对角线"),代价分别是 0 和 1。这样才能表示"如果我选这条路就要额外旋转"。

4. 跑 0-1 BFS

cpp 复制代码
int start = id(0, 0);
int target = id(N, M);

vector<int> dist(P, 1e9);
deque<int> dq;

dist[start] = 0;
dq.push_front(start);

while (!dq.empty()) {
    int u = dq.front(); dq.pop_front();
    // ... 更新邻居
}

BFS 结束后,dist[target] 就是从左上角到右下角的最少旋转数。

5. 判断无解

cpp 复制代码
if (dist[target] == 1e9) cout << "NO SOLUTION\n";
else cout << dist[target] << '\n';

六、为什么可以提前判断无解(一个小技巧)

你想:从 (0,0) 出发,走的每条边都是对角线,也就是说每一次移动,i + j 的奇偶性都不变(+1 再 -1,或者 -1 再 +1,总和不变)。

  • 起点 (0,0):i + j = 0(偶)
  • 终点 (N,M):i + j = N + M

如果 N + M 是奇数,就永远到不了! 直接输出 NO SOLUTION 就行。

不过不判断也没关系,BFS 自然会算出无法到达(距离还是无穷大)。


七、完整流程演示

用题目样例:

复制代码
3 5
\\/\\
\\///
/\\\\

最终答案是 1,意味着只需要旋转 1 个元件。BFS 会找到一条所有边代价加起来为 1 的路径。

(具体哪条路径不方便手画,但原理一样。)


八、复杂度分析

设 V = (N+1)(M+1) 是点数,E 是边数。

  • 每个格子最多贡献 2 条无向边 ,所以 E = O(NM)
  • 0-1 BFS 每个点最多出队一次,每条边最多被松弛几次 → O(V + E) = O(NM)
  • 空间:O(V + E) = O(NM)

题目 N, M ≤ 500,NM ≤ 250000,完全没问题。

如果用 Dijkstra + 优先队列 是 O(NM log NM),也能过,但 0-1 BFS 更快更简洁。


九、易错点提醒

  1. C++ 里 \ 要写成 '\\' ,比较字符时要用 board[i][j] == '\\'。
  2. 顶点是 (N+1) × (M+1) 个 ,不是 N × M(因为角点比格子多一行一列)。数组大小别开小了。
  3. 无向边要加两次,否则可能漏方向。
  4. 队首取点时要判断 visited,防止同一个点被处理多次。
  5. 0-1 BFS 中 边权为 0 放队首,边权为 1 放队尾,这个千万别搞反,搞反就成了普通 BFS 求错答案。

十、整体思路回顾

复制代码
理解题意
   ↓
把每个角点当作图的顶点
每个格子当成连接对角两点的"边"
   ↓
边权:本来就连通的 = 0,旋转后连通的 = 1
   ↓
求从 (0,0) 到 (N,M) 的最短路
   ↓
边权只有 0/1 → 用 0-1 BFS 高效求解
   ↓
如果终点距离还是 INF → NO SOLUTION

核心就一句话:"最少旋转次数" = "一条路径上边权之和的最小值",而边权只有 0 和 1,所以用 0-1 BFS。

这就是这道题的灵魂。慢慢消化这个"把物理问题转成图"的思路,以后很多题都会用到。

相关推荐
扶尔魔ocy1 小时前
【C++开发】从裸指针到智能指针
c++·智能指针·1024程序员节
xUxIAOrUIII1 小时前
CS329a-推理时计算扩展
1024程序员节
KryHan1 小时前
【JavaScript】第三章数组
1024程序员节
小南家的青蛙1 小时前
Redis的资料
redis·缓存·1024程序员节
初阳7851 小时前
【基于Qt5仿音乐播放器(一)】——项目准备
qt·1024程序员节
CAIE研习社2 小时前
跨专业考证如何帮助求职?从原有专业到AI应用能力的组合思路
1024程序员节
小小龙学IT2 小时前
Python Polars 高性能数据处理引擎深度解析
开发语言·python·rust·1024程序员节
Nuanyt2 小时前
Hot 100 刷题笔记 · 数组与哈希专题(3 题)
java·笔记·算法·leetcode·哈希算法·1024程序员节
Hardworking6662 小时前
2026年10月24日系统集成项目管理工程师上午考试内容
1024程序员节·系统集成项目管理工程师