방향 그래프에서 "서로 도달 가능한 정점들의 묶음"을 어떻게 찾을 수 있을까요?

강연결 요소(SCC)란

강연결 요소(Strongly Connected Component, SCC) 는 방향 그래프에서 모든 정점 쌍 사이에 양방향 경로가 존재하는 최대 부분 그래프입니다.

쉽게 말해, SCC 안의 어떤 정점에서 출발해도 같은 SCC 안의 다른 모든 정점에 도달할 수 있습니다.

PLAINTEXT
예: A → B → C → A (하나의 SCC)
    D → E (D에서 E는 가지만, E에서 D로는 못 감 → 같은 SCC 아님)

왜 필요한가

SCC를 찾으면 복잡한 방향 그래프를 DAG(방향 비순환 그래프) 로 압축할 수 있습니다. 각 SCC를 하나의 노드로 치환하면 사이클이 사라지므로, 위상 정렬 같은 DAG 전용 알고리즘을 적용할 수 있게 됩니다.

실무에서의 활용도 다양합니다.

  • **의존성 분석 **: 모듈 간 순환 의존성 탐지 (Maven, Gradle 빌드 도구)
  • ** 데드락 탐지 **: 트랜잭션 대기 그래프에서 순환 찾기
  • ** 소셜 네트워크 **: 커뮤니티 탐지
  • ** 컴파일러 **: 상호 재귀 함수 그룹 식별

동작 원리 — Kosaraju 알고리즘

Kosaraju 알고리즘은 DFS를 두 번 수행합니다. 이해하기 쉬워서 먼저 다루겠습니다.

단계

  1. 원래 그래프에서 DFS를 수행하며, ** 종료 시점 **을 스택에 기록합니다
  2. 모든 간선을 ** 역전 **한 그래프를 만듭니다
  3. 스택에서 하나씩 꺼내며 역전 그래프에서 DFS를 수행합니다. 각 DFS가 하나의 SCC입니다

왜 작동하는가

원래 그래프에서 DFS 종료가 늦은 정점은 "더 상위"의 SCC에 속합니다. 역전 그래프에서 이 순서로 탐색하면, SCC 간 경계를 넘지 못하기 때문에 정확히 하나의 SCC만 탐색하게 됩니다.

JAVA
public class Kosaraju {
    private int n;
    private List<List<Integer>> graph;    // 원래 그래프
    private List<List<Integer>> reversed; // 역전 그래프
    private boolean[] visited;
    private Deque<Integer> finishOrder;   // 종료 순서 스택

    public List<List<Integer>> findSCCs(List<List<Integer>> graph, int n) {
        this.n = n;
        this.graph = graph;
        this.reversed = new ArrayList<>();
        this.visited = new boolean[n];
        this.finishOrder = new ArrayDeque<>();

        // 역전 그래프 구성
        for (int i = 0; i < n; i++) reversed.add(new ArrayList<>());
        for (int u = 0; u < n; u++) {
            for (int v : graph.get(u)) {
                reversed.get(v).add(u);
            }
        }

        // 1단계: 원래 그래프에서 DFS, 종료 순서 기록
        for (int i = 0; i < n; i++) {
            if (!visited[i]) dfs1(i);
        }

        // 2단계: 역전 그래프에서 종료 역순으로 DFS
        Arrays.fill(visited, false);
        List<List<Integer>> sccs = new ArrayList<>();

        while (!finishOrder.isEmpty()) {
            int node = finishOrder.pop();
            if (!visited[node]) {
                List<Integer> scc = new ArrayList<>();
                dfs2(node, scc);
                sccs.add(scc);
            }
        }
        return sccs;
    }

    private void dfs1(int u) {
        visited[u] = true;
        for (int v : graph.get(u)) {
            if (!visited[v]) dfs1(v);
        }
        finishOrder.push(u); // 종료 시점에 스택에 추가
    }

    private void dfs2(int u, List<Integer> scc) {
        visited[u] = true;
        scc.add(u);
        for (int v : reversed.get(u)) {
            if (!visited[v]) dfs2(v, scc);
        }
    }
}

시간 복잡도는 O(V + E) 입니다. DFS를 두 번 하지만 각각 O(V + E)이므로 전체도 선형입니다.

동작 원리 — Tarjan 알고리즘

Tarjan 알고리즘은 DFS 한 번으로 SCC를 찾습니다.

핵심 아이디어

각 정점에 발견 순서(disc) 와 도달 가능한 최소 순서(low) 를 부여합니다. DFS를 돌면서 low[u] == disc[u]인 정점을 만나면, 그 정점이 SCC의 루트이고, 스택에서 해당 정점까지 꺼내면 하나의 SCC가 됩니다.

JAVA
public class Tarjan {
    private int n, timer;
    private int[] disc, low;
    private boolean[] onStack;
    private Deque<Integer> stack;
    private List<List<Integer>> graph;
    private List<List<Integer>> sccs;

