|

순서 제약 문제 풀이: 위상 정렬을 언제 써야 할까

순서 제약 문제 풀이: 위상 정렬을 언제 써야 할까
어떤 일을 하기 전에 먼저 끝나야 할 일이 있다면 위상 정렬을 의심해볼 수 있다

위상 정렬은 그래프 알고리즘을 배웠어도 실제 문제에서 잘 안 떠오르는 경우가 많습니다. 이유는 문제에 “위상 정렬을 하세요”라고 적혀 있지 않고, 대신 선수 과목, 빌드 순서, 작업 의존성처럼 일상적인 말로 순서 제약을 설명하기 때문입니다.

이번 글에서는 위상 정렬을 공식으로 외우는 대신, 어떤 일을 하기 전에 먼저 끝나야 할 일이 있는 문제로 읽는 감각을 중심으로 설명하겠습니다.

순서 제약 문제 풀이 요약 카드
핵심 판단 기준을 먼저 잡는 요약 카드

위상 정렬은 어떤 문제를 푸는 도구일까

위상 정렬은 순서 제약이 있는 문제를 푸는 도구입니다. 즉 A를 하기 전에 B가 먼저 되어야 하고, C를 하기 전에 A가 먼저 되어야 한다면 이런 선후관계를 만족하는 전체 순서를 만드는 문제에 잘 맞습니다.

대표적으로 선수 과목 문제, 작업 스케줄링, 빌드 의존성, 선행 조건이 있는 게임/퀘스트 순서 문제에서 자주 등장합니다.


왜 그래프 문제로 바뀌는가

A를 하기 전에 B가 먼저여야 한다는 말은, 그래프에서는 보통 B → A 간선으로 표현할 수 있습니다. 즉 먼저 해야 하는 쪽에서 나중에 할 쪽으로 방향을 그립니다.

이렇게 하면 문제의 핵심은 결국 “방향 그래프에서 선후관계를 깨지 않는 순서를 만들 수 있는가”로 바뀝니다. 그래서 위상 정렬은 방향 그래프 문제로 다뤄집니다.


진입차수는 왜 중요할까

진입차수는 어떤 정점으로 들어오는 간선 수입니다. 즉 그 일을 시작하기 전에 아직 남아 있는 선행 조건 개수라고 생각하면 됩니다.

진입차수가 0이라는 것은, 지금 당장 시작해도 되는 일이라는 뜻입니다. 그래서 위상 정렬에서는 진입차수 0인 정점부터 queue에 넣고 처리합니다.

입문자가 많이 헷갈리는 부분은 진입차수를 단순한 그래프 숫자로만 보는 것입니다. 실제로는 “아직 풀리지 않은 선행 조건 수”라고 해석하는 편이 훨씬 이해가 쉽습니다. 이렇게 바꾸면 queue에 들어가는 순간도 자연스럽게 이해됩니다.

from collections import deque

def topo_sort(graph, indegree, n):
    q = deque([i for i in range(n) if indegree[i] == 0])
    order = []

    while q:
        cur = q.popleft()
        order.append(cur)
        for nxt in graph[cur]:
            indegree[nxt] -= 1
            if indegree[nxt] == 0:
                q.append(nxt)

    return order

이 코드의 핵심은 “지금 바로 가능한 일부터 처리하고, 그 일이 끝났다고 표시하면서 다음 후보의 선행 조건을 줄인다”는 흐름입니다. 즉 진입차수 배열은 단순한 숫자 배열이 아니라, 남은 선행 조건 개수를 추적하는 장치입니다.


왜 queue가 자연스럽게 연결될까

위상 정렬의 BFS 스타일 구현은 queue를 아주 자연스럽게 사용합니다. 현재 가능한 후보들을 queue에 넣고 하나씩 꺼내면서, 다음에 가능해진 후보를 뒤에 추가하는 흐름이기 때문입니다.

그래서 위상 정렬은 그래프 문제이면서도 queue 패턴과 강하게 연결됩니다. 이 점에서 큐와 덱 글, BFS 글과도 연결됩니다.


문제에서 어떤 신호가 보이면 위상 정렬일까

  • 선수 과목처럼 먼저 해야 할 일이 있다
  • 작업 순서가 조건에 따라 정해진다
  • 전체 가능한 순서를 하나 구하라고 한다
  • 사이클이 있으면 모순이 생긴다고 해석할 수 있다

이 신호가 보이면 최단거리나 단순 탐색보다 위상 정렬 가능성을 먼저 보는 편이 좋습니다.


사이클이 있으면 왜 문제가 될까

위상 정렬에서 정상 순서와 사이클 모순 비교 카드
정상 DAG와 사이클 모순을 비교해 보여주는 카드

많은 입문자가 이 부분에서 “왜 queue가 비는데 정점이 남지?”를 단순 구현 실수로 오해합니다. 하지만 실제로는 순서 제약 자체가 서로 물고 도는 모순일 수 있습니다. 이 해석이 가능해야 위상 정렬 문제를 더 깊게 이해할 수 있습니다.

A 전에 B, B 전에 C, C 전에 A가 필요하다면 어떤 것도 먼저 시작할 수 없습니다. 즉 순서 제약이 서로 물고 돌아가면 전체 순서를 만들 수 없습니다.

이 부분이 특히 중요한 이유는, 위상 정렬 문제 중 일부는 순서를 출력하는 것보다 “가능한지 불가능한지 판별” 자체가 핵심이기 때문입니다. 즉 모든 정점을 다 처리하지 못했다면, 그건 단순 구현 실패가 아니라 제약 자체가 모순이라는 뜻일 수 있습니다.

그래서 위상 정렬을 했는데 모든 정점을 다 처리하지 못했다면, 보통은 사이클이 있다는 뜻으로 해석할 수 있습니다.


자주 하는 실수

  1. 간선 방향을 반대로 잡는다
  2. 진입차수 의미를 단순 방문 수처럼 착각한다
  3. queue에 처음부터 모든 정점을 넣는다
  4. 사이클이 있는 경우를 생각하지 않는다

특히 간선 방향을 잘못 잡으면 전체 해석이 완전히 뒤집힙니다. “누가 누구보다 먼저인가”를 문장으로 다시 읽어보고 방향을 정하는 습관이 중요합니다.


순서 제약 문제 풀이 동작 그림
동작 과정을 그림으로 다시 풀어 정리한 도식
위상 정렬 단계 추적 그림
진입차수와 queue가 어떻게 변하는지 단계적으로 보여주는 도식

이 도식을 따라가 보면 위상 정렬은 그래프 이론 문제라기보다, “가능한 일부터 하나씩 처리하면서 다음 가능한 일을 열어 주는 과정”이라는 점이 더 또렷해집니다. 이 감각이 잡히면 선수 과목, 빌드 의존성, 작업 순서 문제가 한 축으로 묶여 보입니다.

마무리

위상 정렬을 이해하는 핵심은 그래프 이름보다 선후관계 문제를 그래프로 번역하는 데 있습니다.

어떤 일을 하기 전에 먼저 끝나야 할 일이 있다면 위상 정렬 가능성을 먼저 떠올리는 습관이 중요합니다.

함께보면 좋은 글