学习 › JavaScript
算法 · 33
广度优先搜索与拓扑排序
用队列从近处向外扩展求最少步数,并把任务排成先决条件在前的顺序。
广度优先搜索(BFS)从起点出发,由近到远一层层扩展;拓扑排序则排出一个顺序,让所有“先做完这个才能做那个”的条件都得到满足。
下面的例子读入表示“做完 a 才能开始 b”的一对对数字,输出从 1 号任务最少沿箭头走几次能到各个任务(到不了输出 -1),以及一个满足条件的做任务顺序。
from collections import deque n, m = map(int, input().split())adj = [[] for _ in range(n + 1)] # adj[a] = a를 끝내야 시작할 수 있는 일들need = [0] * (n + 1) # need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수for _ in range(m): a, b = map(int, input().split()) adj[a].append(b) need[b] += 1 # 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지dist = [-1] * (n + 1) # -1이면 아직 안 간 곳dist[1] = 0queue = deque([1])while queue: v = queue.popleft() for w in adj[v]: if dist[w] == -1: dist[w] = dist[v] + 1 # 큐에 넣을 때 바로 적어요 queue.append(w)print(*dist[1:]) # 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요order = []queue = deque(v for v in range(1, n + 1) if need[v] == 0)while queue: v = queue.popleft() order.append(v) for w in adj[v]: need[w] -= 1 # v가 끝났으니 w가 기다리는 일이 하나 줄어요 if need[w] == 0: queue.append(w)print(*order)const [[n, m], ...pairs] = require("fs").readFileSync(0, "utf8").trim().split("\n").map((l) => l.trim().split(/\s+/).map(Number));const adj = Array.from({ length: n + 1 }, () => []); // adj[a] = a를 끝내야 시작할 수 있는 일들const need = new Array(n + 1).fill(0); // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수for (const [a, b] of pairs.slice(0, m)) { adj[a].push(b); need[b]++;} // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지const dist = new Array(n + 1).fill(-1); // -1이면 아직 안 간 곳dist[1] = 0;let queue = [1];let head = 0; // 큐의 맨 앞 자리. 꺼낼 때마다 한 칸씩 뒤로 가요while (head < queue.length) { const v = queue[head++]; for (const w of adj[v]) { if (dist[w] === -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.push(w); } }}console.log(dist.slice(1).join(" ")); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요const order = [];queue = [];head = 0;for (let v = 1; v <= n; v++) { if (need[v] === 0) queue.push(v);}while (head < queue.length) { const v = queue[head++]; order.push(v); for (const w of adj[v]) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] === 0) queue.push(w); }}console.log(order.join(" "));import { readFileSync } from "node:fs";const [[n, m], ...pairs]: number[][] = readFileSync(0, "utf8").trim().split("\n").map((l) => l.trim().split(/\s+/).map(Number));const adj: number[][] = Array.from({ length: n + 1 }, () => []); // adj[a] = a를 끝내야 시작할 수 있는 일들const need: number[] = new Array<number>(n + 1).fill(0); // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수for (const [a, b] of pairs.slice(0, m)) { adj[a].push(b); need[b]++;} // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지const dist: number[] = new Array<number>(n + 1).fill(-1); // -1이면 아직 안 간 곳dist[1] = 0;let queue: number[] = [1];let head: number = 0; // 큐의 맨 앞 자리. 꺼낼 때마다 한 칸씩 뒤로 가요while (head < queue.length) { const v = queue[head++]; for (const w of adj[v]) { if (dist[w] === -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.push(w); } }}console.log(dist.slice(1).join(" ")); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요const order: number[] = [];queue = [];head = 0;for (let v = 1; v <= n; v++) { if (need[v] === 0) queue.push(v);}while (head < queue.length) { const v = queue[head++]; order.push(v); for (const w of adj[v]) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] === 0) queue.push(w); }}console.log(order.join(" "));import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Arrays;import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(), m = sc.nextInt(); ArrayList<ArrayList<Integer>> adj = new ArrayList<>(); // adj.get(a) = a를 끝내야 시작할 수 있는 일들 for (int v = 0; v <= n; v++) adj.add(new ArrayList<>()); int[] need = new int[n + 1]; // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수 for (int i = 0; i < m; i++) { int a = sc.nextInt(), b = sc.nextInt(); adj.get(a).add(b); need[b]++; } // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지 int[] dist = new int[n + 1]; Arrays.fill(dist, -1); // -1이면 아직 안 간 곳 dist[1] = 0; ArrayDeque<Integer> queue = new ArrayDeque<>(); queue.add(1); while (!queue.isEmpty()) { int v = queue.poll(); for (int w : adj.get(v)) { if (dist[w] == -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.add(w); } } } StringBuilder sb = new StringBuilder(); for (int v = 1; v <= n; v++) sb.append(v > 1 ? " " : "").append(dist[v]); System.out.println(sb); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요 StringBuilder order = new StringBuilder(); for (int v = 1; v <= n; v++) { if (need[v] == 0) queue.add(v); } while (!queue.isEmpty()) { int v = queue.poll(); order.append(order.length() > 0 ? " " : "").append(v); for (int w : adj.get(v)) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] == 0) queue.add(w); } } System.out.println(order); }}using System;using System.Collections.Generic;using System.Linq; class Program{ static void Main() { int[] nm = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); int n = nm[0], m = nm[1]; var adj = new List<int>[n + 1]; // adj[a] = a를 끝내야 시작할 수 있는 일들 for (int v = 0; v <= n; v++) adj[v] = new List<int>(); int[] need = new int[n + 1]; // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수 for (int i = 0; i < m; i++) { int[] ab = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); int a = ab[0], b = ab[1]; adj[a].Add(b); need[b]++; } // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지 int[] dist = Enumerable.Repeat(-1, n + 1).ToArray(); // -1이면 아직 안 간 곳 dist[1] = 0; var queue = new Queue<int>(); queue.Enqueue(1); while (queue.Count > 0) { int v = queue.Dequeue(); foreach (int w in adj[v]) { if (dist[w] == -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.Enqueue(w); } } } Console.WriteLine(string.Join(" ", dist.Skip(1))); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요 var order = new List<int>(); for (int v = 1; v <= n; v++) { if (need[v] == 0) queue.Enqueue(v); } while (queue.Count > 0) { int v = queue.Dequeue(); order.Add(v); foreach (int w in adj[v]) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] == 0) queue.Enqueue(w); } } Console.WriteLine(string.Join(" ", order)); }}#include <iostream>#include <queue>#include <vector> int main() { int n, m; std::cin >> n >> m; std::vector<std::vector<int>> adj(n + 1); // adj[a] = a를 끝내야 시작할 수 있는 일들 std::vector<int> need(n + 1, 0); // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수 for (int i = 0; i < m; i++) { int a, b; std::cin >> a >> b; adj[a].push_back(b); need[b]++; } // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지 std::vector<int> dist(n + 1, -1); // -1이면 아직 안 간 곳 dist[1] = 0; std::queue<int> queue; queue.push(1); while (!queue.empty()) { int v = queue.front(); queue.pop(); for (int w : adj[v]) { if (dist[w] == -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.push(w); } } } for (int v = 1; v <= n; v++) std::cout << (v > 1 ? " " : "") << dist[v]; std::cout << '\n'; // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요 std::vector<int> order; for (int v = 1; v <= n; v++) { if (need[v] == 0) queue.push(v); } while (!queue.empty()) { int v = queue.front(); queue.pop(); order.push_back(v); for (int w : adj[v]) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] == 0) queue.push(w); } } for (size_t i = 0; i < order.size(); i++) std::cout << (i > 0 ? " " : "") << order[i]; std::cout << '\n';}#include <stdio.h> #define MAX 101 // 일은 100개까지라고 정해 둬요 int adj[MAX][MAX], deg[MAX]; // adj[a][0..deg[a]-1] = a를 끝내야 시작할 수 있는 일들int need[MAX]; // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수int dist[MAX], queue[MAX], order[MAX]; int main(void) { int n, m; scanf("%d %d", &n, &m); for (int i = 0; i < m; i++) { int a, b; scanf("%d %d", &a, &b); adj[a][deg[a]++] = b; need[b]++; } // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지 for (int v = 1; v <= n; v++) dist[v] = -1; // -1이면 아직 안 간 곳 dist[1] = 0; int head = 0, tail = 0; // 큐: head에서 꺼내고 tail에 넣어요 queue[tail++] = 1; while (head < tail) { int v = queue[head++]; for (int i = 0; i < deg[v]; i++) { int w = adj[v][i]; if (dist[w] == -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue[tail++] = w; } } } for (int v = 1; v <= n; v++) printf(v > 1 ? " %d" : "%d", dist[v]); printf("\n"); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요 int count = 0; head = tail = 0; for (int v = 1; v <= n; v++) { if (need[v] == 0) queue[tail++] = v; } while (head < tail) { int v = queue[head++]; order[count++] = v; for (int i = 0; i < deg[v]; i++) { int w = adj[v][i]; need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 if (need[w] == 0) queue[tail++] = w; } } for (int i = 0; i < count; i++) printf(i > 0 ? " %d" : "%d", order[i]); printf("\n"); return 0;}nm = 控制台.读数字();n = nm[0];m = nm[1];adj = 新列表(n + 1, []); // adj[a] = a를 끝내야 시작할 수 있는 일들need = 新列表(n + 1, 0); // need[b] = b보다 먼저 해야 하는데 아직 안 끝난 일의 수循环 (i = 0; i < m; i++){ ab = 控制台.读数字(); a = ab[0]; b = ab[1]; adj[a].添加(b); need[b]++;} // 너비 우선 탐색: 1번 일에서 화살표를 가장 적게 몇 번 따라가면 닿는지dist = 新列表(n + 1, -1); // -1이면 아직 안 간 곳dist[1] = 0;queue = [1];head = 0; // 큐의 맨 앞 자리. 꺼낼 때마다 한 칸씩 뒤로 가요当 (head < queue.数量){ v = queue[head]; head++; 遍历 (w 在 adj[v]) { 如果 (dist[w] == -1) { dist[w] = dist[v] + 1; // 큐에 넣을 때 바로 적어요 queue.添加(w); } }}控制台.输出(dist.子列表(1, n).连接(" ")); // 위상 정렬: 먼저 할 일이 남지 않은 일부터 꺼내요order = [];queue = [];head = 0;循环 (v = 1; v <= n; v++){ 如果 (need[v] == 0) { queue.添加(v); }}当 (head < queue.数量){ v = queue[head]; head++; order.添加(v); 遍历 (w 在 adj[v]) { need[w]--; // v가 끝났으니 w가 기다리는 일이 하나 줄어요 如果 (need[w] == 0) { queue.添加(w); } }}控制台.输出(order.连接(" "));6 6 1 2 2 3 3 4 1 4 4 5 6 3
0 1 2 1 2 -1 1 6 2 3 4 5
要点
- BFS 把“已经找到、但还没看过邻居的地方”放在队列里。
先把起点以 0 步放进去,然后重复到队列为空:取出队首,对每个还没到过的邻居写下dist[w] = dist[v] + 1,并把它放到队尾。 - 队列先进先出,所以出来的顺序是 0 步的地方、1 步的地方、2 步的地方。
因此一个地方第一次被找到时的步数就是最少步数。
例子里 4 号也能经 1 → 2 → 3 → 4 到达,但先由 1 → 4 找到,所以是 1。 - 标记要在放入队列时做。
例子把dist不是 -1 的地方当作已经找到,所以标记和步数是一次写下的。 - 在网格上也一样。
一个格子就是一个点,上、下、左、右的格子是邻居。
不用建邻居列表,而是用二维列表那一课的方向列表(di、dj)求出相邻的格子,再去掉网格外面的和墙。 - 拓扑排序是 BFS 结束之后,用一个空队列从头另做的一遍(输出的第二行)。
它用同一个adj,但不用dist。
它为每个任务数一个need:还没做完的先决任务有几个。
把need为 0 的任务放进队列;取出一个,写进顺序里,再把等着它的那些任务的need各减 1。
减到 0 的任务现在可以做了,就放进队列。 - 满足条件的顺序可能不止一个(例子里
1 6 2 3 4 5和6 1 2 3 4 5都对)。
如果条件成了环(2 → 3 → 2),环里的任务need始终到不了 0,就不会出现在顺序里。
所以顺序的长度比 n 短,就说明有环。
各语言的写法
| Python | from collections import deque — 用 queue.append(w) 放入,用 queue.popleft() 取出队首。初始值像 deque([1]) 这样给 |
|---|---|
| JavaScript | 没有专门的队列。 用 push 放进数组,另设队首位置 head,用 queue[head++] 取出。queue.shift() 也能取出队首,但剩下的值都要往前挪一位,就像栈与队列那一课看到的,值一多就慢 |
| TypeScript | 和 JavaScript 一样。 类型是 number[] |
| Java | ArrayDeque<Integer> — 用 queue.add(w) 放入,用 queue.poll() 取出队首。新建的 int[] 全是 0,所以用 Arrays.fill(dist, -1) 改掉 |
| C# | Queue<int> — 用 Enqueue(w) 放入,用 Dequeue() 取出队首。填满 -1 的数组是 Enumerable.Repeat(-1, n + 1).ToArray() |
| C++ | std::queue<int> — 用 push(w) 放入。用 front() 读队首,用 pop() 去掉(pop() 不返回值)。填满 -1 的 vector 是 std::vector<int> dist(n + 1, -1) |
| C | 没有队列,所以用一个数组和两个下标来做。 用 queue[tail++] = w 放入,用 queue[head++] 取出。head < tail 就表示还有剩下的 |
| Craykon | 用 queue.添加(w) 放到列表末尾,另设队首位置 head,读 queue[head] 之后 head++。当 (head < queue.数量) 就表示还有剩下的。填满 -1 的列表是 新列表(n + 1, -1) |
常见错误
从队列取出时才做标记
int v = queue.front();
queue.pop();
seen[v] = true; // 取出时才标记
for (int w : adj[v]) {
if (!seen[w]) {
dist[w] = dist[v] + 1;
queue.push(w);
}
}→ 对 1 → 2、1 → 3、2 → 3 会输出 0 1 2(正确答案是 0 1 1)。3 号还在队列里等着的时候,2 号又把 3 号放进去一次,并把步数改写成 2
就在放入队列的那个地方做标记。
这样每个地方只会进队列一次。
不是从队首而是从队尾取出Python · JavaScript · TypeScript
adj = [[], [2, 3], [5], [4], [5], []]
dist = [-1] * 6
dist[1] = 0
queue = [1]
while queue:
v = queue.pop()
for w in adj[v]:
if dist[w] == -1:
dist[w] = dist[v] + 1
queue.append(w)
print(*dist[1:])→ 0 1 1 2 3 — 正确答案是 0 1 1 2 2。pop() 取出的是最后放入的那个,于是它不是队列而是栈,会先往远处钻。JavaScript 数组的 pop() 也一样
用 deque 的 popleft() 取出队首。
JavaScript 另设下标 head,用 queue[head++] 取出。
没有检查顺序的长度
// 1 → 2, 2 → 3, 3 → 2
while (!queue.isEmpty()) {
int v = queue.poll();
order.add(v);
for (int w : adj.get(v)) {
need[w]--;
if (need[w] == 0) queue.add(w);
}
}
System.out.println(order);→ [1] — 没有报错就结束了,但少了 2 号和 3 号。它们互相等待,need 到不了 0
结束后检查顺序里是否有 n 个任务:if (order.size() < n)。
不够就说明有环,不可能全部做完。
各语言的差异
只有每个箭头都算同样的一步时,BFS 得到的才是最少步数。
另外在拓扑排序里,如果一对“a b”表示“a 在先”,就把 b 放进
只有每个箭头都算同样的一步时,BFS 得到的才是最少步数。
另外在拓扑排序里,如果一对“a b”表示“a 在先”,就把 b 放进
adj[a],并把 need[b] 加 1:数的是指向这个任务的箭头有几个。