目录
[一、认识 0-1 BFS](#一、认识 0-1 BFS)
[1.1 什么是 0-1 BFS](#1.1 什么是 0-1 BFS)
[1.2 为什么不直接使用普通 BFS](#1.2 为什么不直接使用普通 BFS)
[二、0-1 BFS 的核心原理](#二、0-1 BFS 的核心原理)
[2.1 使用双端队列维护节点](#2.1 使用双端队列维护节点)
[2.2 松弛操作](#2.2 松弛操作)
[三、0-1 BFS 的代码实现](#三、0-1 BFS 的代码实现)
[3.1 算法复杂度](#3.1 算法复杂度)
[四、0-1 BFS 的正确性证明](#四、0-1 BFS 的正确性证明)
[4.1 双端队列的处理顺序](#4.1 双端队列的处理顺序)
[4.2 为什么第一次有效取出时,距离已经最短](#4.2 为什么第一次有效取出时,距离已经最短)
[5.1 洛谷 P4554:小明的游戏](#5.1 洛谷 P4554:小明的游戏)
[5.2 洛谷 P1346:电车](#5.2 洛谷 P1346:电车)
[5.3 AtCoder ABC176 D:Wizard in Maze](#5.3 AtCoder ABC176 D:Wizard in Maze)
[5.4 Codeforces 1063B:Labyrinth](#5.4 Codeforces 1063B:Labyrinth)
[6.1 洛谷 P4667:Switch the Lamp On](#6.1 洛谷 P4667:Switch the Lamp On)
[6.2 Codeforces 590C:Three States](#6.2 Codeforces 590C:Three States)
[6.3 洛谷 P1948:Telephone Lines](#6.3 洛谷 P1948:Telephone Lines)
[七、0-1 BFS 的常见错误](#七、0-1 BFS 的常见错误)
[7.1 使用普通队列](#7.1 使用普通队列)
[7.2 将第一次发现节点当作最短距离](#7.2 将第一次发现节点当作最短距离)
[7.3 忽略有向图和无向图的区别](#7.3 忽略有向图和无向图的区别)
[7.4 将 0-1 BFS 用于任意非负边权](#7.4 将 0-1 BFS 用于任意非负边权)
一、认识 0-1 BFS
在图论中,最短路问题是一个非常重要的知识点。对于不同的边权情况,我们通常会选择不同的算法解决。
例如:
-
当所有边权都相同 时,可以使用普通 BFS 求最短路。
-
当边权非负,但不一定相同时,可以使用 Dijkstra 算法求最短路。
-
当边权只有 0 和 1 两种取值时,我们还可以使用一种特殊的最短路算法------0-1 BFS。
0-1 BFS 可以看作对普通 BFS 的一种扩展。它利用双端队列维护待处理的节点,在边权只有 0 和 1 的情况下,可以在线性时间内求出单源最短路。
1.1 什么是 0-1 BFS

1.2 为什么不直接使用普通 BFS
普通 BFS 适用于无权图,或者所有边权都相同的图。
在普通 BFS 中,我们按照节点距离起点的层数依次遍历。由于每经过一条边,路径长度都会增加相同的值,因此第一次到达某个节点时,就已经获得了它的最短距离。
但在 0-1 权图中,情况发生了变化。
假设从起点 (s) 到节点 (v) 存在两条路径:
-
路径 A:经过 3 条边,每条边权均为 0,总权值为 0。
-
路径 B:只经过 1 条边,但这条边的权值为 1,总权值为 1。
显然,路径 A 才是最短路。
这说明:经过的边数更少,并不意味着路径权值之和更小。
普通 BFS 无法直接按照边数确定最短路,因此我们需要改变节点的处理顺序。
二、0-1 BFS 的核心原理
2.1 使用双端队列维护节点
普通 BFS 使用普通队列 queue,而 0-1 BFS 使用双端队列 deque。
双端队列既可以从队头插入和删除元素,也可以从队尾插入和删除元素。
在 0-1 BFS 中,我们根据边权决定新节点的插入位置:
-
如果边权为 0,将更新后的节点加入队头
-
如果边权为 1,将更新后的节点加入队尾

通过这种方式,0-1 BFS 利用双端队列维护待处理节点的距离顺序,使距离较小的节点优先得到处理,从而高效地求解最短路问题。
2.2 松弛操作
0-1 BFS 依然使用最短路算法中的松弛操作。

然后根据边权,将节点 v 插入双端队列的队头或队尾。
这里需要注意,不能仅仅因为某个节点已经被发现,就认为它的最短距离已经确定。
三、0-1 BFS 的代码实现
cpp
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
/*
*/
static const int moves[] = {-1, 0, 1, 0, -1};
void solve()
{
int n, a, b;
cin >> n >> a >> b;
vector<vector<pair<int, int>>> grahp(n + 1);
for(int i = 1; i <= n; ++i)
{
int sz;
cin >> sz;
for(int j = 0; j < sz; ++j)
{
int v, w = 1;
cin >> v;
if(j == 0)
w = 0;
grahp[i].push_back({v, w});
}
}
// a -> b 的最短路径
vector<int> dis(n + 1, INT_MAX);
deque<int> dq;
dq.emplace_front(a);
dis[a] = 0;
while(!dq.empty())
{
int node = dq.front();
dq.pop_front();
if(node == b)
{
cout << dis[node] << "\n";
return ;
}
for(auto &[next, w] : grahp[node])
{
if(dis[node] + w < dis[next])
{
dis[next] = dis[node] + w;
if(w == 0)
dq.push_front(next);
else
dq.push_back(next);
}
}
}
cout << -1 << "\n";
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t = 1;
// cin >> t;
while (t--)
solve();
return 0;
}
这里使用邻接表存储图,graphu 中的每个元素都是一个二元组:
-
v:当前边指向的节点
-
w:当前边的权值,必须为 0 或 1
需要注意,这个模板默认使用非负的 0/1 边权。如果原图是无向图,建图时需要将每条边分别加入两个方向;如果原图是有向图,则只添加题目允许的方向。
3.1 算法复杂度
对于具有 (N) 个节点、(M) 条边的图,0-1 BFS 的时间复杂度为:O(N + M)
空间复杂度为:O(N + M)
其中,邻接表用于存储图,距离数组和双端队列用于维护最短路计算过程中的状态。
在边权严格限制为 0 和 1 的情况下,0-1 BFS 通常比使用堆优化的 Dijkstra 更简单,也具有更低的常数开销。
四、0-1 BFS 的正确性证明
为什么只根据边权决定队头或队尾,就能够保证求出最短路?
我们可以从普通 BFS 和 Dijkstra 的思路出发进行理解。
4.1 双端队列的处理顺序
假设当前处理的节点距离为 (d)。
对于权值为 0 的边,新节点距离仍然为 (d),因此将它加入队头。
对于权值为 1 的边,新节点距离为 (d+1),因此将它加入队尾。
在这个过程中,双端队列能够按照有效距离的非递减顺序处理节点。这与 Dijkstra 算法使用优先队列,每次取出当前距离最小的节点,有相似的思想。
4.2 为什么第一次有效取出时,距离已经最短
假设节点 (u) 第一次以当前最短距离对应的有效记录被取出,但它的距离还不是最短距离。
那么必然存在另一条更短的路径到达 (u)。
沿着这条更短的路径,考虑第一个尚未被以最短距离处理的节点。它的前驱节点必然能够先以不大于 (distu) 的距离得到处理,并通过权值为 0 或 1 的边将这个节点加入双端队列。
由于更短路径对应的距离更小,这个节点应当优先于距离较大的 (u) 被处理,与 (u) 已经以当前距离成为第一个有效取出节点相矛盾。
因此,节点第一次以有效记录被取出时,其距离已经是最短距离。
这也解释了为什么过期记录可以直接跳过:后续路径不会再将一个已经以最短距离处理的节点更新为更短距离。
五、经典题目中的建模思路
掌握 0-1 BFS 的模板只是第一步。实际做题时,更重要的是判断题目中的什么操作对应权值为 0,什么操作对应权值为 1。
5.1 洛谷 P4554:小明的游戏
题目链接:P4554 --- 小明的游戏
这道题将网格中的相邻移动转换为边。
当从一个格子移动到相邻格子时:
-
如果两个格子的字符相同,移动代价为 0。
-
如果两个格子的字符不同,移动代价为 1。
因此,我们可以直接将每个格子看作图中的节点,并根据相邻格子的字符关系确定边权。
这道题的重点在于理解:移动距离不一定等于移动步数,真正需要最小化的是路径上的总代价。
5.2 洛谷 P1346:电车
题目链接:P1346 --- 电车
这道题的关键是根据题意构造有向图。
每个路口可以通向若干其他路口,其中:
-
第一条指定方向的线路代价为 0。
-
选择其他方向的线路,需要付出代价 1。
因此,我们将路口作为节点,将可以通行的线路作为有向边,并根据是否需要改变原先指定的方向设置边权。
这里的难点不在于双端队列的操作,而在于将实际问题中的选择和代价抽象成有向图中的边权。
5.3 AtCoder ABC176 D:Wizard in Maze
题目链接:ABC176 D --- Wizard in Maze
这道题进一步改变了普通网格图的转移方式。
我们可以将移动操作分为两类:
-
普通上下左右移动:代价为 0。
-
使用魔法移动到允许的范围内:代价为 1。
于是,网格中的每个可通行格子仍然作为一个节点,普通移动对应权值为 0 的边,魔法移动对应权值为 1 的边。
这道题说明,0-1 BFS 并不要求图必须具有某种固定结构。只要能够把状态之间的转移表示为权值为 0 或 1 的边,就可以考虑使用它。
5.4 Codeforces 1063B:Labyrinth
这道题的关键是将移动限制转换为最短路问题。
题目分别限制向左和向右移动的次数。我们可以将:
-
向左移动的代价设置为 1。
-
向右、向上、向下移动的代价设置为 0。
通过 0-1 BFS,我们可以计算到达每个格子所需要的最少向左移动次数。
设起点的列坐标为 (s_y),当前格子的列坐标为 (y),最少向左移动次数为 (L),向右移动次数为 (R)。
由于每次向右移动使列坐标增加 1,每次向左移动使列坐标减少 1,因此:
y - s_y = R - L
整理可得:
R = L + y - s_y
这样,我们就可以根据计算得到的 (L) 推导出 (R),进而判断当前格子是否满足向左、向右移动次数的限制。
最短路算法不仅可以求解路径代价,还可以通过恰当的代价定义,将题目中的约束转换为可以计算的量。
六、进阶题目与进一步思考
完成前四道题之后,接下来可以逐步尝试更复杂的建模问题。
6.1 洛谷 P4667:Switch the Lamp On
题目链接:P4667 --- Switch the Lamp On
这道题不再是简单地将每个格子作为节点,而是需要重新定义图中的节点和边。
一种常见建模方式是:
-
将网格交点作为节点。
-
将相邻交点之间的对角线连接关系作为边。
-
如果当前格子的斜线方向符合穿越要求,边权为 0。
-
如果需要旋转斜线才能通过,边权为 1。
这道题的重点是如何从图形结构中提取节点、边以及对应的修改代价。
此外,还需要注意网格的奇偶性:如果起点与终点之间的格点奇偶性不满足可达条件,就不存在合法路径。
6.2 Codeforces 590C:Three States
这道题涉及三个不同的区域,需要考虑如何连接它们。
可以分别从三个区域出发进行最短路计算:
-
进入普通空地需要付出代价 1。
-
进入区域本身的格子不需要额外付出代价。
-
墙壁不能通行。
每次计算都可以使用 0-1 BFS 得到对应区域到其他位置的最小代价。
之后,需要考虑如何组合三组距离,得到连接三个区域的最小总代价。
特别要注意,当三个区域通过同一个普通空地连接时,这个空地可能会在三组距离中重复计费,因此组合答案时必须修正重复计算的部分。
这道题进一步训练的是多源最短路结果的组合,而不只是单次最短路的计算。
6.3 洛谷 P1948:Telephone Lines
题目链接:P1948 --- Telephone Lines
这道题的核心是将二分答案与 0-1 BFS 结合起来。
假设当前二分的候选答案为 (x),那么我们可以将每条边的权值重新定义为:

此时,0-1 BFS 求出的最短距离,就表示从起点到终点的路径中,权值超过 (x) 的边的最少数量。
我们只需要判断这个数量是否不超过题目允许的免费边数量,就能检查当前的候选答案是否可行。
这道题的难点在于:先通过二分答案确定判定条件,再把判定过程转化为 0-1 BFS。
它说明,算法的价值不仅在于单独解决某个问题,也在于和其他算法组合后解决更复杂的问题。
七、0-1 BFS 的常见错误
7.1 使用普通队列
如果仍然使用普通队列,并且所有新节点都从队尾加入,就无法保证节点按照最短距离的顺序处理。因此,必须根据边权选择双端队列的插入位置。
7.2 将第一次发现节点当作最短距离
在最短路算法中,一个节点被发现,不意味着它的最短距离已经确定。
如果后续找到了更短的路径,就需要更新距离。
在本文的模板中,我们使用松弛操作更新距离,并通过距离记录判断队列中的旧状态是否已经失效。
7.3 忽略有向图和无向图的区别
有向图中的边只能沿指定方向通行,而无向图中的边通常需要同时添加两个方向。
如果建图时没有正确处理边的方向,即使 0-1 BFS 模板完全正确,也可能得到错误答案。
7.4 将 0-1 BFS 用于任意非负边权
0-1 BFS 的高效性建立在边权只有 0 和 1 的条件上。
如果边权可能为 2、3 或其他非负数,就不能直接使用这个模板。一般情况下,应考虑使用 Dijkstra 等适合相应边权范围的算法。
八、总结
从 P4554 到 P1346,再到 ABC176 D 和 CF1063B,虽然它们都可以使用 0 - 1 BFS,但每道题的建模方式并不相同:
-
有的题目根据格子属性确定移动代价。
-
有的题目根据操作选择确定边权。
-
有的题目将普通移动和特殊移动划分为两类。
-
有的题目将移动次数限制转换为最短路距离。
对于 0-1 BFS,我认为最重要的不是记住双端队列的两种插入方式,而是建立这样一种思考习惯:
当题目中的转移代价只有 0 和 1 时,能否将问题转换为图上的最短路?如果可以,节点、边以及边权应该如何定义?
当能够独立回答这些问题时,才算真正开始掌握 0-1 BFS。