위상 정렬 (Topological Sorting)
정의
위상 정렬 (Topological Sorting) 은 방향 비순환 그래프 (DAG) 의 모든 정점을 간선 방향을 어기지 않도록 일렬로 나열하는 것. 간선 u → v 가 있으면 정렬 결과에서 u 가 v 보다 앞에 온다.
DAG 에만 존재하며, 사이클이 있으면 위상 정렬이 불가능. 여러 해가 있을 수 있다.
문제 상황과 동기
선수 과목 그래프, 작업 의존성 (Makefile), 빌드 순서, 상속 / import 위계 등 의존 관계가 순환하지 않는 상황에서 실행 가능한 순서 를 찾는다.
- naive: DFS 로 탐색 후 임의 정렬. 간선 방향 보장 없음.
- topological sort: O(V + E) 에 의존성이 만족되는 순서를 출력.
핵심 통찰: DFS 의 post-order 역순 또는 in-degree 가 0 인 노드부터 BFS 하면 자연스럽게 위상 순서.
시각화
핵심 아이디어
방법 1: Kahn’s Algorithm (BFS, in-degree)
- 모든 정점의 진입 차수 (in-degree) 를 센다.
- in-degree 가 0 인 정점을 큐에 넣는다.
- 큐에서 빼며 정렬 결과에 추가, 그 정점에서 나가는 간선의 도착 정점 in-degree 를 1 감소.
- 새로 in-degree 가 0 이 된 정점을 큐에 넣는다.
- 큐가 빌 때까지 반복.
결과 개수가 V 미만이면 사이클 존재.
방법 2: DFS post-order 역순
- DFS 를 돌며 탐색이 끝난 (post-order) 순서를 스택에 쌓는다.
- 스택을 역순으로 출력하면 위상 정렬.
사이클 감지는 DFS 중 back edge 가 있는지 체크.
두 방법 모두 O(V + E) 시간, 공간 O(V).
알고리즘
Kahn’s (BFS)
topological_sort_kahn(G):
in_degree[v] = 0 for all v
for each edge u → v:
in_degree[v] += 1
Q = queue of all v with in_degree[v] = 0
result = []
while Q not empty:
u = Q.dequeue()
result.append(u)
for each edge u → v:
in_degree[v] -= 1
if in_degree[v] = 0:
Q.enqueue(v)
if len(result) < V:
return "CYCLE"
return result
DFS post-order 역순
topological_sort_dfs(G):
visited = [false] × V
stack = []
for v in V:
if not visited[v]:
dfs(v)
return reverse(stack)
dfs(v):
visited[v] = true
for u in neighbors[v]:
if not visited[u]:
dfs(u)
# back edge check: if u is in recursion stack → cycle
stack.push(v)
구현
// Kahn's algorithm, in-degree BFS
#include <bits/stdc++.h>
using namespace std;
int main() {
int v, e; cin >> v >> e;
vector<vector<int>> adj(v + 1);
vector<int> indeg(v + 1, 0);
for (int i = 0; i < e; i++) {
int a, b; cin >> a >> b; // a → b
adj[a].push_back(b);
indeg[b]++;
}
queue<int> Q;
for (int i = 1; i <= v; i++)
if (!indeg[i]) Q.push(i);
vector<int> res;
while (!Q.empty()) {
int u = Q.front(); Q.pop();
res.push_back(u);
for (int w : adj[u]) {
if (--indeg[w] == 0) Q.push(w);
}
}
if ((int)res.size() < v) {
cout << "CYCLE\n";
} else {
for (int x : res) cout << x << " ";
cout << "\n";
}
}6 7
1 2
1 3
2 4
3 4
4 5
3 5
5 61 2 3 4 5 6복잡도
| 항목 | 값 |
|---|---|
| 시간 (Kahn) | O(V + E) |
| 시간 (DFS) | O(V + E) |
| 공간 | O(V + E) (인접 리스트) |
| 유일성 | ✗ (해가 여러 개 가능) |
변형 / 활용
1. Lexicographically smallest order
Kahn 에서 큐 대신 priority queue 사용. 매번 가능한 정점 중 가장 작은 번호를 뽑음. 시간 O((V + E) log V).
2. 모든 위상 정렬 열거
백트래킹. in-degree 0 인 정점을 선택, 간선 제거, 재귀. 최악 O(V!).
3. Dynamic programming on DAG
위상 순서대로 DP 를 채우면 의존성 보장. 최장 경로, 경로 개수 등.
4. Critical Path Method (CPM)
작업 일정 관리. 최장 경로가 프로젝트 최소 완료 시간.
함정
1. 사이클 감지 누락
Kahn 에서 res.size() < V 체크, DFS 에서 back edge 체크. 둘 다 안 하면 잘못된 결과.
2. 1-indexed / 0-indexed 혼동
정점 번호 체계를 일관되게. in-degree 배열 크기 주의.
3. 다중 간선 / 자기 루프
자기 루프는 사이클. 다중 간선은 in-degree 중복 증가 주의.
4. disconnected graph
여러 컴포넌트가 있으면 모든 컴포넌트를 순회. DFS 에서 모든 정점 시작 체크.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 2252 | 줄 세우기 | 49.8% | kokoa-lab |
| BOJ 1766 | 문제집 | 38.6% | kokoa-lab |
| BOJ 1005 | ACM Craft | 33.2% | kokoa-lab |
| BOJ 2623 | 음악프로그램 | 52.3% | kokoa-lab |
참고
이 글의 용어 (4개)
- 강한 연결 요소 (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…
💬 댓글