개념

방향 그래프에서 선행 순서를 지키며 모든 정점을 나열하는 알고리즘

**사이클이 없는 방향 그래프(DAG)**에서만 가능


시간복잡도

구현 방식시간복잡도
DFS 기반O(V + E)
진입 차수 (BFS) 기반O(V + E)
공간복잡도O(V + E)

특징

장점

  • 순서 보장: 선행 관계 유지
  • 빠른 속도: O(V + E)
  • 사이클 탐지: 위상 정렬 불가 = 사이클 존재

단점

  • DAG만 가능: 사이클 있으면 불가
  • 유일하지 않음: 여러 결과 가능

DAG (Directed Acyclic Graph)

  • 방향 그래프: 간선에 방향 있음
  • 비순환: 사이클이 없음
  • 예시: 과목 선수과목, 작업 순서

구현 방법

진입 차수 기반 (Kahn’s Algorithm)

from collections import deque
 
def topological_sort_kahn(n, edges):
    """위상 정렬 - 진입 차수 기반"""
    # 그래프 및 진입 차수 초기화
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
 
    # 진입 차수가 0인 정점을 큐에 추가
    queue = deque()
    for i in range(n):
        if in_degree[i] == 0:
            queue.append(i)
 
    result = []
 
    while queue:
        current = queue.popleft()
        result.append(current)
 
        # 인접 정점의 진입 차수 감소
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
 
            # 진입 차수가 0이 되면 큐에 추가
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
 
    # 사이클 확인
    if len(result) != n:
        return None  # 사이클 존재
 
    return result
 
# 테스트
edges = [
    (0, 1), (0, 2),
    (1, 3), (2, 3)
]
 
print(topological_sort_kahn(4, edges))  # [0, 1, 2, 3] 또는 [0, 2, 1, 3]
 
# 시간복잡도: O(V + E)
# 공간복잡도: O(V + E)

DFS 기반

def topological_sort_dfs(n, edges):
    """위상 정렬 - DFS 기반"""
    graph = [[] for _ in range(n)]
    for u, v in edges:
        graph[u].append(v)
 
    visited = [False] * n
    stack = []
 
    def dfs(v):
        visited[v] = True
 
        for neighbor in graph[v]:
            if not visited[neighbor]:
                dfs(neighbor)
 
        # 후위 순회 순서로 스택에 추가
        stack.append(v)
 
    # 모든 정점에서 DFS
    for i in range(n):
        if not visited[i]:
            dfs(i)
 
    # 스택을 역순으로 반환
    return stack[::-1]
 
# 테스트
edges = [
    (0, 1), (0, 2),
    (1, 3), (2, 3)
]
 
print(topological_sort_dfs(4, edges))  # [0, 2, 1, 3] 또는 [0, 1, 2, 3]
 
# 시간복잡도: O(V + E)

사이클 탐지 포함

def topological_sort_with_cycle_detection(n, edges):
    """사이클 탐지 포함"""
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
 
    queue = deque()
    for i in range(n):
        if in_degree[i] == 0:
            queue.append(i)
 
    result = []
    count = 0
 
    while queue:
        current = queue.popleft()
        result.append(current)
        count += 1
 
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
 
    # 처리된 정점 수가 n보다 적으면 사이클
    if count != n:
        return None, "사이클 존재"
 
    return result, "성공"
 
# 테스트
# 사이클 있는 경우
edges_with_cycle = [
    (0, 1), (1, 2), (2, 0)
]
 
result, msg = topological_sort_with_cycle_detection(3, edges_with_cycle)
print(msg)  # "사이클 존재"

동작 과정

그래프: (선수과목 관계)
    0 → 1
    ↓   ↓
    2 → 3

간선: (0→1), (0→2), (1→3), (2→3)

진입 차수:
  0: 0 (선수과목 없음)
  1: 1 (0 필요)
  2: 1 (0 필요)
  3: 2 (1, 2 필요)

1단계: 진입 차수 0인 정점
  큐: [0]
  결과: []

