알고리즘 · 32
그래프와 깊이 우선 탐색
누가 누구와 이어져 있는지 적어 두고, 닿는 곳을 빠짐없이 찾아가기.
그래프는 점(사람, 마을, 방)과 그 사이를 잇는 선(친구 사이, 길, 문)으로 된 자료예요.
아래 예제는 친구 쌍을 읽어 사람마다 친구 목록을 만들고, 1번에서 친구를 따라 닿는 사람을 찾아간 차례와, 서로 닿는 사람끼리 묶은 무리의 수를 찍어요.
n, m = map(int, input().split())adj = [[] for _ in range(n + 1)] # adj[v] = v번의 친구들for _ in range(m): a, b = map(int, input().split()) adj[a].append(b) # 서로 친구라 양쪽에 넣어요 adj[b].append(a) seen = [False] * (n + 1)order = [] def dfs(v): # 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요 seen[v] = True order.append(v) for w in adj[v]: if not seen[w]: dfs(w) dfs(1)print(*order) # 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나seen = [False] * (n + 1)groups = 0for s in range(1, n + 1): if seen[s]: continue groups += 1 seen[s] = True stack = [s] while stack: v = stack.pop() for w in adj[v]: if not seen[w]: seen[w] = True stack.append(w)print(groups)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[v] = v번의 친구들for (const [a, b] of pairs.slice(0, m)) { adj[a].push(b); // 서로 친구라 양쪽에 넣어요 adj[b].push(a);} let seen = new Array(n + 1).fill(false);const order = []; // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요function dfs(v) { seen[v] = true; order.push(v); for (const w of adj[v]) { if (!seen[w]) dfs(w); }} dfs(1);console.log(order.join(" ")); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나seen = new Array(n + 1).fill(false);let groups = 0;for (let s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = true; const stack = [s]; while (stack.length > 0) { const v = stack.pop(); for (const w of adj[v]) { if (!seen[w]) { seen[w] = true; stack.push(w); } } }}console.log(groups);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[v] = v번의 친구들for (const [a, b] of pairs.slice(0, m)) { adj[a].push(b); // 서로 친구라 양쪽에 넣어요 adj[b].push(a);} let seen: boolean[] = new Array<boolean>(n + 1).fill(false);const order: number[] = []; // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요function dfs(v: number): void { seen[v] = true; order.push(v); for (const w of adj[v]) { if (!seen[w]) dfs(w); }} dfs(1);console.log(order.join(" ")); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나seen = new Array<boolean>(n + 1).fill(false);let groups: number = 0;for (let s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = true; const stack: number[] = [s]; while (stack.length > 0) { const v = stack.pop()!; for (const w of adj[v]) { if (!seen[w]) { seen[w] = true; stack.push(w); } } }}console.log(groups);import java.util.ArrayDeque;import java.util.ArrayList;import java.util.Scanner; public class Main { static ArrayList<ArrayList<Integer>> adj = new ArrayList<>(); // adj.get(v) = v번의 친구들 static boolean[] seen; static StringBuilder order = new StringBuilder(); // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요 static void dfs(int v) { seen[v] = true; order.append(order.length() > 0 ? " " : "").append(v); for (int w : adj.get(v)) { if (!seen[w]) dfs(w); } } public static void main(String[] args) { Scanner sc = new Scanner(System.in); int n = sc.nextInt(), m = sc.nextInt(); for (int v = 0; v <= n; v++) adj.add(new ArrayList<>()); // 빈 목록을 사람마다 하나씩 새로 for (int i = 0; i < m; i++) { int a = sc.nextInt(), b = sc.nextInt(); adj.get(a).add(b); // 서로 친구라 양쪽에 넣어요 adj.get(b).add(a); } seen = new boolean[n + 1]; dfs(1); System.out.println(order); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나 seen = new boolean[n + 1]; int groups = 0; for (int s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = true; ArrayDeque<Integer> stack = new ArrayDeque<>(); stack.push(s); while (!stack.isEmpty()) { int v = stack.pop(); for (int w : adj.get(v)) { if (!seen[w]) { seen[w] = true; stack.push(w); } } } } System.out.println(groups); }}using System;using System.Collections.Generic;using System.Linq; class Program{ static List<int>[] adj; // adj[v] = v번의 친구들 static bool[] seen; static List<int> order = new List<int>(); // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요 static void Dfs(int v) { seen[v] = true; order.Add(v); foreach (int w in adj[v]) { if (!seen[w]) Dfs(w); } } static void Main() { int[] nm = Console.ReadLine().Split(' ', StringSplitOptions.RemoveEmptyEntries).Select(int.Parse).ToArray(); int n = nm[0], m = nm[1]; adj = new List<int>[n + 1]; for (int v = 0; v <= n; v++) adj[v] = new List<int>(); // 칸마다 빈 목록을 하나씩 새로 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); // 서로 친구라 양쪽에 넣어요 adj[b].Add(a); } seen = new bool[n + 1]; Dfs(1); Console.WriteLine(string.Join(" ", order)); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나 seen = new bool[n + 1]; int groups = 0; for (int s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = true; var stack = new Stack<int>(); stack.Push(s); while (stack.Count > 0) { int v = stack.Pop(); foreach (int w in adj[v]) { if (!seen[w]) { seen[w] = true; stack.Push(w); } } } } Console.WriteLine(groups); }}#include <iostream>#include <vector> std::vector<std::vector<int>> adj; // adj[v] = v번의 친구들std::vector<bool> seen;std::vector<int> order; // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요void dfs(int v) { seen[v] = true; order.push_back(v); for (int w : adj[v]) { if (!seen[w]) dfs(w); }} int main() { int n, m; std::cin >> n >> m; adj.resize(n + 1); for (int i = 0; i < m; i++) { int a, b; std::cin >> a >> b; adj[a].push_back(b); // 서로 친구라 양쪽에 넣어요 adj[b].push_back(a); } seen.assign(n + 1, false); dfs(1); for (size_t i = 0; i < order.size(); i++) std::cout << (i > 0 ? " " : "") << order[i]; std::cout << '\n'; // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나 seen.assign(n + 1, false); int groups = 0; for (int s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = true; std::vector<int> stack = {s}; while (!stack.empty()) { int v = stack.back(); stack.pop_back(); for (int w : adj[v]) { if (!seen[w]) { seen[w] = true; stack.push_back(w); } } } } std::cout << groups << '\n';}#include <stdio.h>#include <string.h> #define MAX 101 // 사람은 100명까지라고 정해 둬요 int adj[MAX][MAX], deg[MAX]; // adj[v][0..deg[v]-1] = v번의 친구들int seen[MAX], order[MAX], count = 0; // 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요void dfs(int v) { seen[v] = 1; order[count++] = v; for (int i = 0; i < deg[v]; i++) { int w = adj[v][i]; if (!seen[w]) dfs(w); }} 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; // 서로 친구라 양쪽에 넣어요 adj[b][deg[b]++] = a; } dfs(1); for (int i = 0; i < count; i++) printf(i > 0 ? " %d" : "%d", order[i]); printf("\n"); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나 memset(seen, 0, sizeof seen); int groups = 0, stack[MAX]; for (int s = 1; s <= n; s++) { if (seen[s]) continue; groups++; seen[s] = 1; int top = 0; stack[top++] = s; while (top > 0) { int v = stack[--top]; for (int i = 0; i < deg[v]; i++) { int w = adj[v][i]; if (!seen[w]) { seen[w] = 1; stack[top++] = w; } } } } printf("%d\n", groups); return 0;}// 재귀: v에 표시하고, 안 가 본 친구가 나오면 바로 그쪽으로 들어가요dfs(v){ seen[v] = 참; order.넣기(v); 각각 (w 에서 adj[v]) { 만약 (!seen[w]) { dfs(w); } }} nm = 콘솔.수들읽기();n = nm[0];m = nm[1];adj = 새목록(n + 1, []); // adj[v] = v번의 친구들반복 (i = 0; i < m; i++){ ab = 콘솔.수들읽기(); a = ab[0]; b = ab[1]; adj[a].넣기(b); // 서로 친구라 양쪽에 넣어요 adj[b].넣기(a);} seen = 새목록(n + 1, 거짓);order = [];dfs(1);콘솔.쓰기(order.잇기(" ")); // 같은 탐색을 스택으로: 아직 안 본 사람에서 시작할 때마다 무리가 하나seen = 새목록(n + 1, 거짓);groups = 0;반복 (s = 1; s <= n; s++){ 만약 (seen[s]) { 건너뛰기; } groups++; seen[s] = 참; stack = [s]; 동안 (stack.개수 > 0) { v = stack[stack.개수 - 1]; stack.자리빼기(stack.개수 - 1); 각각 (w 에서 adj[v]) { 만약 (!seen[w]) { seen[w] = 참; stack.넣기(w); } } }}콘솔.쓰기(groups);7 5 1 2 1 3 2 4 3 4 5 6
1 2 4 3 3
핵심
- 입력은 보통 "a b" 쌍으로 와요.
이것을 사람마다 이웃을 모은 목록adj[v]로 바꿔 둬요(인접 목록).
그러면 "v의 이웃이 누구인지"를 쌍을 다시 훑지 않고 바로 알 수 있어요.
서로 이어진 사이면adj[a]에 b를,adj[b]에 a를 둘 다 넣어요. - 번호가 1부터면 칸을
n + 1개 만들고 0번 칸은 비워 둬요.
그러면 번호를 그대로 자리로 쓸 수 있어요. - 깊이 우선 탐색(DFS)은 지금 있는 곳에 가 봤다는 표시(
seen)를 하고, 안 가 본 이웃이 나오면 바로 그쪽으로 들어가요.
더 갈 데가 없으면 왔던 곳으로 돌아와 남은 이웃을 봐요.
예제의dfs(v)는 번호 v만 받고,adj·seen·order는 함수 밖에 둔 것을 함께 써요.
예제에서 1번의 친구는 2번과 3번인데, 2번으로 들어간 김에 4번까지 간 뒤에야 3번을 봐서 차례가1 2 4 3이에요. - 표시가 있어서 한 사람에게는 한 번만 들어가고, 친구 사이 하나는 양쪽에서 한 번씩만 봐요.
그래서 일의 양이 사람 수에 쌍의 수를 더한 만큼이에요. - 재귀 대신 스택을 직접 써도 돼요.
시작점을 넣고, 하나 꺼내서 안 가 본 이웃을 표시하며 넣는 일을 스택이 빌 때까지 되풀이해요.
찾아가는 차례는 달라질 수 있지만(예제를 스택으로 돌면1 3 4 2) 닿는 사람은 같아요. - 아직 표시가 없는 사람에서 탐색을 새로 시작할 때마다 무리가 하나예요.
예제는 {1, 2, 3, 4}, {5, 6}, {7}이라 3이에요.
언어별로 쓰는 법
| Python | adj = [[] for _ in range(n + 1)], adj[a].append(b).스택은 리스트의 append와 pop()이에요 |
|---|---|
| JavaScript | Array.from({ length: n + 1 }, () => []), adj[a].push(b).스택은 배열의 push와 pop()이에요 |
| TypeScript | JavaScript와 같아요. 타입은 number[][].stack.pop()의 타입은 number | undefined라서, 비어 있지 않은 것을 확인한 자리에서는 stack.pop()!로 써요 |
| Java | ArrayList<ArrayList<Integer>>에 빈 ArrayList를 n + 1개 add해 두고 adj.get(a).add(b).스택은 ArrayDeque의 push와 pop이에요 |
| C# | new List<int>[n + 1]은 칸이 모두 null이라, 칸마다 new List<int>()를 넣어 줘요.그다음 adj[a].Add(b).스택은 Stack<int>의 Push와 Pop이에요 |
| C++ | std::vector<std::vector<int>> adj(n + 1) — 빈 벡터 n + 1개가 따로따로 생겨요.adj[a].push_back(b).스택은 vector의 push_back, back(), pop_back()으로 써요 |
| C | 늘어나는 목록이 없어요.int adj[MAX][MAX]와 이웃 수 deg[v]를 두고 adj[a][deg[a]++] = b로 넣어요.스택도 배열과 top 번호로 만들어요 |
| Craykon | 새목록(n + 1, [])은 칸마다 따로인 빈 목록을 만들어요.adj[a].넣기(b), 이웃은 각각 (w 에서 adj[v]).스택은 목록의 끝을 써요: stack.넣기(w)로 넣고, stack[stack.개수 - 1]을 읽은 뒤 stack.자리빼기(stack.개수 - 1)로 빼요 |
자주 하는 실수
가 봤다는 표시를 빼먹었어요
void dfs(int v) {
order.push_back(v);
for (int w : adj[v]) dfs(w);
}→ 1번과 2번이 친구이기만 해도 1 → 2 → 1 → 2 …로 끝없이 들어가다가 스택이 넘쳐 SIGSEGV로 죽어요. 같은 실수를 하면 Python은 RecursionError, JavaScript는 RangeError, Java는 StackOverflowError로 멈추고, C#은 Stack overflow.를 찍고 죽고, 크레이콘은 "함수를 너무 깊이 불러서 멈췄어요"로 멈춰요
들어가자마자 seen[v] = true로 표시하고, 표시가 없는 이웃으로만 들어가요.
이웃 목록을 [[]] * n으로 만들었어요Python · JavaScript · TypeScript
adj = [[]] * 3
adj[1].append(2)
print(adj)→ [[2], [2], [2]] — 빈 목록 하나를 세 칸이 함께 가리켜서, 1번에 넣은 친구가 모든 사람에게 들어가요. JavaScript new Array(3).fill([])도 똑같아요
칸마다 새로 만들어요: [[] for _ in range(n + 1)], Array.from({ length: n + 1 }, () => []).
배열만 만들고 칸에 목록을 넣지 않았어요Java · C#
List<int>[] adj = new List<int>[3];
adj[1].Add(2);→ NullReferenceException — 배열을 만들면 칸은 모두 null이에요. Java에서 new ArrayList[3]으로 만들어도 NullPointerException으로 멈춰요
쓰기 전에 반복문으로 칸마다 adj[v] = new List<int>();(Java는 new ArrayList<>())를 넣어요.
재귀 DFS는 짧지만, 사람들이 한 줄로 길게 이어지면 그만큼 깊이 들어가요.
채점기에서 예제의 Python 코드는 900명이 한 줄로 이어진 입력은 풀지만 1,100명에서는
RecursionError로 멈추고, 크레이콘 코드는 1,100명은 풀지만 5,000명에서 멈춰요.n이 몇천을 넘을 수 있으면 처음부터 스택으로 쓰세요.