引言
在图论中,Dijkstra 算法可以求解非负权边的单源最短路径问题,时间复杂度为 O((V+E) log V)。然而,当图中所有边的权重仅为 0 或 1 时,存在一种更优雅的算法——双端队列 BFS(0-1 BFS)。它利用双端队列(Deque)在 O(V + E) 的线性时间内完成求解,无需优先队列的开销。本文将用 Java 实现该算法,并结合迷宫逃生场景进行讲解。
核心思想
0-1 BFS 的关键观察:
– 经过权重为 0 的边时,新距离 不大于 当前节点距离,应放在队列头部
– 经过权重为 1 的边时,新距离 大于 当前节点距离,应放在队列尾部
这样双端队列始终按距离递增顺序存储节点,保证了首次访问即为最短距离,类似于 Dijkstra 但无需堆结构。
算法流程
- 初始化距离数组
dist[]为无穷大,dist[start] = 0 - 将起点放入双端队列
- 从队首取出节点
u - 遍历
u的所有邻接边(u, v, w): - 若
dist[u] + w < dist[v],更新dist[v] - 若
w == 0,将v加入队首;若w == 1,将v加入队尾 - 重复直到队列为空
完整 Java 实现
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
/**
* 0-1 BFS 实现:求解 0-1 权重无向图的最短路径
*/
public class ZeroOneBFS {
/**
* 图的邻接表边表示
*/
static class Edge {
int to; // 目标节点
int weight; // 权重(仅 0 或 1)
Edge(int to, int weight) {
this.to = to;
this.weight = weight;
}
}
private final int n; // 节点数量
private final List<Edge>[] graph; // 邻接表
private final int INF = Integer.MAX_VALUE / 2;
@SuppressWarnings("unchecked")
public ZeroOneBFS(int n) {
this.n = n;
this.graph = new ArrayList[n];
for (int i = 0; i < n; i++) {
graph[i] = new ArrayList<>();
}
}
/**
* 添加无向边
*/
public void addEdge(int u, int v, int weight) {
if (weight != 0 && weight != 1) {
throw new IllegalArgumentException("权重只能是 0 或 1");
}
graph[u].add(new Edge(v, weight));
graph[v].add(new Edge(u, weight));
}
/**
* 0-1 BFS 核心算法
* @param start 起点
* @return 从 start 到所有节点的最短距离数组
*/
public int[] shortestPath(int start) {
int[] dist = new int[n];
Arrays.fill(dist, INF);
dist[start] = 0;
// 双端队列:队首存储距离较小的节点
Deque<Integer> deque = new ArrayDeque<>();
deque.offerFirst(start);
while (!deque.isEmpty()) {
int u = deque.pollFirst();
for (Edge e : graph[u]) {
int v = e.to;
int w = e.weight;
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
// 关键:根据边权重决定加入队首还是队尾
if (w == 0) {
deque.offerFirst(v); // 0 权边,距离不变,放队首
} else {
deque.offerLast(v); // 1 权边,距离+1,放队尾
}
}
}
}
return dist;
}
/**
* 打印最短路径(用于调试)
*/
public void printPath(int start) {
int[] dist = shortestPath(start);
System.out.println("从节点 " + start + " 出发的最短距离:");
for (int i = 0; i < n; i++) {
System.out.println(" 到节点 " + i + ": " + (dist[i] == INF ? "不可达" : dist[i]));
}
}
/**
* 迷宫逃生场景:0 表示可通过(权重 0),1 表示需要破墙(权重 1)
* 求从左上角到右下角的最少破墙次数
*/
public static int minWallsToBreak(int[][] maze) {
int rows = maze.length;
int cols = maze[0].length;
int n = rows * cols;
// 将二维坐标映射为一维节点:id = r * cols + c
ZeroOneBFS solver = new ZeroOneBFS(n);
int[][] dirs = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (int r = 0; r < rows; r++) {
for (int c = 0; c < cols; c++) {
int u = r * cols + c;
for (int[] d : dirs) {
int nr = r + d[0], nc = c + d[1];
if (nr >= 0 && nr < rows && nc >= 0 && nc < cols) {
int v = nr * cols + nc;
// 进入目标格子是否需要破墙
int weight = maze[nr][nc];
// 为避免重复添加,只添加右和下方向的边
if (r < nr || c < nc) {
solver.addEdge(u, v, weight);
}
}
}
}
}
int[] dist = solver.shortestPath(0);
return dist[n - 1];
}
public static void main(String[] args) {
// 测试一般 0-1 权重图
ZeroOneBFS graph = new ZeroOneBFS(6);
graph.addEdge(0, 1, 0);
graph.addEdge(0, 2, 1);
graph.addEdge(1, 2, 0);
graph.addEdge(1, 3, 1);
graph.addEdge(2, 4, 0);
graph.addEdge(3, 4, 0);
graph.addEdge(3, 5, 1);
graph.addEdge(4, 5, 0);
graph.printPath(0);
// 预期:0->0, 1->0, 2->0, 3->1, 4->0, 5->1
// 测试迷宫场景
int[][] maze = {
{0, 1, 0, 0},
{0, 1, 1, 0},
{0, 0, 1, 0},
{1, 0, 0, 0}
};
System.out.println("\n迷宫最少破墙次数: " + minWallsToBreak(maze)); // 1
}
}
复杂度分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 时间复杂度 | O(V + E) | 每个节点入队出队一次,每条边遍历一次 |
| 空间复杂度 | O(V) | 距离数组与双端队列 |
对比 Dijkstra 的 O((V+E) log V),0-1 BFS 在 0-1 权重场景下完全消除了对数因子,是线性最优算法。
扩展:推广到 0-K 权重
当边权范围为 [0, K] 时,可使用单调队列优化:维护 K+1 个桶(Dial 算法),将节点按距离模 (K+1) 分桶,时间复杂度为 O(K * V + E)。当 K 为常数时仍保持线性。
应用场景
- 迷宫问题:最少破墙次数、传送门与步行混合场景
- 网络路由:链路状态为”畅通”(0) 或”拥塞”(1)
- 图像处理:二值图像中的最短路径(像素值差为 0 或 1)
- 状态转换:状态变化代价为 0 或 1 的自动机最短路径
总结
双端队列 BFS 是 0-1 权重图最短路径问题的最优解。其核心在于利用边的权重视为”距离增量”,通过队首/队尾的插入策略隐式维护了距离单调性。掌握这一技巧,将在面对特定约束的图论问题时,获得比通用算法更优的性能与更简洁的实现。