算法讲解062 必备 宽度优先遍历及其扩展

来源:算法讲解062【必备】宽度优先遍历及其扩展 · 2026-10-27
依据:视频字幕 2000 字

一句话概括

宽度优先遍历(Breadth-First Search, BFS)是从起点出发按「距离层」逐层扩展的图/网格遍历方法,天然求出无权图单源最短路,并可通过「多源入队」「0-1 BFS」「分层 BFS」等扩展解决一大类最短路、连通块、最小步数问题。

它解决什么问题

基础知识(前置概念,用大白话解释)

原理(为什么这么做是对的,尽量给直觉)

BFS 用队列保证「先发现的点先扩展」。关键性质:队列里的点,其距离值最多只有两种,且单调不降(先是 \(d\),后面是 \(d+1\))。

直觉:从源点 \(s\) 出发,距离为 0 的只有 \(s\);把 \(s\) 弹出时把距离 1 的点全部入队;这些距离 1 的点依次弹出时把距离 2 的点入队……于是点是按距离从小到大被处理的。因此第一次访问到某个点 \(v\) 时,走的就是最短路径——因为如果存在更短路径,那条路径上的前驱会更早被处理,\(v\) 会更早入队,矛盾。

形式化一点:设 \(v\) 第一次被访问时距离为 \(d(v)\),则所有距离 \(< d(v)\) 的点都已被弹出,所有距离 \(= d(v)\) 的点要么已弹出要么在队列中;\(v\) 只能由距离 \(d(v)-1\) 的点扩展而来,所以 \(d(v)\) 就是最短路长度。

多源 BFS 同理:把所有源点距离设为 0 一起入队,等价于加一个「超级源点」连向所有源点,求出的是「到最近源点的距离」。

0-1 BFS:边权为 0 时走 0 边不应增加层数,所以 0 边扩展的点塞到队头(push_front),1 边扩展的点塞到队尾(push_back),队列仍保持距离单调,等价于一个小根堆但常数更小。

步骤(编号,可照着写)

  1. 建图:读入边,往邻接表里加边(无向图加两次)。
  2. 初始化 dist 数组全为 -1(或 INF),源点 dist[s] = 0。
  3. 源点入队。
  4. 当队列非空:

1. 弹出队头 u;

2. 遍历 u 的所有邻居 v;

3. 若 dist[v] == -1(未访问),则 dist[v] = dist[u] + 1,v 入队。

  1. 队列空时结束,dist 即答案;dist[t] == -1 表示不可达。

多源版本:第 2、3 步把所有源点 dist = 0 并全部入队。

0-1 BFS 版本:第 4.1 步用 deque 弹出队头;第 4.3 步若边权为 0 则 push_front,为 1 则 push_back;允许「松弛更优」而不是只判未访问(见代码)。

样例(一个具体小例子,带每步变化)

无向图,5 个点,边:1-2, 1-3, 2-4, 3-4, 4-5,源点 1(下标从 1)。

初始:dist = [0, -1, -1, -1, -1],队列 [1]。

| 步骤 | 弹出 | 邻居 | 动作 | 队列 | dist[1..5] |

|---|---|---|---|---|---|

| 1 | 1 | 2,3 | 都未访问,dist=1,入队 | [2,3] | 0,1,1,-1,-1 |

| 2 | 2 | 1,4 | 1 已访问;4 未访问,dist=2,入队 | [3,4] | 0,1,1,2,-1 |

| 3 | 3 | 1,4 | 都已访问,跳过 | [4] | 0,1,1,2,-1 |

| 4 | 4 | 2,3,5 | 2,3 已访问;5 未访问,dist=3,入队 | [5] | 0,1,1,2,3 |

| 5 | 5 | 4 | 已访问 | [] | 0,1,1,2,3 |

结果:到 5 的最短路是 1→2→4→5,长度 3。

代码(Python 一份:好读、用于理解;C++ 一份:竞赛风,能直接抄进比赛,带注释,不用玄学宏)

Python(普通 BFS + 多源 BFS + 0-1 BFS 三种写法):

from collections import deque

