개념

선후 관계를 지키는 순서

위상 정렬은 방향 간선 u→vu \to v마다 uu가 vv보다 앞에 오도록 정점을 나열하는 방법입니다. 먼저 끝내야 하는 작업이나 먼저 수강해야 하는 과목이 정해져 있을 때 사용합니다. DAG에서 위상 정렬이 가능하며, 조건을 만족하는 순서는 여러 개일 수 있습니다.

진입 차수를 이용한 처리

진입 차수는 그 정점으로 들어오는 간선의 수입니다. 작업 관계에서는 아직 처리해야 할 선행 작업의 개수에 해당합니다. 진입 차수가 00인 정점을 큐에 넣고, 하나씩 꺼내며 나가는 간선을 제거합니다. 간선이 제거되어 진입 차수가 00이 된 이웃을 큐에 추가합니다.

아래 그래프에는 정점이 99개 있습니다. 진한 정점은 큐에 들어 있는 처리 후보입니다. 아래 표는 남은 진입 차수이고, 점선은 처리하면서 제거한 간선입니다. 처음에는 들어오는 간선이 없는 1,21,2를 큐에 넣습니다.

1 2 3 4 5 6 7 8 9 진입 차수 1 0 2 0 3 1 4 2 5 1 6 2 7 2 8 2 9 2 큐 1 2
▲ [그림 1] 처음 처리할 수 있는 정점 1과 2

11을 처리하면 1→31\to3, 1→41\to4를 제거합니다. 33의 진입 차수는 00이 되어 큐에 들어가지만, 44에는 2→42\to4가 남아 있으므로 진입 차수가 11입니다. 큐는 앞에서부터 2,32,3입니다.

1 2 3 4 5 6 7 8 9 진입 차수 1 — 2 0 3 0 4 1 5 1 6 2 7 2 8 2 9 2 큐 2 3
▲ [그림 2] 한 선행 정점만 처리한 상태

22를 처리하면 2→42\to4, 2→52\to5도 제거합니다. 4,54,5의 진입 차수가 모두 00이 되어 큐는 3,4,53,4,5가 됩니다. 이후 같은 과정을 반복하면 1,2,3,4,5,6,7,8,91,2,3,4,5,6,7,8,9 순서로 모든 정점을 처리할 수 있습니다.

1 2 3 4 5 6 7 8 9 진입 차수 1 — 2 — 3 0 4 0 5 0 6 2 7 2 8 2 9 2 큐 3 4 5
▲ [그림 3] 선행 조건을 모두 만족한 정점을 큐에 추가

후보를 일반 큐에 넣으면 추가된 순서로 처리합니다. 후보 중 번호가 가장 작은 정점을 먼저 선택하려면 최소 우선순위 큐를 사용합니다.

처리한 정점 수로 방향 사이클 판별하기

진입 차수로 위상 정렬을 수행하면 방향 그래프의 사이클 유무도 판별할 수 있습니다. 큐가 빌 때까지 처리한 정점 수를 CC, 전체 정점 수를 VV라고 하면 다음과 같습니다.

처리 결과판정
C=VC=V모든 정점을 처리했으므로 방향 사이클이 없습니다.
C<VC<V처리할 수 없는 정점이 남았으므로 방향 사이클이 있습니다.

사이클 안의 각 정점에는 사이클의 이전 정점에서 들어오는 간선이 있습니다. 그 이전 정점을 먼저 처리해야 간선을 제거할 수 있지만, 모든 정점이 같은 조건으로 서로를 기다리므로 진입 차수가 00이 되지 않습니다.

앞의 그래프에 8→48\to4를 추가하겠습니다. 진한 간선 4→6→8→44\to6\to8\to4가 방향 사이클을 이룹니다. 그래도 1,21,2의 진입 차수는 00이므로 일부 정점은 처리할 수 있습니다.

1 2 3 4 5 6 7 8 9 진입 차수 1 0 2 0 3 1 4 3 5 1 6 2 7 2 8 2 9 2 큐 1 2
▲ [그림 4] 일부 정점은 처리할 수 있지만 사이클이 있는 그래프

