{}연습장
대시보드학습스킬 경로문제모의고사라이브 코테복습데이터
← 학습 목록로드맵에서 보기
Lv.3 중급

위상정렬

선행 관계가 있는 작업들의 실행 순서 찾기 + 사이클(데드락) 판정. 진입 차수 0부터 처리하는 Kahn's algorithm이 표준입니다.

지문 신호“선수 과목”“먼저 완료해야”“의존 관계”
0/2 해결
1

눈으로 보기

재생을 누르고 단계별로 동작을 따라가세요
0
재료 손질
0
육수 내기
2
볶기
2
간 맞추기
1
플레이팅
1/8위상정렬: 선행 관계(화살표)를 지키는 실행 순서 찾기. 각 노드의 진입 차수(들어오는 화살표 수)를 세는 것부터 시작합니다.
노드 안 숫자 = 남은 진입 차수■ 초록 = 처리 완료 · ■ 앰버 = 처리 중 (Kahn’s algorithm)
2

개념 이해

핵심 패턴 코드는 손으로 따라 쳐 보는 것을 권장
1 / 8

비유로 이해하기

아침에 옷을 입는 순서를 생각해 봅니다. 양말을 신어야 신발을 신을 수 있고, 셔츠를 입어야 재킷을 걸칠 수 있습니다. 그래서 지금 바로 입을 수 있는 옷(앞에 걸린 조건이 없는 옷)부터 하나씩 입고, 하나 입을 때마다 그 옷을 기다리던 다른 옷들이 새로 입을 수 있는 상태가 됩니다. 만약 "A를 입어야 B를 입고, B를 입어야 A를 입는다"면 영원히 시작할 수 없습니다.

위상정렬은 앞에 걸린 조건이 없는 작업부터 꺼내고, 꺼낼 때마다 그 작업을 기다리던 일들의 대기 개수를 하나씩 줄여 가며 전체 순서를 만드는 방법입니다.

  • 대기 개수 0인 옷: 지금 입을 수 있음 → 꺼낸다
  • 옷 하나를 입음: 그 옷을 기다리던 옷들의 대기 개수 -1
  • 아무도 대기 개수가 0이 되지 않는데 남은 옷이 있다: 서로 기다리는 상태(사이클) — 순서가 없다

작업 4개와 선행 관계 3개를 손으로 따라가 봅니다. 0 → 1, 2 → 1, 1 → 3 (화살표는 "먼저 → 나중").

시작할 때 각 작업의 진입 차수(자기 앞에 걸린 선행 작업의 수)는 [0, 2, 0, 1]입니다. 0번과 2번은 앞에 아무것도 없으므로 대기열에서 출발합니다.

단계 대기열 꺼낸 작업 진입 차수 [0,1,2,3] 확정된 순서
시작 [0, 2] — [0, 2, 0, 1] []
1 [2] 0 [0, 1, 0, 1] (1의 대기 2→1) [0]
2 [1] 2 [0, 0, 0, 1] (1이 0이 되어 대기열 진입) [0, 2]
3 [3] 1 [0, 0, 0, 0] (3이 0이 되어 진입) [0, 2, 1]
끝 [] 3 [0, 0, 0, 0] [0, 2, 1, 3]

작업 4개가 모두 결과에 담겼으므로 사이클이 없고, [0, 2, 1, 3]은 유효한 실행 순서입니다. 만약 3 → 0 간선이 하나 더 있었다면 처음부터 진입 차수 0인 작업이 없어 결과가 비고, 그것이 곧 "불가능" 판정입니다.

표준 구현은 Kahn's algorithm (BFS 기반)입니다. 위 표의 과정을 그대로 코드로 옮깁니다.

  1. 진입 차수를 셉니다 — 화살표가 도착하는 쪽에 +1 합니다.
for (const [from, to] of edges) {
  adj[from].push(to);
  indeg[to]++;
}
  1. 진입 차수 0인 작업을 전부 대기열에 넣습니다 — 시작점이 여럿일 수 있고, 간선이 하나도 없는 고립 노드도 여기 포함됩니다.
for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i);
  1. 하나 꺼내 확정하고, 그 작업이 가리키던 이웃들의 대기 개수를 줄입니다. 0이 되면 새로 대기열에 넣습니다.
const cur = queue[head++];
order.push(cur);
for (const next of adj[cur]) {
  if (--indeg[next] === 0) queue.push(next);
}
  1. 마지막에 개수를 확인합니다 — 전부 담기지 못했다면 남은 작업들이 서로를 기다리는 사이클입니다.
return order.length === n ? order : [];

전체 코드:

