🎁 지금 가입하면 온라인 AI를 최대 30분 무료로 사용할 수 있습니다. 신용카드는 필요하지 않습니다.

코사라주 알고리즘: 강한 연결 요소 가이드

August 22, 2026
코사라주 알고리즘이 두 번의 DFS로 강한 연결 요소를 찾는 방법을 예제, 테스트된 Python 코드, 복잡도, 흔한 실수와 함께 설명합니다.
코사라주 알고리즘: 강한 연결 요소 가이드
코사라주 알고리즘
강한 연결 요소
그래프 알고리즘
코딩 인터뷰 준비

Kosaraju 알고리즘은 방향 그래프의 강한 연결 요소(SCC)를 찾는 알고리즘입니다. SCC란 모든 정점이 서로에게 도달할 수 있는 정점 그룹을 말합니다. 이 알고리즘은 두 번의 깊이 우선 탐색을 실행합니다. 첫 번째 탐색은 각 정점이 탐색을 마치는 순서를 기록하고, 두 번째 탐색은 모든 간선을 뒤집은 그래프에서 정점을 탐색 완료의 역순으로 처리합니다. 두 번째 탐색에서 만들어지는 각 트리가 하나의 SCC입니다. 시간 복잡도는 O(V + E)입니다.

각 SCC를 하나의 노드로 압축하면 사이클이 없는 방향 그래프가 남습니다. 그래서 SCC는 의존성 분석, 교착 상태 탐지, 2-SAT, 그리고 사이클을 먼저 합쳐야 하는 모든 문제에서 등장합니다.

두 번의 탐색, 단계별로

  1. 전체 그래프에 대해 DFS를 실행합니다. 정점이 탐색을 마치면(나가는 모든 이웃을 처리하면) 리스트에 추가합니다.
  2. 모든 간선을 뒤집어 전치 그래프를 만듭니다.
  3. 탐색 완료 리스트를 마지막부터 처음까지 순회합니다. 아직 방문하지 않은 정점마다 전치 그래프에서 DFS를 실행합니다. 그 탐색이 도달하는 모든 정점이 하나의 SCC입니다.

그래프가 연결되어 있지 않을 수 있으므로 두 반복 모두 정점 0에서만이 아니라 방문하지 않은 모든 정점에서 시작해야 합니다.

예제로 따라가기

8개의 정점과 다음 간선들이 있다고 합시다:

0 → 1, 1 → 2, 2 → 0      (cycle A: 0, 1, 2)
2 → 3
3 → 4, 4 → 5, 5 → 3      (cycle B: 3, 4, 5)
6 → 5
6 → 7, 7 → 6             (cycle C: 6, 7)

SCC는 2, 5, 7이 되어야 합니다.

첫 번째 탐색

0에서 DFS를 시작해 0 → 1 → 2 → 3 → 4 → 5로 갑니다. 정점 5는 새로 갈 곳이 없으므로 가장 먼저 탐색을 마치고, 그다음 4, 그다음 3이 마칩니다. 2로 돌아오면 남은 게 없으므로 2, 1, 0이 차례로 마칩니다. 정점 6은 아직 방문하지 않았으므로 거기서 시작합니다. 5는 이미 처리되었으니 7로 가고, 7이 마친 뒤 6이 마칩니다. 탐색 완료 순서는 다음과 같습니다:

5, 4, 3, 2, 1, 0, 7, 6

두 번째 탐색

모든 간선을 뒤집고 그 리스트의 끝에서부터 정점을 처리합니다. 먼저 6입니다. 뒤집힌 그래프에서 6은 7에만 도달할 수 있으므로 7이 첫 번째 SCC입니다. 다음으로 방문하지 않은 정점은 0이고, 뒤집힌 그래프에서 2와 1에 도달하므로 2가 됩니다. 간선 2 → 3은 이제 3 → 2를 가리키지만 2는 이미 배정되었으므로 탐색은 거기서 멈춥니다. 마지막으로 3은 5와 4에 도달해 5가 됩니다.

왜 동작하는가

SCC들로 이루어진 그래프(각 요소를 하나의 노드로 압축한 것)를 생각해 보면 사이클이 없습니다. 첫 번째 탐색에서 가장 마지막에 탐색을 마치는 정점은 항상 "소스" 요소, 즉 다른 미배정 요소로부터 들어오는 간선이 없는 요소에 속합니다. 간선을 뒤집으면 그 소스가 싱크가 되므로, 거기서 시작한 DFS는 자기 요소를 벗어날 수 없습니다. 그 요소를 제거하면 같은 논리가 다음 요소에도 적용됩니다.

면접에서는 보통 이 정도 수준의 증명을 기대합니다. 탐색 완료 순서가 안전한 시작 요소를 골라 주고, 간선을 뒤집으면 두 번째 탐색이 다른 요소로 새어 나가지 않는다는 것입니다.