2단계: 0 처리
  0의 인접: 1, 2
  진입 차수: 1→0, 2→0
  큐: [1, 2]
  결과: [0]

3단계: 1 처리
  1의 인접: 3
  진입 차수: 3→1
  큐: [2]
  결과: [0, 1]

4단계: 2 처리
  2의 인접: 3
  진입 차수: 3→0
  큐: [3]
  결과: [0, 1, 2]

5단계: 3 처리
  큐: []
  결과: [0, 1, 2, 3]

완료: [0, 1, 2, 3]

템플릿

from collections import deque
 
def topological_sort_template(n, edges):
    # 1. 그래프 및 진입 차수 초기화
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
 
    # 2. 진입 차수 0인 정점 큐에 추가
    queue = deque()
    for i in range(n):
        if in_degree[i] == 0:
            queue.append(i)
 
    # 3. 위상 정렬
    result = []
 
    while queue:
        current = queue.popleft()
        result.append(current)
 
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
 
    # 4. 사이클 확인
    if len(result) != n:
        return None
 
    return result

핵심 요약

개념설명
원리선행 순서를 지키며 정점 나열
시간복잡도O(V + E)
공간복잡도O(V + E)
적용 조건DAG (사이클 없는 방향 그래프)
활용작업 순서, 선수과목

구현 방식 비교

방식장점단점
진입 차수 (Kahn)사이클 탐지 쉬움추가 배열 필요
DFS간단한 구현사이클 탐지 복잡

주의사항

1. DAG 확인

# 사이클이 있으면 위상 정렬 불가
def is_dag(n, edges):
    result = topological_sort_kahn(n, edges)
    return result is not None

2. 여러 정답

# 위상 정렬 결과는 유일하지 않음
# [0, 1, 2, 3]도 가능
# [0, 2, 1, 3]도 가능

3. 간선 방향

# 반드시 방향 그래프
# u → v: u가 v보다 먼저
edges = [(0, 1)]  # 0 → 1 (0이 1보다 먼저)

자주 나오는 문제 유형

1. 기본 위상 정렬

문제: 선수과목 순서대로 수강

def course_schedule(n, prerequisites):
    """과목 수강 순서"""
    result = topological_sort_kahn(n, prerequisites)
 
    if result is None:
        return "불가능"  # 사이클 존재
 
    return result
 
# 테스트
# (A, B): B를 듣기 전에 A를 들어야 함
prerequisites = [
    (0, 1), (0, 2),
    (1, 3), (2, 3)
]
 
print(course_schedule(4, prerequisites))  # [0, 1, 2, 3]

2. 사이클 탐지

문제: 순환 참조가 있는지 확인

def can_finish(n, prerequisites):
    """모든 과목을 들을 수 있는지"""
    result = topological_sort_kahn(n, prerequisites)
 
    return result is not None
 
# 테스트
# 사이클 있는 경우
prerequisites = [(0, 1), (1, 0)]
print(can_finish(2, prerequisites))  # False
 
# 사이클 없는 경우
prerequisites = [(0, 1), (1, 2)]
print(can_finish(3, prerequisites))  # True

3. 위상 정렬 순서 출력

문제: 백준 2252번 - 줄 세우기

def line_up(n, comparisons):
    """키 순서대로 줄 세우기"""
    result = topological_sort_kahn(n, comparisons)
 
    if result is None:
        return []
 
    return result
 
# 테스트
# (A, B): A가 B보다 앞에
comparisons = [
    (1, 3), (2, 3)
]
 
print(line_up(3, comparisons))  # [0, 1, 2] 또는 [0, 2, 1]

4. 작업 순서

문제: 선행 작업을 완료해야 하는 작업 순서

def task_order(n, dependencies):
    """작업 순서"""
    result = topological_sort_kahn(n, dependencies)
 
    if result is None:
        return "순환 의존성"
 
    return result
 