def bfs(n, adj, s):
    # n 个点,编号 0..n-1;adj[u] 是 u 的邻居列表
    dist = [-1] * n
    dist[s] = 0
    q = deque([s])
    while q:
        u = q.popleft()
        for v in adj[u]:
            if dist[v] == -1:
                dist[v] = dist[u] + 1
                q.append(v)
    return dist

def multi_source_bfs(n, adj, sources):
    dist = [-1] * n
    q = deque()
    for s in sources:
        if dist[s] == -1:      # 去重,防止重复入队
            dist[s] = 0
            q.append(s)
    while q:
        u = q.popleft()
        for v in adj[u]:
            if dist[v] == -1:
                dist[v] = dist[u] + 1
                q.append(v)
    return dist

def zero_one_bfs(n, adj, s):
    # adj[u] 存 (v, w),w 只能是 0 或 1
    INF = float('inf')
    dist = [INF] * n
    dist[s] = 0
    dq = deque([s])
    while dq:
        u = dq.popleft()
        for v, w in adj[u]:
            if dist[u] + w < dist[v]:
                dist[v] = dist[u] + w
                if w == 0:
                    dq.appendleft(v)
                else:
                    dq.append(v)
    return dist

C++(竞赛风,普通 BFS + 多源 + 0-1 BFS):

#include <bits/stdc++.h>
using namespace std;

const int INF = 0x3f3f3f3f;

// 普通 BFS:n 个点(0..n-1),无向图,源点 s
// 返回 dist 数组,-1 表示不可达
vector<int> bfs(int n, const vector<vector<int>>& adj, int s) {
    vector<int> dist(n, -1);
    queue<int> q;
    dist[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}

// 多源 BFS:sources 里所有点距离为 0,求每个点到最近源点的距离
vector<int> multiSourceBfs(int n, const vector<vector<int>>& adj,
                           const vector<int>& sources) {
    vector<int> dist(n, -1);
    queue<int> q;
    for (int s : sources) {
        if (dist[s] == -1) {          // 去重
            dist[s] = 0;
            q.push(s);
        }
    }
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}

// 0-1 BFS:边权只有 0/1,adj[u] 存 {v, w}
vector<int> zeroOneBfs(int n, const vector<vector<pair<int,int>>>& adj, int s) {
    vector<int> dist(n, INF);
    deque<int> dq;
    dist[s] = 0;
    dq.push_back(s);
    while (!dq.empty()) {
        int u = dq.front(); dq.pop_front();
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (w == 0) dq.push_front(v);
                else        dq.push_back(v);
            }
        }
    }
    return dist;
}

// 网格 BFS 示例:n 行 m 列,'.' 可走 '#' 不可走,求 (sx,sy) 到 (tx,ty) 最少步数
int gridBfs(const vector<string>& g, int sx, int sy, int tx, int ty) {
    int n = g.size(), m = g[0].size();
    vector<vector<int>> dist(n, vector<int>(m, -1));
    static const int dx[4] = {-1, 1, 0, 0};
    static const int dy[4] = {0, 0, -1, 1};
    queue<pair<int,int>> q;
    dist[sx][sy] = 0;
    q.push({sx, sy});
    while (!q.empty()) {
        auto [x, y] = q.front(); q.pop();
        if (x == tx && y == ty) return dist[x][y];
        for (int d = 0; d < 4; ++d) {
            int nx = x + dx[d], ny = y + dy[d];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
            if (g[nx][ny] == '#' || dist[nx][ny] != -1) continue;
            dist[nx][ny] = dist[x][y] + 1;
            q.push({nx, ny});
        }
    }
    return -1;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    // 按需调用上面的函数
    return 0;
}

复杂度(时间/空间,最好/平均/最坏,顺带说常数大不大)

竞赛里什么时候用(看到什么题面特征就该想到它)

竞赛模板(C++,可直接背下来的写法;变量名清楚)

#include <bits/stdc++.h>
using namespace std;

// 通用 BFS 模板:n 点 0..n-1,无向图,源点 s
// 需要多源时把 sources 全部 dist=0 入队即可
vector<int> bfs(int n, const vector<vector<int>>& adj, int s) {
    vector<int> dist(n, -1);
    queue<int> q;
    dist[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] != -1) continue;
            dist[v] = dist[u] + 1;
            q.push(v);
        }
    }
    return dist;
}

