[Daily morning study] 강한 연결 요소 (SCC)와 코사라주·타잔 알고리즘

#daily morning study

Image


강한 연결 요소 (Strongly Connected Components, SCC)

개념

방향 그래프(directed graph)에서 SCC란 서로 간에 도달 가능한 정점들의 최대 집합이다.

즉, SCC 내의 임의의 두 정점 u, v에 대해 u → v 경로와 v → u 경로가 모두 존재한다.

    A → B → C
    ↑       ↓
    D ← E ← F

위 예시에서 {A, B, C, D, E, F}가 모두 서로 도달 가능하면 하나의 SCC를 형성한다.

SCC를 구하면 그래프를 DAG(방향 비순환 그래프) 로 압축할 수 있다. 각 SCC를 하나의 노드로 취급하면 사이클 없는 구조가 된다.


코사라주 알고리즘 (Kosaraju’s Algorithm)

동작 원리

두 번의 DFS로 SCC를 구한다.

  1. 원래 그래프에서 DFS를 수행하며 종료 순서(finish order) 를 스택에 기록
  2. 그래프의 역방향 간선으로 만든 역그래프(transposed graph)에서, 스택에서 꺼낸 순서대로 DFS 수행
  3. 역그래프에서 DFS로 방문한 정점들의 묶음이 하나의 SCC

핵심 직관

원래 그래프에서 나중에 종료된 정점일수록 “상위” SCC에 속한다. 역그래프에서 그 정점을 출발점으로 DFS하면 같은 SCC 내 정점만 방문하게 된다.

구현 (Python)

from collections import defaultdict

def kosaraju(n, edges):
    graph = defaultdict(list)
    rev_graph = defaultdict(list)
    
    for u, v in edges:
        graph[u].append(v)
        rev_graph[v].append(u)
    
    visited = [False] * (n + 1)
    order = []
    
    def dfs1(v):
        visited[v] = True
        for nxt in graph[v]:
            if not visited[nxt]:
                dfs1(nxt)
        order.append(v)
    
    for i in range(1, n + 1):
        if not visited[i]:
            dfs1(i)
    
    visited = [False] * (n + 1)
    sccs = []
    
    def dfs2(v, scc):
        visited[v] = True
        scc.append(v)
        for nxt in rev_graph[v]:
            if not visited[nxt]:
                dfs2(nxt, scc)
    
    while order:
        v = order.pop()
        if not visited[v]:
            scc = []
            dfs2(v, scc)
            sccs.append(scc)
    
    return sccs

시간 복잡도

O(V + E) — 각 DFS가 모든 정점과 간선을 한 번씩 방문한다.


타잔 알고리즘 (Tarjan’s Algorithm)

동작 원리

단 한 번의 DFS로 SCC를 구한다. 각 정점에 두 값을 유지한다:

  • disc[v]: 정점 v를 처음 방문한 시간(discovery time)
  • low[v]: v를 루트로 하는 DFS 서브트리에서 도달 가능한 정점의 최솟값

스택을 사용해 현재 DFS 경로 상의 정점을 유지한다. disc[v] == low[v]인 정점 v가 SCC의 루트다.

구현 (Python)

from collections import defaultdict

def tarjan(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
    
    disc = [-1] * (n + 1)
    low = [0] * (n + 1)
    on_stack = [False] * (n + 1)
    stack = []
    timer = [0]
    sccs = []
    
    def dfs(v):
        disc[v] = low[v] = timer[0]
        timer[0] += 1
        stack.append(v)
        on_stack[v] = True
        
        for nxt in graph[v]:
            if disc[nxt] == -1:          # 미방문 정점
                dfs(nxt)
                low[v] = min(low[v], low[nxt])
            elif on_stack[nxt]:           # 스택에 있는 정점 (역방향 간선)
                low[v] = min(low[v], disc[nxt])
        
        # SCC 루트 발견
        if low[v] == disc[v]:
            scc = []
            while True:
                u = stack.pop()
                on_stack[u] = False
                scc.append(u)
                if u == v:
                    break
            sccs.append(scc)
    
    for i in range(1, n + 1):
        if disc[i] == -1:
            dfs(i)
    
    return sccs

핵심 포인트

on_stack 배열로 현재 DFS 경로 상의 정점을 추적한다. 이미 방문했더라도 스택에 없으면 다른 SCC에 속하므로 low 값을 갱신하지 않는다.

시간 복잡도

O(V + E) — 코사라주와 동일하지만 DFS를 한 번만 수행한다.


두 알고리즘 비교

항목코사라주타잔
DFS 횟수2회1회
역그래프 필요 여부필요불필요
구현 복잡도비교적 단순비교적 복잡
메모리역그래프 추가 저장스택만 사용
시간 복잡도O(V + E)O(V + E)

실무에서는 역그래프 구성이 부담스러울 때 타잔을 선호한다. 구현 용이성을 우선하면 코사라주가 직관적이다.


SCC의 활용

1. 2-SAT 문제

불리언 변수들의 AND/OR 조건이 만족 가능한지 판별하는 문제다. 변수마다 변수와 부정을 정점으로 만들고 함의 관계를 간선으로 표현하면, SCC로 모순 여부를 확인할 수 있다.

  • 변수 x와 ¬x가 같은 SCC에 있으면 → 불만족(UNSAT)
  • 같은 SCC에 없으면 → 만족 가능(SAT)

2. 그래프 압축

SCC를 하나의 슈퍼노드로 묶어 DAG를 만들면, 사이클을 제거하고 위상 정렬이나 동적 계획법을 적용할 수 있다.

3. 도달 가능성 분석

컴파일러 최적화, 데이터 흐름 분석에서 어느 코드 블록이 반드시 실행되는지 판별할 때 사용한다.


예제: 백준 2150 (Strongly Connected Component)

import sys
from collections import defaultdict
input = sys.stdin.readline
sys.setrecursionlimit(100000)

def solve():
    V, E = map(int, input().split())
    graph = defaultdict(list)
    for _ in range(E):
        a, b = map(int, input().split())
        graph[a].append(b)
    
    disc = [-1] * (V + 1)
    low = [0] * (V + 1)
    on_stack = [False] * (V + 1)
    stack = []
    timer = [0]
    sccs = []
    
    def dfs(v):
        disc[v] = low[v] = timer[0]
        timer[0] += 1
        stack.append(v)
        on_stack[v] = True
        for nxt in graph[v]:
            if disc[nxt] == -1:
                dfs(nxt)
                low[v] = min(low[v], low[nxt])
            elif on_stack[nxt]:
                low[v] = min(low[v], disc[nxt])
        if low[v] == disc[v]:
            scc = []
            while True:
                u = stack.pop()
                on_stack[u] = False
                scc.append(u)
                if u == v:
                    break
            sccs.append(sorted(scc))
    
    for i in range(1, V + 1):
        if disc[i] == -1:
            dfs(i)
    
    sccs.sort()
    print(len(sccs))
    for scc in sccs:
        print(*scc, -1)

solve()

정리

  • SCC는 방향 그래프에서 서로 도달 가능한 정점들의 최대 집합이다.
  • 코사라주는 두 번의 DFS와 역그래프를 사용한다. 구현이 단순하다.
  • 타잔은 한 번의 DFS와 스택, disc/low 배열로 SCC를 구한다.
  • 둘 다 O(V + E) 시간 복잡도를 가진다.
  • 2-SAT, 그래프 압축, 도달 가능성 분석 등에 활용된다.