edubun English

그래프 탐색: BFS와 DFS

1736년 레온하르트 오일러는 쾨니히스베르크의 다리 일곱 개를 한 번씩만 건너는 산책길이 없음을 보였고, 그래프 이론은 이 답에서 시작했습니다.

점과 선

그래프는 정점(또는 노드)이라는 점들과, 그 점들을 잇는 간선이라는 선으로 이루어집니다. 생각은 간단한데 들어맞는 곳이 아주 많습니다. 도로로 이어진 도시, 링크로 이어진 웹 페이지, 친구 관계로 이어진 사람들, 벽이 없는 곳으로 이어진 미로의 칸들이 모두 그래프입니다. 문제를 그래프로 그려 놓으면 「여기서 저기로 갈 수 있을까?」 「가장 짧은 길은?」 같은 질문에 같은 몇 가지 알고리즘으로 답할 수 있습니다.

배경쾨니히스베르크(지금의 칼리닌그라드)에는 강 가운데 섬 두 곳과 양쪽 강가를 잇는 다리가 일곱 개 있었습니다. 사람들은 모든 다리를 한 번씩만 건너는 산책이 가능한지 궁금해했습니다. 1736년 레온하르트 오일러는 불가능하다고 답했습니다. 그가 한 결정적인 일은 지도를 버린 것입니다. 중요한 것은 땅덩어리와 그 사이의 다리뿐이었습니다. 땅덩어리마다 점 하나, 다리마다 선 하나로 바꿨습니다. 그런 다음 이런 산책이 가능하려면 출발점과 도착점을 빼고 모든 점에 이어진 선이 짝수 개여야 함을 보였는데, 쾨니히스베르크의 땅 네 곳은 모두 홀수 개였습니다.
A B C D E F
이 단원의 예시 그래프. A가 출발점, F가 목표입니다. 간선: A‑B, A‑C, B‑D, C‑D, C‑E, D‑F, E‑F.

코드로 그래프 담기

그래프를 담는 흔한 방법은 인접 리스트입니다. 정점마다 이웃 목록을 적어 둡니다. MIT 6.006 강의 노트도 바로 이 방식으로, Adj[u]에 u와 이어진 정점들을 적은 배열이나 사전 Adj를 씁니다. 위 그래프는 이렇게 됩니다.

const graph = {
  A: ["B", "C"],
  B: ["A", "D"],
  C: ["A", "D", "E"],
  D: ["B", "C", "F"],
  E: ["C", "F"],
  F: ["D", "E"],
};

너비 우선 탐색: 한 겹씩

너비 우선 탐색(BFS)은 출발점 둘레를 한 겹씩 넓혀 가며 그래프를 탐색합니다. 먼저 거리 0인 출발점 자신, 다음에 거리 1인 이웃 전부, 그다음에 그 옆의 새 정점들인 거리 2, 이렇게 나아갑니다. 이를 위해 먼저 들어온 것이 먼저 나가는 줄인 큐를 씁니다. 큐 맨 앞의 정점을 꺼내 방문하고, 아직 못 본 이웃을 큐 뒤에 붙입니다. 예시에서 A부터 시작하면 BFS는 A, 그다음 B와 C, 그다음 D와 E, 마지막으로 F를 방문합니다. 바깥으로 한 겹씩 나아가므로 BFS가 어떤 정점에 처음 닿았을 때는 간선 수가 가장 적은 길을 찾은 것입니다. F는 A에서 간선 3개 떨어져 있고, 예를 들면 A, B, D, F입니다.

function bfs(graph, start) {
  const seen = new Set([start]);
  const queue = [start];
  const order = [];
  while (queue.length) {
    const u = queue.shift();        // first in, first out
    order.push(u);
    for (const v of graph[u]) {
      if (!seen.has(v)) { seen.add(v); queue.push(v); }
    }
  }
  return order;   // A B C D E F
}

깊이 우선 탐색: 갈 수 있는 데까지 가고 되돌아오기

깊이 우선 탐색(DFS)은 미로에서 한 손을 벽에 대고 걷는 사람처럼 움직입니다. 한 길을 갈 수 있는 데까지 깊이 따라갑니다. 막다른 곳이나 이미 본 곳에 닿으면 가장 최근 갈림길로 되돌아가 다음 길을 시도합니다. 큐 대신 나중에 들어간 것이 먼저 나오는 스택을 쓰며, 호출 스택이 기억을 대신해 주는 재귀로 쓰면 아주 자연스럽습니다. A부터 재귀 DFS를 하면 A, B, D, C, E, F 순서로 방문합니다. DFS도 BFS가 닿는 정점에 모두 닿지만, 따라간 길이 가장 짧은 길이라는 보장은 없습니다.

function dfs(graph, u, seen = new Set(), order = []) {
  seen.add(u);
  order.push(u);
  for (const v of graph[u]) if (!seen.has(v)) dfs(graph, v, seen, order);
  return order;   // A B D C E F
}
불러오는 중