// 网格 BFS 模板:n 行 m 列,g[x][y]=='.' 可走
// 返回 (tx,ty) 的最少步数,不可达返回 -1
int gridBfs(const vector<string>& g, int sx, int sy, int tx, int ty) {
    int n = (int)g.size(), m = (int)g[0].size();
    vector<int> dist(n * m, -1);
    auto id = [m](int x, int y) { return x * m + y; };
    static const int dx[4] = {-1, 1, 0, 0};
    static const int dy[4] = {0, 0, -1, 1};
    queue<int> q;
    dist[id(sx, sy)] = 0;
    q.push(id(sx, sy));
    while (!q.empty()) {
        int cur = q.front(); q.pop();
        int x = cur / m, y = cur % m;
        if (x == tx && y == ty) return dist[cur];
        for (int d = 0; d < 4; ++d) {
            int nx = x + dx[d], ny = y + dy[d];
            if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
            if (g[nx][ny] == '#' || dist[id(nx, ny)] != -1) continue;
            dist[id(nx, ny)] = dist[cur] + 1;
            q.push(id(nx, ny));
        }
    }
    return -1;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    return 0;
}

边界与易错点(WA 的常见原因,越具体越好)

同类题 / 变形(说清楚怎么改)

常见坑

拓展(变体、实际用途、面试常问)

自测 3 题(题目 + 折叠答案)

题 1:\(n\) 个点 \(m\) 条边的无向无权图,求 1 号点到 \(n\) 号点的最短路径长度,不可达输出 -1。\(n, m \le 2\times10^5\)。

答案

标准 BFS:dist[1]=0 入队,逐层扩展,最后输出 dist[n](-1 表示不可达)。时间 \(O(n+m)\)。

vector<int> dist(n + 1, -1);
queue<int> q;
dist[1] = 0; q.push(1);
while (!q.empty()) {
    int u = q.front(); q.pop();
    for (int v : adj[u]) {
        if (dist[v] != -1) continue;
        dist[v] = dist[u] + 1;
        q.push(v);
    }
}
cout << dist[n] << '\n';

</details>

题 2:\(n \times m\) 网格,每个格子是 0 或 1,求每个 1 到最近的 0 的距离(曼哈顿距离,四方向)。\(n,m \le 1000\)。

答案

多源 BFS:把所有 0 的格子 dist = 0 入队,BFS 扩散,dist 即为答案。时间 \(O(nm)\)。

vector<vector<int>> dist(n, vector<int>(m, -1));
queue<pair<int,int>> q;
for (int i = 0; i < n; ++i)
    for (int j = 0; j < m; ++j)
        if (g[i][j] == 0) { dist[i][j] = 0; q.push({i, j}); }
// 之后标准 BFS 四方向扩展

</details>

题 3:有 \(n\) 个城市,\(m\) 条航线,每条航线要么是「普通航班」代价 1,要么是「免费航班」代价 0(但免费航班总共只能用 \(k\) 次,\(k \le 10\))。求 1 到 \(n\) 的最小代价。\(n,m \le 10^5\)。

答案

分层图 + 0-1 BFS:状态 (u, used) 表示在城市 u、已用 used 次免费航班,共 \(n(k+1)\) 个状态。普通边 (u,used) -> (v,used) 权 1;免费边 (u,used) -> (v,used+1) 权 0(used < k 时)。在分层图上跑 0-1 BFS,答案取 min(dist[n][used])。时间 \(O((n+m)k)\)。

// 状态 id = u * (k + 1) + used
vector<int> dist(n * (k + 1), INF);
deque<int> dq;
dist[0 * (k + 1) + 0] = 0; dq.push_back(0);
while (!dq.empty()) {
    int cur = dq.front(); dq.pop_front();
    int u = cur / (k + 1), used = cur % (k + 1);
    for (auto [v, w] : adj[u]) {          // w=1 普通, w=0 免费
        int nu = used + (w == 0 ? 1 : 0);
        if (nu > k) continue;
        int nxt = v * (k + 1) + nu;
        if (dist[cur] + w < dist[nxt]) {
            dist[nxt] = dist[cur] + w;
            if (w == 0) dq.push_front(nxt);
            else        dq.push_back(nxt);
        }
    }
}

</details>