알고리즘 · 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이 아니면 이미 찾은 곳으로 쳐서, 표시와 걸음 수를 한 번에 적어요. - 격자에서도 똑같아요.
칸 하나가 점이고 위·아래·왼쪽·오른쪽 칸이 이웃이에요.
이웃 목록을 만드는 대신 2차원 목록 단원의 방향 목록(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로 채운 벡터는 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로 덮어써요
큐에 넣는 바로 그 자리에서 표시해요.
그러면 한 곳은 큐에 한 번만 들어가요.
차례의 길이를 확인하지 않았어요
// 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).
모자라면 고리가 있어서 모두 할 수는 없는 거예요.
맨 앞이 아니라 맨 뒤에서 꺼냈어요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++]로 꺼내요.
언어마다 다른 점
BFS가 가장 적은 걸음 수를 주는 것은 모든 화살표가 똑같이 한 걸음일 때예요.
그리고 위상 정렬에서 쌍 "a b"가 "a 먼저"라는 뜻이면,
화살표가 들어오는 쪽의 수를 세는 거예요.
BFS가 가장 적은 걸음 수를 주는 것은 모든 화살표가 똑같이 한 걸음일 때예요.
그리고 위상 정렬에서 쌍 "a b"가 "a 먼저"라는 뜻이면,
adj[a]에 b를 넣고 need[b]를 올려요.화살표가 들어오는 쪽의 수를 세는 거예요.