1,2,3,51,2,3,5를 처리한 뒤에는 큐가 비게 됩니다. 44는 88을, 66은 44를 기다립니다. 88도 6,76,7이 처리되기를 기다리므로 남은 간선을 제거할 시작점이 없습니다. 처리한 정점 수는 44이고 전체는 99이므로 사이클이 있다고 판정합니다.

1 2 3 4 5 6 7 8 9 진입 차수 1 — 2 — 3 — 4 1 5 — 6 1 7 1 8 2 9 1 큐 비어 있음
▲ [그림 5] 4개만 처리하고 멈춘 상태: 4 < 9이므로 사이클 존재

처리되지 않은 정점이 모두 사이클에 속하는 것은 아닙니다. 그림의 99는 사이클에 속하지 않지만, 처리되지 않은 77에서 간선이 들어오므로 함께 남습니다. 이 방법으로는 사이클의 존재를 판별하며, 사이클을 이루는 정점 목록을 바로 얻는 것은 아닙니다.

반대로 사이클이 없는 유한 방향 그래프에는 진입 차수가 00인 정점이 존재합니다. 그런 정점이 없다고 가정하고 선행 정점을 계속 따라가면 결국 같은 정점을 다시 만나 사이클이 생기기 때문입니다. 따라서 사이클이 없다면 정점을 제거한 뒤에도 다음 후보가 존재하여 모든 정점을 처리할 수 있습니다.

복잡도

정점 수를 VV, 간선 수를 EE라고 합니다.

연산시간복잡도공간복잡도
일반 큐 위상 정렬O(V+E)O(V+E)그래프 O(V+E)O(V+E), 추가 O(V)O(V)
최소 우선순위 큐 위상 정렬O(E+Vlog⁡V)O(E+V\log V)그래프 O(V+E)O(V+E), 추가 O(V)O(V)

예제

문제

각 작업의 선행 관계가 주어진다. 가능한 작업 중 번호가 가장 작은 작업을 매번 선택하여 전체 작업 순서를 구하여라.

입력

첫째 줄에 작업 수 NN과 선행 관계 수 MM이 공백을 사이에 두고 주어진다. 1≤N≤1001\le N\le100; 0≤M≤2000\le M\le200이다.

다음 MM개 줄에 작업 번호 a,ba,b가 공백을 사이에 두고 주어진다. 1≤a,b≤N1\le a,b\le N이다. 작업 bb를 시작하려면 작업 aa를 먼저 마쳐야 한다.

출력

모든 작업을 수행할 수 있으면 작업 번호를 수행 순서대로 한 줄에 공백을 사이에 두고 출력한다. 수행할 수 없으면 IMPOSSIBLE을 출력한다.

예제 입력

5 4
1 3
2 3
3 5
2 4

예제 출력

1 2 3 4 5

예제 코드

C++20

#include <iostream>
#include <vector>
#include <queue>
#include <functional>

using namespace std;

vector<vector<int>> g;
vector<int> d, ans;

int main() {
    int n, m, i, a, b, u;
    priority_queue<int, vector<int>, greater<int>> pq;

    cin >> n >> m;
    g.resize(n + 1);
    d.assign(n + 1, 0);
    for (i = 0; i < m; ++i) {
        cin >> a >> b;
        g[a].push_back(b);
        // d[v]: 아직 처리하지 않은 진입 간선의 수
        ++d[b];
    }
    for (i = 1; i <= n; ++i) {
        // 선행 조건이 없는 정점을 넣고 작은 번호부터 선택
        if (d[i] == 0) pq.push(i);
    }

    while (!pq.empty()) {
        u = pq.top();
        pq.pop();
        ans.push_back(u);
        for (int v : g[u]) {
            // 현재 정점에서 나가는 간선을 제거하여 다음 정점의 진입 차수 감소
            --d[v];
            if (d[v] == 0) pq.push(v);
        }
    }

    // 모든 정점을 처리하지 못했으면 방향 사이클 존재
    if ((int)ans.size() != n) cout << "IMPOSSIBLE\n";
    else {
        for (int v : ans) {
            cout << v << ' ';
        }
        cout << '\n';
    }
}