Codeforces 1063B Labyrinth:用位移守恒压缩状态,再走进 0-1 BFS
520B 用普通 BFS 处理单位代价,20C 用 Dijkstra 处理一般非负边权。这一次,我们把边权限制为 0 和 1,看看为什么小根堆可以换成双端队列。
不过,1063B 最值得练习的地方还在队列之前:题目同时限制左移和右移次数,为什么每个格子只需要保存一个最短距离?只有证明两个资源之间的关系,状态压缩才有依据。
1. 官方信息与问题模型
1063B · Labyrinth 官方题目 于 2026-09-09 核对:难度 1800,标签为 graphs、shortest paths,时间限制 2 秒,内存限制 512 MB。
将题意整理成一个模型:在有障碍的矩形网格中,从给定空格出发,每步走向一个相邻空格;上下不限次数,左移至多 x 次,右移至多 y 次。求有多少个格子能由某条符合预算的路径到达,起点也算一个。
| 官方约束 | 算法含义 |
|---|---|
1 ≤ n,m ≤ 2000 |
最多四百万个格子,不宜为每格分配复杂容器 |
0 ≤ x,y ≤ 10^9 |
不能把预算直接作为大规模 DP 数组维度 |
| 起点行列为 1-based,且保证为空格 | 读入后统一减一,起点距离为 0 |
. 为空格,* 为障碍 |
只在相邻空格间生成边,无需显式邻接表 |
这里的“可达”是逐个格子的存在性判断,不要求用同一条路径访问所有计入答案的格子。
2. 为什么朴素搜索不够
直接记录 (行, 列, 已左移次数, 已右移次数) 是容易理解的正确模型,但状态上界会带上 (x+1)(y+1),无法应对十亿预算。它适合小数据验证,不适合提交。
只记录“这个格子是否访问过”的普通 BFS 则换了优化目标:它先找到的是总步数最少的路径。上下走很远但不消耗左移预算的路线,可能比走得短但频繁横移的路线更有价值。第一次入队不能据此锁定最佳资源消耗。
如果已经得到正确的 0/1 权重,直接用 Dijkstra 也能求解;它的堆操作会增加对数因子。我们先推导权重,再利用只有两种边权的特殊结构。
3. 位移守恒:右移次数可以算出来
起点列为 c,到达目标列 j 的一条路径左移 L 次、右移 R 次。上下移动不改变列,每次右移加一、左移减一,因此:
1 | R - L = j - c |
对同一个目标格子,j-c 是常数。若两条路径满足 L₁ < L₂,必有 R₁ < R₂。所以在这个格子上,左移更少的路径同时也右移更少,不存在“省左移却多花右移”的权衡。
于是定义:
1 | dist[i][j] = 从起点到 (i,j) 的最少左移次数 |
如果格子可达,其对应的最少右移次数就是 dist[i][j] + j - c。这里两者由同一条最少左移路径同时实现,并非分别求两个可能无法兼容的最优值。
flowchart TD
A[固定目标格子] --> B[列位移固定]
B --> C[右移次数 = 左移次数 + 列位移]
C --> D[左移更少则右移也更少]
D --> E[每格只保存最少左移次数]
E --> F[还原右移次数并检查两项预算]
这一步不能机械迁移到任意双资源问题。如果移动还会消耗燃料、钥匙或时间,而它们不能由位置和一个资源唯一确定,就可能需要保留多个互不支配的状态。
4. 把移动变成有向 0/1 边
| 移动 | 左移次数变化 | 边权 | 成功松弛后放在哪里 |
|---|---|---|---|
| 上 | 0 | 0 | 队首 |
| 下 | 0 | 0 | 队首 |
| 右 | 0 | 0 | 队首 |
| 左 | 1 | 1 | 队尾 |
右移边权为 0 只表示它不增加当前距离指标——左移次数,并不表示右移预算无限。右移限制会在最后检查。
几何上相邻的两个空格可以互相走,但左右两个方向的边权不同,分析时要把它们看作有向边。
下面是说明队列顺序的抽象图,并非题目的网格:
graph LR
S[起点 S] -->|1| T[目标 T]
S -->|0| A[中间点 A]
A -->|0| T
若把 T 第一次入队时的距离 1 当成最终答案,就漏掉了经 A 到达 T 的代价 0。正确做法是允许严格改善距离;旧的距离记录留在队列里,弹出时再跳过。
5. 双端队列为何保持最短路顺序
队列元素保存 (入队时的距离, 格子编号),而不只保存编号。每次弹出队首,若记录距离与最新 dist 不同,说明它已过期。
设当前弹出的有效距离是 d。队列中的记录按距离非递减排列,而且只涉及当前的两个相邻层。沿 0 权边得到 d,插到队首;沿 1 权边得到 d+1,插到队尾。这样就维持了顺序,不需要堆来寻找最小值。
| 操作 | 队列示意,左侧为队首 |
|---|---|
弹出有效记录 (d,u) 后 |
[d 的其他记录 … d+1 的记录] |
| 0 权边改善 v | [(d,v), d 的其他记录 … d+1 的记录] |
| 1 权边改善 w | [d 的记录 … d+1 的记录, (d+1,w)] |
| 弹出旧记录 | 距离不一致,直接跳过,不扩展邻居 |
最短距离正确性。 假设某格子 u 首次以有效记录弹出时,记录值大于真实最短距离。在一条更短路径上,取第一个尚未确定的格子 v;其前驱已确定,因而曾沿路径边向 v 松弛。非负边权保证 v 的记录不会大于那条路径到 u 的总成本,故队列中应有比 u 更小的有效记录先弹出,矛盾。
这就是 Dijkstra 的选点论证,只是这里由双端队列实现按最小距离扩展。0 权边不破坏证明;负权边才会破坏所依赖的非负性。
6. 从最短距离到预算判定的证明
令目标格子的最短距离为 d,列位移为 Δ。判定条件是:
1 | d 不是无穷大,并且 d ≤ x,并且 d + Δ ≤ y |
必要性: 假设存在可行路径,其左右次数为 L、R。由最短距离定义有 d≤L≤x,由位移恒等式有 d+Δ≤L+Δ=R≤y。
充分性: 取一条实现最短距离 d 的真实路径,它的右移次数必为 d+Δ。若两项均不超过预算,这条路径就是可行路径。沿路左右次数只增不减,终点累计不超预算也保证每个前缀不超预算。
右移次数不会为负,因为它来自真实路径;代码仍显式检查非负,方便读者对照含义。不可达格子必须先排除,不能拿无穷大参与位移计算。
7. 手工演算:同一列也可能必须左右绕行
使用自拟网格,起点 S 位于第 1 行第 3 列,x=0,y=1。输入时将 S 换成 .:
1 | ..S.. |
| 目标(1-based) | 一条最少左移路径 | d | Δ | 右移 d+Δ | 是否计入 |
|---|---|---|---|---|---|
| (1,3) | 不移动 | 0 | 0 | 0 | 是 |
| (1,4) | 右 | 0 | 1 | 1 | 是 |
| (2,4) | 右、下 | 0 | 1 | 1 | 是 |
| (3,4) | 右、下、下 | 0 | 1 | 1 | 是 |
| (3,3) | 右、下、下、左 | 1 | 0 | 1 | 否,超出左移预算 |
| (1,5) | 右、右 | 0 | 2 | 2 | 否,超出右移预算 |
| (1,2) | 左 | 1 | -1 | 0 | 否,超出左移预算 |
最终答案是 4。起点正下方的障碍说明:仅按目标的列差检查预算不够,必须考虑绕路;同列也不等于左右次数都是零。
8. 完整 C++17 实现
将 (i,j) 编为 i*m+j,用连续数组存距离;邻居现场计算。搜索阶段不裁剪预算,最后统一统计,使状态含义与证明一致。
1 |
|
9. 复杂度、整数与错误清单
每个格子首次有效弹出时距离已确定;旧记录不会重复扩展邻居。每条边至多被扫描一次,每次成功松弛产生一条入队记录,总时间 O(V+E)=O(nm)。格子出度至多 4,队列连同旧记录仍为 O(E)=O(nm) 空间;网格与距离数组同样为 O(nm)。
非负权图中的最短路可以去掉环,故最少左移次数不超过 nm-1≤3,999,999。编号、距离以及最多四百万的答案都能用 int。预算及还原右移次数用 long long,无穷大不参与加法。
| 易错点 | 后果 | 检查方式 |
|---|---|---|
| 把右移也算成当前指标的 1 | 指标变成总横移,原来的还原公式失效 | 明确 dist 只统计左移 |
写成 R=L+c-j |
左右方向颠倒 | 起点右邻格必须得到 R=1 |
| 0 权边入队尾、1 权边入队首 | 不再保证按最短距离扩展 | 对照队列不变量 |
| 第一次入队就永久标记访问 | 后来的更优路线无法松弛 | 用严格距离改善,弹出时查旧记录 |
| 相等距离也入队 | 上下零代价环造成无谓重复 | 只接受 candidate < dist[next] |
| 忘记起点或允许穿障碍 | 答案偏差 | 单格、包围起点、单行、单列 |
| 只检查列差 | 忽略必须先绕行的障碍 | 使用上一节自拟网格 |
将横向两种移动都赋权 1 也能发展出另一种正确算法,但此时距离是 H=L+R,还原式必须改成 L=(H-Δ)/2,R=(H+Δ)/2。不要把两套距离定义与公式混用。
10. 用不压缩状态的算法独立对拍
验证器从本文代码块提取原始 C++17,编译后逐个输入测试,避免“文章代码”和“测试代码”各写一套。
小网格的参考算法保留 (行,列,L,R) 全部四维状态,用普通队列枚举所有预算内状态,最终对位置去重。它不使用位移公式,也不使用双端队列,可以同时检验状态压缩和最短路实现。
验证入口:tests/verify-labyrinth-article.cjs。覆盖官方两组样例、本文绕行表、所有 2×2 障碍布局的合法起点与小预算组合、固定种子随机网格,以及 2000×2000 空网格和十亿预算。大网格使用可直接计数的独立答案,检查规模与边界;随机对拍不能替代上面的正确性证明。
完成后可以返回 算法练习路径,对比三种队列:单位边权用 FIFO,0/1 边权用 deque,一般非负边权用小根堆。真正共通的目标,是让最小的待处理距离先得到扩展。