    public List<List<Integer>> findSCCs(List<List<Integer>> graph, int n) {
        this.n = n;
        this.graph = graph;
        this.timer = 0;
        this.disc = new int[n];
        this.low = new int[n];
        this.onStack = new boolean[n];
        this.stack = new ArrayDeque<>();
        this.sccs = new ArrayList<>();
        Arrays.fill(disc, -1);

        for (int i = 0; i < n; i++) {
            if (disc[i] == -1) dfs(i);
        }
        return sccs;
    }

    private void dfs(int u) {
        disc[u] = low[u] = timer++;
        stack.push(u);
        onStack[u] = true;

        for (int v : graph.get(u)) {
            if (disc[v] == -1) {
                dfs(v);
                low[u] = Math.min(low[u], low[v]);
            } else if (onStack[v]) {
                low[u] = Math.min(low[u], disc[v]);
            }
        }

        // u가 SCC의 루트인 경우
        if (low[u] == disc[u]) {
            List<Integer> scc = new ArrayList<>();
            while (true) {
                int v = stack.pop();
                onStack[v] = false;
                scc.add(v);
                if (v == u) break;
            }
            sccs.add(scc);
        }
    }
}

SCC 축약(DAG 압축)

SCC를 찾은 후, 각 SCC를 하나의 노드로 치환하면 DAG가 됩니다.

JAVA
// SCC 축약 후 DAG 구성
int[] sccId = new int[n]; // 각 정점이 속한 SCC 번호
// ... SCC 찾기 과정에서 sccId 설정 ...

Set<Long> dagEdges = new HashSet<>();
List<List<Integer>> dag = new ArrayList<>();
for (int i = 0; i < sccCount; i++) dag.add(new ArrayList<>());

for (int u = 0; u < n; u++) {
    for (int v : graph.get(u)) {
        if (sccId[u] != sccId[v]) {
            long key = (long) sccId[u] * n + sccId[v];
            if (dagEdges.add(key)) {
                dag.get(sccId[u]).add(sccId[v]);
            }
        }
    }
}
// 이제 dag에 위상 정렬 등을 적용할 수 있습니다

2-SAT 문제

2-SAT은 SCC의 대표적인 응용입니다. 각 변수가 참 또는 거짓인 불리언 만족 가능성 문제 로, 절(clause)이 모두 2개의 리터럴로 구성됩니다.

2-SAT → 함의 그래프

(x₁ ∨ x₂) 같은 절을 함의(implication)로 바꿉니다.

PLAINTEXT
(x₁ ∨ x₂) → (¬x₁ → x₂) ∧ (¬x₂ → x₁)

이렇게 만든 함의 그래프 에서 SCC를 구하면 됩니다.

판정 기준

  • 어떤 변수 x에 대해 x와 ¬x가 같은 SCC에 속하면 → 해가 없습니다 (UNSATISFIABLE)
  • 그렇지 않으면 → 해가 존재합니다

백엔드/실무 연결

Spring 순환 의존성

Spring에서 빈(Bean) 간 순환 참조가 발생하면 BeanCurrentlyInCreationException이 발생합니다. 이는 의존성 그래프에서 SCC(사이클)를 탐지하는 것과 같은 원리입니다.

빌드 도구의 의존성 분석

Maven이나 Gradle은 모듈 간 의존성을 DAG로 관리합니다. 순환 의존성이 발견되면 빌드를 실패시키는데, 이때 SCC 알고리즘으로 어느 모듈들이 순환을 이루는지 알려줍니다.

데이터베이스 데드락 탐지

트랜잭션 대기 그래프(wait-for graph)에서 사이클이 있으면 데드락입니다. SCC를 찾으면 어떤 트랜잭션들이 서로 기다리고 있는지 식별할 수 있습니다.

Kosaraju vs Tarjan 비교

항목KosarajuTarjan
DFS 횟수2회1회
역전 그래프필요불필요
구현 난이도쉬움중간
시간 복잡도O(V + E)O(V + E)
실무 선호이해하기 좋음메모리 효율적

주의할 점

Tarjan과 Kosaraju 알고리즘의 구현 세부사항 혼동

Kosaraju는 DFS 후위 순서 + 역방향 그래프에서 DFS를 수행합니다. Tarjan은 하나의 DFS에서 low-link 값으로 SCC를 찾습니다. 두 알고리즘의 구현을 섞으면 정확한 결과가 나오지 않습니다.

2-SAT에서 변수와 부정의 인덱스 매핑 실수

변수 x를 2i, 부정 ~x를 2i+1로 매핑하는 관례에서 인덱스를 잘못 계산하면 논리식 전체가 틀어집니다.


정리

  • SCC 는 방향 그래프에서 서로 도달 가능한 정점들의 최대 묶음입니다
  • Kosaraju: DFS 2회 + 역전 그래프로 직관적으로 SCC를 찾습니다
  • Tarjan: DFS 1회 + low/disc 값으로 효율적으로 SCC를 찾습니다
  • SCC를 축약하면 DAG 가 되어 위상 정렬 등 다양한 알고리즘을 적용할 수 있습니다
  • 2-SAT 은 SCC의 대표적 응용으로, 함의 그래프에서 SCC를 구해 만족 가능성을 판정합니다
  • 순환 의존성 탐지, 데드락 감지 등 백엔드에서도 핵심적인 역할을 합니다
댓글 로딩 중...