설명

그래프 이론: 강한 연결 요소들(SCC)

강한 연결 요소의 의미와 타잔 알고리즘의 발견 번호·low-link·활성 스택 불변식을 예제와 구현으로 설명합니다.

이 글의 목차

인터랙티브

타잔은 서로 도달 가능한 정점을 SCC로 묶는다

방향 그래프

012345

다음 SCC를 눌러 첫 성분을 드러내세요.

SCC를 접으면 방향 비순환 그래프(DAG)가 된다

강한 연결 성분 안에서는 모든 정점이 서로 도달할 수 있습니다. 각 성분을 하나로 접으면 DAG가 됩니다. 여기서는 {0,1,2} → {3,4} → {5}입니다. 타잔은 한 번의 DFS로 모든 성분을 찾습니다.

타잔 알고리즘으로 찾는 강한 연결 요소

방향 그래프에서 두 정점이 서로 도달할 수 있으면 서로 강하게 연결되어 있다고 합니다. **강한 연결 요소(SCC)**는 이 상호 도달 성질을 만족하는 정점들의 최대 집합입니다. 각 SCC를 하나의 정점으로 합치면 방향 비순환 그래프가 되어 의존 관계나 도달성을 분석하는 데 활용할 수 있습니다.

타잔 알고리즘의 한 번 탐색

타잔 알고리즘은 깊이 우선 탐색(DFS) 한 번으로 모든 SCC를 O(V + E) 시간에 찾습니다. 정점 v의 index[v]는 발견 순서 번호입니다. lowLink[v]는 DFS 트리 간선을 따라가고, 마지막에 아직 활성 스택에 있는 정점으로 간선을 최대 한 번 따라갔을 때 도달할 수 있는 가장 작은 발견 번호입니다.

DFS 트리 자식의 탐색이 끝나면 부모는 자식의 lowLink를 반영합니다. 아직 스택에 있는 정점으로 향하는 간선은 그 정점의 index를 사용하고, 이미 스택에서 빠진 정점으로 향하는 간선은 무시합니다. 여기서 활성 스택은 재귀 호출 스택과 다릅니다. SCC가 완성되지 않은 정점을 보관합니다.

lowLink[v] === index[v]이면 v의 DFS 하위 트리에서 도달 가능한 정점은 더 앞선 활성 정점으로 돌아갈 수 없습니다. 따라서 v는 SCC의 루트이고, 활성 스택에서 v까지 꺼낸 정점들이 정확히 그 요소를 이룹니다. 바깥 반복문은 아직 방문하지 않은 정점에서도 탐색을 시작하므로 서로 연결되지 않은 부분도 처리합니다.

JavaScript 구현

함수는 인접 리스트 graph를 받습니다. graph[u]에는 u에서 나가는 간선의 도착점인 0부터 시작하는 인덱스를 나열합니다. 모든 도착점이 유효한 인덱스라고 가정합니다. 반환값은 0부터 시작하는 정점 인덱스 배열들의 목록이며, 요소와 각 요소 안의 순서는 정해져 있지 않습니다.

function stronglyConnectedComponents(graph) {
  const n = graph.length;
  const index = Array(n).fill(-1);
  const lowLink = Array(n).fill(0);
  const onStack = Array(n).fill(false);
  const stack = [];
  const components = [];
  let nextIndex = 0;

  function visit(v) {
    index[v] = nextIndex;
    lowLink[v] = nextIndex;
    nextIndex += 1;
    stack.push(v);
    onStack[v] = true;

    for (const w of graph[v]) {
      if (index[w] === -1) {
        visit(w);
        lowLink[v] = Math.min(lowLink[v], lowLink[w]);
      } else if (onStack[w]) {
        lowLink[v] = Math.min(lowLink[v], index[w]);
      }
    }

    if (lowLink[v] === index[v]) {
      const component = [];
      let w;
      do {
        w = stack.pop();
        onStack[w] = false;
        component.push(w);
      } while (w !== v);
      components.push(component);
    }
  }

  for (let v = 0; v < n; v += 1) {
    if (index[v] === -1) visit(v);
  }

  return components;
}

간선이 0→1, 1→2, 2→0, 2→3, 3→4, 4→3, 4→5인 그래프의 결과는 {0,1,2}, {3,4}, {5}입니다(순서는 달라도 됩니다). 앞의 세 정점은 하나의 순환을 이루고, 3과 4도 서로 순환하며, 5에서는 도달할 수 있고 다시 돌아오는 정점이 없습니다.

시간 복잡도는 O(V + E), 보조 공간 복잡도는 O(V)입니다. 이 구현은 재귀를 사용하므로 호출 깊이가 O(V)까지 늘 수 있습니다. 정점 수가 매우 많거나 깊은 그래프에서는 JavaScript 실행 환경의 호출 스택 한도를 넘을 수 있으므로 반복형 DFS를 고려하세요.

핵심: 발견 번호는 탐색 순서를, low-link는 도달 가능한 가장 앞선 활성 정점을 나타냅니다. low-link가 루트의 발견 번호와 같아지는 순간 SCC 하나를 꺼냅니다.