본문으로 건너뛰기
김신건의 로그

Transitive Closure: 도달 가능성 폐포

· 수정 · 📖 약 4분 · 1,323자/단어 #algorithm #graph #reachability #floyd-warshall #bitset
Transitive Closure, 이행 폐포, reachability closure, 도달 가능성 행렬, 도달 가능성 폐포

정의

Transitive Closure (이행 폐포) 는 그래프의 모든 정점 쌍 (u, v) 에 대해 u 에서 v 로 도달 가능한지를 담은 부울 행렬입니다.

TC[i][j] = \begin\{cases\} 1 & \text\{i에서 j로 경로 존재\} \\ 0 & \text{경로 없음} \end\{cases\}

자기 자신 (TC[i][i] = 1) 은 항상 도달 가능으로 간주합니다.

문제 상황과 동기

언제 필요한가

  • 의존성 분석: 패키지 A가 패키지 C를 간접 의존하는가?
  • 접근 제어 (RBAC): 역할 A가 역할 B의 권한을 상속받는가?
  • DAG 분석: 컴파일 유닛 간 전이 의존 관계, 선수 과목 체계
  • 그래프 축약: SCC 압축 후 DAG에서 이행 폐포로 도달 가능 판단

단순 BFS 대비 TC의 장점

  • “A에서 B까지 갈 수 있나?” 단일 쿼리: BFS 1번, O(V+E)
  • 모든 쌍 (u, v) 에 대한 도달 가능성 전처리: TC를 한 번 구축하면 이후 쿼리 O(1)
방법전처리쿼리조건
BFS per sourceO(V(V+E))O(1)Sparse 그래프 유리
Floyd-Warshall TCO(V³)O(1)Dense 그래프 유리
Bitset 최적화 TCO(V³/64)O(1)V 크고 Dense 일 때

시각화

예시 그래프

flowchart LR
    n0["0"] --> n1["1"]
    n1 --> n2["2"]
    n2 --> n3["3"]
    n0 --> n3

직접 간선: 0→1, 1→2, 2→3, 0→3.

이행 폐포가 추가하는 간선: 0→2 (경로: 0→1→2), 1→3 (경로: 1→2→3).

Floyd-Warshall 순서로 구축되는 TC

중간 경유 정점 k를 순서대로 처리하며 점진적으로 완성됩니다.

flowchart TD
    Init["초기: 직접 간선만 TC에 포함"]
    K0["k=0: 0을 경유한 경로 추가"]
    K1["k=1: 1을 경유한 경로 추가 (TC[0][2] = 1)"]
    K2["k=2: 2를 경유한 경로 추가 (TC[1][3] = 1)"]
    K3["k=3: 3을 경유한 경로 추가"]
    Done["완성: V x V 도달 가능성 행렬"]

    Init --> K0 --> K1 --> K2 --> K3 --> Done

k=1 처리 후: TC[0][1]=1, TC[1][2]=1 이므로 TC[0][2] = 1 추가. k=2 처리 후: TC[1][2]=1, TC[2][3]=1 이므로 TC[1][3] = 1 추가.

핵심 아이디어

Floyd-Warshall 변형

최단 경로 대신 도달 가능성을 다루도록 변환. 핵심 점화식:

“i에서 k로, 그리고 k에서 j로 갈 수 있으면 i에서 j로 갈 수 있다.”

# Floyd-Warshall TC 초기화
for u in 0..V-1: TC[u][u] = true
for (u, v) in edges: TC[u][v] = true

# 경유 정점 k 순서로 갱신
for k in 0..V-1:
    for i in 0..V-1:
        for j in 0..V-1:
            TC[i][j] = TC[i][j] OR (TC[i][k] AND TC[k][j])

최적화: if TC[i][k] 체크로 내부 루프 스킵 가능.

DFS/BFS per Source

모든 정점 s에서 BFS/DFS를 실행해 도달 가능한 정점을 표시. O(V(V+E)). Sparse 그래프에서 Floyd-Warshall 보다 유리.

for s in 0..V-1:
    TC[s][s] = true
    BFS/DFS from s:
        TC[s][v] = true for each visited v

Bitset 최적화

TC의 각 행을 bitset으로 표현하면 OR 연산이 64비트 단위로 처리됩니다.

