学ぶ › TypeScript
アルゴリズム · 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 が終わったあと、空のキューで最初から別にもう一度回す処理です(出力の 2 行目)。
同じ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 に上書きします
キューに入れるその場所で印を付けます。
そうすれば、一つの所はキューに一度しか入りません。
先頭ではなく末尾から取り出した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 が最少の歩数を出すのは、どの矢印も同じ 1 歩のときです。
そしてトポロジカルソートでは、組「a b」が「a が先」という意味なら、
矢印が入ってくる側の数を数えるのです。
BFS が最少の歩数を出すのは、どの矢印も同じ 1 歩のときです。
そしてトポロジカルソートでは、組「a b」が「a が先」という意味なら、
adj[a] に b を入れて need[b] を増やします。矢印が入ってくる側の数を数えるのです。