{ }codelper

배우기 › C

알고리즘 · 32

그래프와 깊이 우선 탐색

누가 누구와 이어져 있는지 적어 두고, 닿는 곳을 빠짐없이 찾아가기.

그래프는 점(사람, 마을, 방)과 그 사이를 잇는 선(친구 사이, 길, 문)으로 된 자료예요.
아래 예제는 친구 쌍을 읽어 사람마다 친구 목록을 만들고, 1번에서 친구를 따라 닿는 사람을 찾아간 차례와, 서로 닿는 사람끼리 묶은 무리의 수를 찍어요.

#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;}
stdin
7 5
1 2
1 3
2 4
3 4
5 6
stdout
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이에요.

언어별로 쓰는 법

Pythonadj = [[] for _ in range(n + 1)], adj[a].append(b).
스택은 리스트의 append와 pop()이에요
JavaScriptArray.from({ length: n + 1 }, () => []), adj[a].push(b).
스택은 배열의 push와 pop()이에요
TypeScriptJavaScript와 같아요.
타입은 number[][].
stack.pop()의 타입은 number | undefined라서, 비어 있지 않은 것을 확인한 자리에서는 stack.pop()!로 써요
JavaArrayList<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이 몇천을 넘을 수 있으면 처음부터 스택으로 쓰세요.

연습 문제