# 테스트
# (A, B): A 완료 후 B 가능
dependencies = [
    (0, 2), (1, 2),
    (2, 3), (2, 4)
]
 
print(task_order(5, dependencies))  # [0, 1, 2, 3, 4]

5. 빌드 순서

문제: 프로그래머스 - 빌드 순서

def build_order(n, dependencies):
    """빌드 순서 (LeetCode 210)"""
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for prereq, course in dependencies:
        graph[prereq].append(course)
        in_degree[course] += 1
 
    queue = deque()
    for i in range(n):
        if in_degree[i] == 0:
            queue.append(i)
 
    result = []
 
    while queue:
        current = queue.popleft()
        result.append(current)
 
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)
 
    return result if len(result) == n else []
 
# 테스트
dependencies = [[1, 0], [2, 0], [3, 1], [3, 2]]
print(build_order(4, dependencies))  # [0, 1, 2, 3] 또는 [0, 2, 1, 3]

6. 모든 위상 정렬 결과

문제: 가능한 모든 위상 정렬 순서

def all_topological_sorts(n, edges):
    """모든 위상 정렬 결과"""
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
 
    result = []
    current = []
 
    def backtrack(in_deg):
        # 진입 차수 0인 정점들
        available = [i for i in range(n) if in_deg[i] == 0]
 
        if not available:
            if len(current) == n:
                result.append(current[:])
            return
 
        for v in available:
            current.append(v)
            new_in_deg = in_deg[:]
            new_in_deg[v] = -1  # 방문 표시
 
            for neighbor in graph[v]:
                new_in_deg[neighbor] -= 1
 
            backtrack(new_in_deg)
            current.pop()
 
    backtrack(in_degree)
    return result
 
# 테스트
edges = [(0, 1), (0, 2)]
print(all_topological_sorts(3, edges))
# [[0, 1, 2], [0, 2, 1]]

7. 최소 학기 수

문제: 선수과목을 고려한 최소 학기

def minimum_semesters(n, prerequisites):
    """최소 학기 수"""
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in prerequisites:
        graph[u].append(v)
        in_degree[v] += 1
 
    queue = deque()
    for i in range(n):
        if in_degree[i] == 0:
            queue.append((i, 1))  # (정점, 학기)
 
    max_semester = 0
    count = 0
 
    while queue:
        current, semester = queue.popleft()
        max_semester = max(max_semester, semester)
        count += 1
 
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append((neighbor, semester + 1))
 
    return max_semester if count == n else -1
 
# 테스트
prerequisites = [
    (0, 1), (0, 2),
    (1, 3), (2, 3)
]
 
print(minimum_semesters(4, prerequisites))  # 3

추천 연습 문제

기초

중급

고급


위상 정렬 변형

1. 우선순위 포함

import heapq
 
def topological_sort_priority(n, edges):
    """사전순 위상 정렬"""
    graph = [[] for _ in range(n)]
    in_degree = [0] * n
 
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1
 
    # 우선순위 큐 사용
    heap = []
    for i in range(n):
        if in_degree[i] == 0:
            heapq.heappush(heap, i)
 
    result = []
 
    while heap:
        current = heapq.heappop(heap)
        result.append(current)
 
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                heapq.heappush(heap, neighbor)
 
    return result if len(result) == n else None
 
# 사전순으로 가장 앞선 위상 정렬

2. 그룹별 위상 정렬

def topological_sort_groups(n, edges, groups):
    """그룹 내부 + 그룹 간 위상 정렬"""
    # 복잡한 구현 - 생략
    pass

언제 사용할까?

사용 가능

  • 선행 관계: 순서가 정해진 작업
  • DAG: 사이클 없는 방향 그래프
  • 컴파일 순서: 의존성 관리
  • 빌드 시스템: Makefile 등

사용 불가

  • 사이클 있음: 순환 의존성
  • 무방향 그래프: 방향 필요
  • 순서 무관: 위상 정렬 불필요

결론: 선행 관계가 있는 작업 순서 결정에 최적!