끌어서 돌리고, 휠이나 두 손가락으로 확대하세요.

그래프로 본 미로: 열린 칸 하나하나가 정점이고, 옆의 열린 칸과 이어져 있습니다. 같은 미로에서 BFS와 DFS를 차례로 돌려 방문한 칸 수와 찾은 길의 길이를 비교해 보세요.

언제 무엇을 쓸까

실험실의 첫 미로에서 BFS는 105칸을 방문하고 가능한 가장 짧은 22번 이동의 길을 찾습니다. DFS는 마침 목표 쪽으로 파고들어 31칸만 방문하지만, 찾은 길은 28번 움직이는 길입니다. 어느 쪽이 늘 덜 일하는 것은 아닙니다. 둘 다 정점과 간선을 정해진 횟수만큼만 다루므로, 정점 V개와 간선 E개인 그래프에서 둘 다 O(V + E) 시간이 걸립니다. 가장 적은 단계가 필요하면 BFS를 고릅니다. 퍼즐의 가장 적은 수, 두 사람 사이의 가장 적은 연결 같은 경우입니다. 모두 둘러보거나 선택을 끝까지 따라가야 하면 DFS를 고릅니다. 미로의 모든 방이 이어져 있는지 확인하거나, 퍼즐의 수를 한 줄씩 시도하다 막히면 되돌아오는 경우입니다. 도로처럼 간선마다 길이가 다르면 둘 다 그대로는 모자라고, 다익스트라 알고리즘 같은 방법이 BFS의 생각에 거리를 더해 씁니다.

두 탐색 모두 이미 본 정점을 기억해야 합니다. 그러지 않으면 A에서 B, D, C를 거쳐 다시 A로 돌아오는 고리가 있는 그래프에서 탐색이 끝없이 맴돕니다.
그래프간선으로 이어진 정점들
정점(노드)그래프의 점
간선두 정점을 잇는 선
인접 리스트정점마다 이웃을 적은 목록
큐먼저 들어온 것이 먼저 나감. BFS가 씀
BFS너비 우선 탐색: 한 겹씩. 간선이 가장 적은 길을 찾음
DFS깊이 우선 탐색: 깊이 먼저, 막히면 되돌아옴. 스택을 씀
O(V + E)BFS와 DFS의 실행 시간
  1. 예시 그래프에서 A에서 F까지 가장 짧은 길에는 간선이 몇 개 있을까요?
  2. 예시 그래프에는 간선이 몇 개 있을까요?
  3. 너비 우선 탐색이 쓰는 자료 구조는?
    • 큐(먼저 들어온 것이 먼저 나감)
    • 스택(나중에 들어온 것이 먼저 나감)
    • 정렬된 목록
    • 아무것도 쓰지 않음
  4. 간선 수가 가장 적은 길을 반드시 찾는 탐색은?
    • BFS
    • DFS
    • 둘 다 언제나
    • 둘 다 아님
  5. A에서 시작한 BFS가 정점을 방문하는 순서대로 놓으세요(이웃은 알파벳순).
  6. A에서 시작한 재귀 DFS가 정점을 방문하는 순서대로 놓으세요(이웃은 알파벳순).
  7. 실험실의 15 × 9 격자에서 벽을 모두 지우면 S(왼쪽 위)에서 G(오른쪽 아래)까지 가장 짧은 길은 몇 번 이동일까요?
  8. BFS와 DFS가 이미 본 정점을 기억해야 하는 이유는?
    • 그러지 않으면 그래프의 고리 때문에 탐색이 끝없이 돌기 때문
    • 마지막에 출력하려고
    • 정렬하려고
    • DFS에서만 필요해서
  9. 오일러가 쾨니히스베르크 다리 문제를 푼 해는?
    • 1683년
    • 1736년
    • 1843년
    • 1945년
  10. 정점 V개, 간선 E개인 그래프에서 BFS의 실행 시간은?
    • O(V + E)
    • O(V × E)
    • O(log V)
    • O(2ⱽ)
  11. 소셜 네트워크에서 두 사람 사이 「친구의 친구」 연결이 가장 적은 경로를 알고 싶습니다. 어떤 탐색을 쓸까요?
    • BFS
    • DFS
    • 이진 탐색
    • 병합 정렬
핵심 정리
  • 그래프는 간선으로 이어진 정점들이고, 인접 리스트로 담습니다.
  • BFS는 큐를 써서 한 겹씩 나아가고 간선이 가장 적은 길을 찾습니다.
  • DFS는 스택(또는 재귀)을 써서 깊이 먼저 갔다가 되돌아옵니다.
  • 둘 다 O(V + E)이고 본 정점을 기억해야 합니다.
  • 1736년 오일러의 쾨니히스베르크 다리 풀이가 그래프 이론의 시작입니다.