강한 연결 요소(SCC) — 코사라주와 타잔 알고리즘

방향 그래프에서 서로 도달 가능한 노드 집합인 SCC를 DFS 2회의 코사라주와 DFS 1회의 타잔 알고리즘으로 O(V+E)에 구하는 방법을 완전 정리합니다.

· 6 min read · PALDYN Team

지난 글에서 무방향 그래프의 MST를 구하는 프림을 배웠습니다. 이번에는 방향 그래프(Directed Graph) 에서 핵심 구조인 **강한 연결 요소(SCC, Strongly Connected Component)**를 찾는 알고리즘을 다룹니다. SCC는 서로 양방향으로 도달 가능한 노드들의 최대 집합입니다. 웹 페이지 링크 분석, 컴파일러의 순환 의존성 탐지, 게임 맵의 폐쇄 구역 분석 등에 활용됩니다.

SCC란?

방향 그래프에서 두 노드 u, v가 u → v 경로와 v → u 경로가 모두 존재할 때 같은 SCC에 속합니다.

모든 SCC를 하나의 노드로 축소하면 **압축 그래프(Condensation Graph)**가 만들어지는데, 이는 항상 **DAG(사이클 없는 방향 그래프)**입니다.

SCC 구조와 압축 그래프

코사라주(Kosaraju) 알고리즘

가장 직관적인 방법입니다.

  1. 1단계: 원래 그래프에서 DFS 수행, 노드 종료 순서를 스택에 기록
  2. 2단계: 그래프의 역방향(transpose) 에서 스택에서 꺼내는 순서대로 DFS — 각 DFS가 탐색하는 노드가 하나의 SCC
from collections import defaultdict

def kosaraju(n, adj):
    # 역방향 그래프 구성
    radj = defaultdict(list)
    for u in adj:
        for v in adj[u]:
            radj[v].append(u)

    # 1단계: 종료 순서 스택
    visited = [False] * n
    order = []

    def dfs1(v):
        visited[v] = True
        for w in adj[v]:
            if not visited[w]:
                dfs1(w)
        order.append(v)

    for v in range(n):
        if not visited[v]:
            dfs1(v)

    # 2단계: 역방향 그래프 DFS
    visited2 = [False] * n
    sccs = []

    def dfs2(v, comp):
        visited2[v] = True
        comp.append(v)
        for w in radj[v]:
            if not visited2[w]:
                dfs2(w, comp)

    for v in reversed(order):
        if not visited2[v]:
            comp = []
            dfs2(v, comp)
            sccs.append(comp)

    return sccs

타잔(Tarjan) 알고리즘

DFS 한 번으로 SCC를 모두 찾습니다. 각 노드에 disc(발견 시각)와 low(도달 가능한 최소 disc)를 부여하고, low[u] == disc[u]이면 u가 SCC의 루트입니다.

def tarjan(n, graph):
    disc = [-1] * n
    low = [-1] * n
    on_stack = [False] * n
    stack = []
    timer = [0]
    sccs = []

    def dfs(u):
        disc[u] = low[u] = timer[0]
        timer[0] += 1
        stack.append(u)
        on_stack[u] = True

        for v in graph[u]:
            if disc[v] == -1:
                dfs(v)
                low[u] = min(low[u], low[v])
            elif on_stack[v]:
                low[u] = min(low[u], disc[v])

        # SCC 루트: low[u] == disc[u]
        if low[u] == disc[u]:
            scc = []
            while True:
                w = stack.pop()
                on_stack[w] = False
                scc.append(w)
                if w == u:
                    break
            sccs.append(scc)

    for v in range(n):
        if disc[v] == -1:
            dfs(v)

    return sccs

코사라주 vs 타잔 코드 비교

재귀 깊이 문제 해결 — 반복 타잔

Python의 기본 재귀 한도(1000)를 초과하는 큰 그래프에서는 반복 버전이 필요합니다.

import sys
sys.setrecursionlimit(10 ** 6)  # 간단한 해결책

# 또는 반복 DFS로 타잔 구현
def tarjan_iterative(n, graph):
    disc = [-1] * n
    low = [-1] * n
    on_stack = [False] * n
    stack = []
    timer = [0]
    sccs = []
    call_stack = []

    for start in range(n):
        if disc[start] != -1:
            continue
        call_stack.append((start, iter(graph[start])))
        disc[start] = low[start] = timer[0]
        timer[0] += 1
        stack.append(start)
        on_stack[start] = True

        while call_stack:
            u, neighbors = call_stack[-1]
            try:
                v = next(neighbors)
                if disc[v] == -1:
                    disc[v] = low[v] = timer[0]
                    timer[0] += 1
                    stack.append(v)
                    on_stack[v] = True
                    call_stack.append((v, iter(graph[v])))
                elif on_stack[v]:
                    low[u] = min(low[u], disc[v])
            except StopIteration:
                call_stack.pop()
                if call_stack:
                    parent = call_stack[-1][0]
                    low[parent] = min(low[parent], low[u])
                if low[u] == disc[u]:
                    scc = []
                    while True:
                        w = stack.pop()
                        on_stack[w] = False
                        scc.append(w)
                        if w == u:
                            break
                    sccs.append(scc)
    return sccs

활용 — 2-SAT

SCC의 대표적인 응용은 **2-SAT(2-Satisfiability Problem)**입니다. 각 리터럴 xi와 ¬xi를 노드로 만들고 논리 함의 관계를 간선으로 표현한 뒤, xi와 ¬xi가 같은 SCC에 있으면 UNSAT입니다.

def two_sat(n, clauses):
    """
    n개 변수, clauses: [(a, b)] where a or b = True
    변수 i: 노드 2i, 변수 ¬i: 노드 2i+1
    """
    graph = defaultdict(list)
    for a, b in clauses:
        # (a or b) == (¬a → b) and (¬b → a)
        graph[a ^ 1].append(b)
        graph[b ^ 1].append(a)

    sccs = tarjan(2 * n, graph)
    comp = [0] * (2 * n)
    for i, scc in enumerate(sccs):
        for v in scc:
            comp[v] = i

    for i in range(n):
        if comp[2 * i] == comp[2 * i + 1]:
            return None  # UNSAT
    return [comp[2 * i] > comp[2 * i + 1] for i in range(n)]

알고리즘 비교

특성코사라주타잔
DFS 횟수2회1회
시간 복잡도O(V+E)O(V+E)
추가 공간역방향 그래프disc/low/stack
구현 난이도낮음중간
실용성이해하기 쉬움상수 작음

요약

SCC는 방향 그래프에서 동치 클래스를 정의합니다. 코사라주는 두 번의 DFS로 직관적으로, 타잔은 한 번의 DFS로 low 값을 활용해 구합니다. 압축 그래프(Condensation)를 만들면 복잡한 방향 그래프를 DAG로 단순화해 후속 처리가 쉬워집니다.


지난 글: 프림(Prim) 알고리즘 — 그리디 방식으로 MST 확장

다음 글: 단절점과 단절선 — 그래프의 취약 구조 탐지


읽어주셔서 감사합니다. 😊