C++ bitset<V> 배열: 각 TC[i] |= TC[k] 가 비트셋 OR 연산 하나.

알고리즘

Floyd-Warshall TC (Dense, V <= 5000)

TC[i][j] = 직접 간선 + 대각선
for k = 0..V-1:
    for i = 0..V-1:
        if not TC[i][k]: continue   # 최적화
        TC[i] |= TC[k]              # 비트셋 가정 시 한 줄

BFS per Source (Sparse, V 크고 E 작을 때)

for s = 0..V-1:
    queue = {s}, visited = {s}
    while queue not empty:
        u = pop(queue)
        TC[s][u] = true
        for v in adj[u]:
            if not TC[s][v]:
                TC[s][v] = true
                push(queue, v)

DAG 최적화: 위상 정렬 역순

DAG라면 위상 정렬 역순으로 처리해 각 정점에서 후계 정점들의 TC를 OR 합산. O(V+E) 전처리 + O(V) OR 합산.

구현

// Transitive Closure: Floyd-Warshall + Bitset 최적화
#include <bits/stdc++.h>
using namespace std;

// 방법 1: Floyd-Warshall (O(V^3))
vector<vector<bool>> tc_floyd(int V, vector<pair<int,int>>& edges) {
  vector<vector<bool>> tc(V, vector<bool>(V, false));
  for (int u = 0; u < V; u++) tc[u][u] = true;
  for (auto [u, v] : edges) tc[u][v] = true;
  for (int k = 0; k < V; k++)
      for (int i = 0; i < V; i++)
          if (tc[i][k])
              for (int j = 0; j < V; j++)
                  tc[i][j] = tc[i][j] || tc[k][j];
  return tc;
}

// 방법 2: BFS per Source (O(V(V+E)), Sparse 유리)
vector<vector<bool>> tc_bfs(int V, vector<vector<int>>& adj) {
  vector<vector<bool>> tc(V, vector<bool>(V, false));
  for (int s = 0; s < V; s++) {
      tc[s][s] = true;
      queue<int> q;
      q.push(s);
      while (!q.empty()) {
          int u = q.front(); q.pop();
          for (int v : adj[u]) {
              if (!tc[s][v]) {
                  tc[s][v] = true;
                  q.push(v);
              }
          }
      }
  }
  return tc;
}

int main() {
  int V = 4;
  vector<pair<int,int>> edges = {{0,1},{1,2},{2,3},{0,3}};
  vector<vector<int>> adj(V);
  for (auto [u, v] : edges) adj[u].push_back(v);

  auto tc1 = tc_floyd(V, edges);
  cout << "Floyd-Warshall TC:\n";
  for (int i = 0; i < V; i++) {
      for (int j = 0; j < V; j++) cout << tc1[i][j] << " ";
      cout << "\n";
  }

  auto tc2 = tc_bfs(V, adj);
  cout << "BFS TC:\n";
  for (int i = 0; i < V; i++) {
      for (int j = 0; j < V; j++) cout << tc2[i][j] << " ";
      cout << "\n";
  }
  return 0;
}
stdin
V=4, edges: 0->1, 1->2, 2->3, 0->3
결과
Floyd-Warshall TC:
1 1 1 1 
0 1 1 1 
0 0 1 1 
0 0 0 1 
BFS TC:
1 1 1 1 
0 1 1 1 
0 0 1 1 
0 0 0 1

복잡도

항목Floyd-WarshallBFS per SourceBitset TC
전처리 시간O(V³)O(V(V+E))O(V³/64)
공간O(V²)O(V²)O(V²/8)
쿼리O(1)O(1)O(1)
유리한 조건Dense, V <= 3000Sparse, E << V²V <= 10000

V = 3000 이면 Floyd-Warshall 약 2.7 x 10^10 연산 (비트 조작) → 실전에서 Bitset 최적화 필요.

Bitset 최적화 코드 (V 클 때)

// Bitset Transitive Closure - O(V^3 / 64)
const int MAXV = 3000;
bitset<MAXV> tc[MAXV];

void build_tc_bitset(int V, vector<pair<int,int>>& edges) {
    for (int u = 0; u < V; u++) tc[u][u] = 1;
    for (auto [u, v] : edges) tc[u][v] = 1;
    for (int k = 0; k < V; k++)
        for (int i = 0; i < V; i++)
            if (tc[i][k]) tc[i] |= tc[k];
}
// 쿼리: tc[i][j] == 1 이면 i에서 j 도달 가능