Python 구현

이 버전은 반복적 DFS를 사용하므로 깊은 그래프에서도 Python의 재귀 한도에 걸리지 않습니다.

from collections import defaultdict

def kosaraju(n, edges):
    graph = defaultdict(list)
    reverse = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        reverse[v].append(u)

    # Pass 1: record vertices in the order they finish (iterative DFS)
    visited = [False] * n
    order = []
    for start in range(n):
        if visited[start]:
            continue
        visited[start] = True
        stack = [(start, iter(graph[start]))]
        while stack:
            node, neighbors = stack[-1]
            for nxt in neighbors:
                if not visited[nxt]:
                    visited[nxt] = True
                    stack.append((nxt, iter(graph[nxt])))
                    break
            else:
                stack.pop()
                order.append(node)

    # Pass 2: DFS on the reversed graph, latest finish time first
    visited = [False] * n
    components = []
    for start in reversed(order):
        if visited[start]:
            continue
        visited[start] = True
        component = []
        stack = [start]
        while stack:
            node = stack.pop()
            component.append(node)
            for nxt in reverse[node]:
                if not visited[nxt]:
                    visited[nxt] = True
                    stack.append(nxt)
        components.append(component)
    return components


edges = [(0, 1), (1, 2), (2, 0), (2, 3), (3, 4), (4, 5), (5, 3), (6, 5), (6, 7), (7, 6)]
print(kosaraju(8, edges))  # [[6, 7], [0, 2, 1], [3, 5, 4]]

각 요소 안에서 정점의 순서는 탐색 방식에 따라 달라지므로 테스트에서는 요소를 집합으로 비교하세요. 이 구현은 2,000개의 무작위 그래프에 대해 완전 탐색 도달 가능성 검사와 대조해 검증했습니다.

복잡도

단계시간
첫 번째 DFSO(V + E)
뒤집힌 그래프 만들기O(V + E)
두 번째 DFSO(V + E)
합계O(V + E)

공간은 두 개의 인접 리스트에 O(V + E), 그리고 방문 배열, 탐색 완료 순서, 스택에 O(V)가 필요합니다.

알고리즘을 망가뜨리는 실수들

가장 흔한 버그는 DFS가 정점에 처음 도달했을 때 기록하는 것입니다. 탐색을 마칠 때가 아니라요. 다른 실수로는 두 번째 탐색을 위해 탐색 완료 순서를 뒤집는 것을 잊는 것, 두 번째 탐색을 뒤집힌 그래프가 아니라 원래 그래프에서 실행하는 것, 두 탐색 사이에 방문 배열을 초기화하지 않는 것, 정점 0에서만 시작해 연결되지 않은 부분을 건너뛰는 것이 있습니다. 재귀적 DFS는 긴 체인에서 스택이 넘칠 수도 있는데, 그래서 위 코드는 반복적 방식입니다.

좋은 테스트 케이스로는 정점 하나, 자기 루프가 있는 정점, 하나의 큰 사이클, 한 방향 체인(모든 정점이 각자 자신의 SCC), 간선 하나로 연결된 두 사이클, 연결되지 않은 그래프가 있습니다.

Kosaraju vs. Tarjan

둘 다 O(V + E)로 실행됩니다. Kosaraju는 두 번의 탐색을 하고 뒤집힌 그래프를 만들어서 설명하기도, 정확히 구현하기도 더 쉽습니다. Tarjan 알고리즘은 뒤집힌 그래프 없이 한 번의 탐색으로 처리하지만, 스택 위에서 발견 인덱스와 로우링크 값을 추적해야 하므로 관리할 상태가 더 많습니다. 면접에서는 정확히 구현하고 설명할 수 있는 쪽을 쓰세요. 면접관이 한 번의 탐색을 요구한다면 그건 Tarjan을 쓰라는 신호입니다.

그래프 문제 연습하기

코드를 작성하기 전에 위 예제를 한 번 손으로 따라가 보세요. 대부분의 버그는 잘못된 탐색 완료 순서로 나타납니다. 관련 가이드: 모든 쌍 최단 경로를 위한 Floyd–Warshall, 음수 간선이 있는 그래프를 위한 Bellman–Ford, 그리고 Big O 치트 시트가 있습니다. 코딩 연습에 YesToTheOffer를 사용한다면, 스크린샷 코딩 도움이 접근 방식을 점검해 주고, 세션 기록을 통해 추론을 명확하게 설명했는지 확인할 수 있습니다.

피드백과 함께 코딩 인터뷰 연습하기

코딩 도움 스크린샷과 연습 세션의 기록. 30분 무료.

iOS & Android — 출시 예정
요금 보기