{ }codelper

배우기 › Craykon

알고리즘 · 32

그래프와 깊이 우선 탐색

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

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

// 재귀: 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);
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이 몇천을 넘을 수 있으면 처음부터 스택으로 쓰세요.

연습 문제