함정

1. 자기 루프 (대각선) 초기화 누락

WARNING

TC[u][u] = true 초기화를 빠뜨리면 자기 자신 도달 가능성이 false 로 나옵니다. SCC 내에서 임의 정점 사이 경로 판단 시 오답.

2. Floyd-Warshall 루프 순서 고정 (k가 최외곽)

k 루프가 반드시 최외곽이어야 합니다. i, j 루프가 바깥이면 중간 경유점 갱신 전에 사용해 잘못된 결과.

// 잘못된 순서
for (int i = 0; i < V; i++)
    for (int j = 0; j < V; j++)
        for (int k = 0; k < V; k++)  // k가 안쪽 -> 오류

3. 무방향 그래프에서 양방향 추가 누락

방향 없는 간선이면 TC[u][v]TC[v][u] 모두 초기화.

4. 음수 가중치 vs TC

TC는 도달 가능성만 담습니다. 최단 거리가 필요하면 floyd-warshall 원본 알고리즘 사용.

5. SCC 압축 후 적용 권장

사이클이 많은 그래프는 먼저 Tarjan SCC 로 응축 DAG를 만들면 TC 크기가 V’ x V’ (V’ = SCC 수) 로 줄어 훨씬 효율적.

응용

DAG 선수 과목

TC[i][j] = true 이면 과목 i 수강 전에 과목 j 선수 필요. 수강 계획 자동 생성.

관계 추론

reaches(A, B) = “A가 B보다 권한이 높은가?” 를 실시간 조회 없이 전처리된 TC로 O(1).

그래프 동치 클래스

TC에서 TC[i][j] && TC[j][i] 이면 i, j 가 같은 SCC에 속함. SCC를 명시적으로 계산하지 않아도 그룹 판별 가능.

BOJ 연습 문제

번호제목유형
BOJ 11403경로 찾기Floyd-Warshall TC 기본
BOJ 1389케빈 베이컨의 6단계 법칙도달 가능 거리 합산
BOJ 9070DAG TC + DP
BOJ 1516게임 개발선수 관계 TC
BOJ 14567선수 과목DAG 도달 가능성

참고

이 글의 용어 (7개)
강한 연결 요소 (SCC)algorithm
정의 강한 연결 요소 (Strongly Connected Component, SCC) 는 방향 그래프에서 서로 도달 가능한 정점들의 최대 부분 집합. SCC 내 임의 두 정점 u…
깊이 우선 탐색 (DFS)algorithm
정의 깊이 우선 탐색 (Depth-First Search, DFS) 는 그래프 G=(V, E) 에서 갈 수 있는 만큼 깊이 들어가다가 막히면 백트래킹하는 알고리즘. 스택 (LIF…
너비 우선 탐색 (BFS)algorithm
정의 너비 우선 탐색 (Breadth-First Search, BFS) 는 그래프 G=(V, E) 에서 시작 정점 s 로부터 가까운 정점부터 순서대로 방문하는 알고리즘. 큐 (F…
방향 비순환 그래프 (DAG)algorithm
정의 방향 비순환 그래프 (Directed Acyclic Graph, DAG) 는 사이클이 없는 방향 그래프. 어떤 정점에서 출발해도 자기 자신으로 돌아오는 경로가 없다. DAG…
플로이드-워셜 알고리즘 (Floyd-Warshall Algorithm)algorithm
정의 플로이드-워셜 알고리즘 (Floyd-Warshall Algorithm) 은 모든 정점 쌍 (i, j) 사이의 최단 거리를 구하는 DP 알고리즘. Robert W. Floyd…
Bitset Optimizationalgorithm
정의 Bitset Optimization 은 불리언 / 비트 단위 정보 를 또는 배열에 패킹해, 한 명령어로 64 비트씩 병렬 연산 함으로써 시간 복잡도를 /64 (또는 , w …
Tarjan SCC: 강한 연결 요소 O(V+E)algorithm
정의 Strongly Connected Component (SCC) 는 유향 그래프에서 서로 도달 가능한 정점들의 극대 집합입니다. 즉, SCC 내 임의 두 정점 u, v 에 대…

이 개념을 다룬 위키 페이지 (1)

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기