function topoSort(n: number, edges: number[][]): number[] {
  const adj: number[][] = Array.from({ length: n }, () => []);
  const indeg = Array(n).fill(0);
  for (const [from, to] of edges) {
    adj[from].push(to);
    indeg[to]++;                          // ① 진입 차수 세기
  }
  const queue: number[] = [];
  let head = 0;
  for (let i = 0; i < n; i++) if (indeg[i] === 0) queue.push(i); // ② 시작점
  const order: number[] = [];
  while (head < queue.length) {
    const cur = queue[head++];
    order.push(cur);                      // ③ 확정
    for (const next of adj[cur]) {
      if (--indeg[next] === 0) queue.push(next); // ④ 선행 해소 → 진입
    }
  }
  return order.length === n ? order : []; // ⑤ 전부 못 담으면 사이클
}

⑤가 중요합니다: order.length < n이면 사이클 존재 — "불가능" 판정 문제의 정답 조건입니다.

문제 지문에 "선수 과목", "먼저 완료해야", "의존 관계", "빌드 순서" 같은 말이 나오면 위상정렬입니다. 격자가 아닌 관계 그래프 + 순서라는 조합이 신호입니다.

  • 작업 목록과 "A는 B보다 먼저"라는 쌍들이 입력으로 주어진다 → 그대로 간선입니다.
  • "가능한 순서를 아무거나 출력하라", "불가능하면 -1" → 사이클 판정까지 요구하는 전형입니다.
  • 대표 상황: 강의 수강 계획(선수 과목), 패키지 설치 순서(의존성), 공정 라인의 작업 스케줄링.
  • 가능한 순서 하나 출력 — 위 코드 그대로
  • 최소 소요 시간 (병렬 수행 가능): 레벨 단위 BFS로 층 수를 세거나, time[next] = max(time[next], time[cur] + cost[next])
  • 사전순으로 가장 빠른 순서: 큐 대신 최소 힙 사용
  • 진입 차수가 0인 노드가 동시에 여럿 = 그 작업들은 병렬 가능
// 최소 소요 시간 — ③④ 자리에서 누적 시간을 함께 갱신
const time = Array(n).fill(0);
for (const next of adj[cur]) {
  time[next] = Math.max(time[next], time[cur] + cost[next]);
  if (--indeg[next] === 0) queue.push(next);
}
  • 진입 차수를 indeg[from]++로 반대로 셈 — 화살표의 도착지에 +1
indeg[from]++;  // 잘못 — 출발지를 세면 시작점이 사라집니다
indeg[to]++;    // 올바름 — 나를 막고 있는 선행 작업의 수
  • ⑤ 사이클 검사 생략 — "불가능한 경우 빈 배열/-1" 요구를 놓침
  • shift()로 큐 구현 — head 인덱스로 (큐 토픽 참고)
const cur = queue.shift()!;   // 매번 O(N) — 노드가 많으면 시간 초과
const cur = queue[head++];    // O(1)
  • 간선이 없는 고립 노드 누락 — 진입 차수 0이므로 처음부터 큐에 들어가야 함
  • 복잡도: 각 노드를 정확히 한 번 꺼내고 각 간선을 정확히 한 번 훑으므로 O(V + E)입니다. 인접 리스트를 쓰는 한 노드 수가 아무리 많아도 간선 수에 비례하는 비용만 듭니다.
  • DFS 기반 구현: 각 노드를 깊이 우선으로 방문하고, 되돌아 나오는 순간(후위) 스택에 담은 뒤 마지막에 뒤집으면 위상 순서가 됩니다. 이때 사이클은 "방문 중" 표시가 붙은 노드를 다시 만나는 것으로 감지합니다. Kahn 방식보다 상태 관리가 까다로워 코테에서는 BFS 쪽이 안전합니다.
  • 순서의 유일성 판정: 매 단계에서 대기열 크기가 항상 1이었다면 가능한 순서가 딱 하나뿐이라는 뜻입니다. "순서가 유일하게 결정되는가"를 묻는 변형은 이 조건을 확인합니다.
  • 사전순 최소 순서의 비용: 최소 힙을 쓰면 꺼내고 넣는 데 log가 붙어 O(V log V + E)가 됩니다. 사전순 요구가 없다면 굳이 힙을 쓰지 않습니다.
  • DAG 위의 DP: 위상 순서대로 훑으면 "이미 앞의 값이 확정된 상태"가 보장되므로, 사이클 없는 그래프의 최장 경로·경로 개수 같은 DP를 한 번의 순회로 계산할 수 있습니다. 최소 소요 시간 변형이 바로 이 형태입니다.

시각화로 "진입 차수 0 → 처리 → 잠금 해제"의 흐름을 본 뒤, 기본 순서 출력 → 최소 시간 → 사이클 판정 순으로 푸세요. BFS(큐)가 선수 지식입니다.

3

문제로 확인

난이도 순서대로 2문제 — 막히면 개념으로 돌아왔다 다시
1레시피 최소 완성 시간#106다음 풀 문제중간2배포 파이프라인 최소 단계#096어려움
← 이전 토픽 · Lv.3DP (동적 계획법)