위상 정렬 (Topology Sort) 알고리즘

반응형
728x90
반응형

위상 정렬 알고리즘

정렬 알고리즘의 일종이다. 위상 정렬이란, 방향 그래프의 모든 노드를 방향성에 거스르지 않도록 순서대로 나열하는 것이다. 예시의 상황을 보자.

 

  • 예시 

자바 공부의 커리큘럼에는 순서가 있다.

1) JAVA

2) JSP

3) Spring

 

이때 우리는 각 과목을 노드로 표현하고, 'JAVA' -> 'JSP'로 이어질 수 있도록 방향성을 갖는 간선을 그릴 수 있다. 다시 말해 그래프 상에서 선후 관계가 있다면, 위상 정렬을 수행하여 모든 선후 관계를 지키는 전체 순서를 계산할 수 있다.

 

  • 책에서의 예시

1) 자료구조

2) 알고리즘

3) 고급 알고리즘

 

위 3가지로 구성되어있는 과목 커리큘럼이 있다. '알고리즘'의 선수 과목으로 '자료구조'가 있고, '고급 알고리즘'의 선수 과목으로 '자료구조', '알고리즘'이 있다. 이 경우에는 '자료구조 -> 알고리즘 -> 고급 알고리즘'의 순서로 강의를 수강해야한다. 그래야 모든 과목 수강이 가능하다.

 

https://freedeveloper.tistory.com/390

 

 

 

 

위상 정렬 알고리즘 구성

  • 진입차수 (Indegree)

특정한 노드로 들어오는 간선의 개수

 

  • 진출차수 (Outdegree)

특정한 노드에서 나가는 간선의 개수

 

https://freedeveloper.tistory.com/390

 

1) 진입차수가 0인 노드를 큐에 넣는다.

2) 큐가 빌때까지 다음의 과정을 반복한다.

 - 큐에서 원소를 꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거한다.

 - 새롭게 진입차수가 0이 된 노드를 큐에 넣는다.

 

결과적으로, 각 노드가 큐에 들어온 순서와 위상 정렬을 수행한 결과와 같다.

 

 

 

위상 정렬 알고리즘의 동작 원리

- 사이클이 없는 방향 그래프다.

사이클이 존재하는 경우, 사이클에 포함되어있는 원소 중에서 어떠한 원소도 큐에 들어가지 못하기 때문에 이렇게 가정한다. 참고로 위상정렬 문제에서는 사이클이 발생하지 않는다고 명시하는 경우가 더 많다.

 

https://freedeveloper.tistory.com/390

 

1) 진입 차수가 0인 노드는 1번 노드이다.

https://freedeveloper.tistory.com/390

 

 

2) 큐에서 노드 1을 꺼낸뒤, 노드 1에서 나가는 간선을 제거한다. 이때 진입차수가 0이되는 2번 노드, 5번 노드가 큐에 삽입된다.

https://freedeveloper.tistory.com/390

 

 

3) 큐에서 노드 2를 꺼낸 후 나가는 간선을 제거한다. 이때 진입차수가 0이 되는 3번 노드가 큐에 삽입된다.

https://freedeveloper.tistory.com/390

 

 

4) 큐에서 노드 5를 꺼낸 후 나가는 간선을 제거한다. 이때 진입차수가 0이 되는 6번 노드가 큐에 삽입된다.

https://freedeveloper.tistory.com/390

 

 

5) 큐에서 노드 3를 꺼낸 후 나가는 간선을 제거한다. 이때 진입차수가 0이 되는 새로운 노드는 없다.

https://freedeveloper.tistory.com/390

 

 

6) 큐에서 노드 6를 꺼낸 후 나가는 간선을 제거한다. 이때 진입차수가 0이 되는 4번 노드가 큐에 삽입된다.

https://freedeveloper.tistory.com/390

 

 

7) 큐에서 노드 4를 꺼낸 후 나가는 간선을 제거한다. 이때 진입차수가 0이 되는 7번 노드가 큐에 삽입된다.

https://freedeveloper.tistory.com/390

 

8) 큐에서 노드 7를 꺼낸 후 나가는 간선을 제거한다. 더이상 큐에 들어갈 노드는 없다.

https://freedeveloper.tistory.com/390

 

 

9) 결과

위에서 각 노드가 큐에 들어온 순서와 위상 정렬을 수행한 결과와 같다고 했다. 결국 위상 정렬의 결과는 아래와 같다.

1 -> 2-> 5-> 3-> 6-> 4-> 7

 

 

 

 

코드 분석

1) 필요한 변수를 선언하고 입력받은 데이터를 각 변수에 셋팅한다.

indegree 배열이 진입차수를 담을 객체다. 큐에 들어간 노드를 꺼냈을때 해당 노드와 연결되어있는 간선이 제거되면 진입차수를 계산하여 해당 배열을 갱신해줘야한다.

 

package Y20_Graph;

import java.util.*;

public class TopologySort {
    public static int v; // 노드의 개수(V)
    public static int e; // 간선(Union 연산)의 개수(E)

    // 노드의 개수는 최대 100,000개라고 가정
    // 모든 노드에 대한 진입차수는 0으로 초기화
    public static int[] indegree = new int[100001];

    // 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트 초기화
    public static ArrayList<ArrayList<Integer>> graph = new ArrayList<ArrayList<Integer>>();

    // 위상 정렬 함수
    public static void topologySort() {
       ...
    }

    /*
     7 8
     1 2
     1 5
     2 3
     2 6
     3 4
     4 9
     5 6
     6 4
     */
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        v = sc.nextInt();
        e = sc.nextInt();

        // 그래프 초기화
        for (int i = 0; i <= v; i++) {
            graph.add(new ArrayList<Integer>());
        }

        // 방향 그래프의 모든 간선 정보를 입력 받기
        for (int i = 0; i < e; i++) {
            int a = sc.nextInt();
            int b = sc.nextInt();

            graph.get(a).add(b); // 정점 A에서 B로 이동 가능

            // 진입 차수를 1 증가
            indegree[b] += 1;
        }

        topologySort();
    }
}

 

2) topologySort() 메서드를 분석해보자.

 

2-1) 결과를 담을 리스트 result와 큐 q 를 선언한다.

...
    // 위상 정렬 함수
    public static void topologySort() {
        ArrayList<Integer> result = new ArrayList<>(); // 알고리즘 수행 결과를 담을 리스트
        Queue<Integer> q = new LinkedList<>(); // 큐 라이브러리 사용

        ...
    }
...

 

2-2) 진입차수가 0인 노드를 큐에 삽입한다.

...
    // 위상 정렬 함수
    public static void topologySort() {
        ArrayList<Integer> result = new ArrayList<>(); // 알고리즘 수행 결과를 담을 리스트
        Queue<Integer> q = new LinkedList<>(); // 큐 라이브러리 사용

        // 처음 시작할 때는 진입차수가 0인 노드를 큐에 삽입
        for (int i = 1; i <= v; i++) {
            if (indegree[i] == 0) {
                q.offer(i);
            }
        }
    }
...

 

 

2-3) 큐가 빌때까지 반복한다.

반복문 안에서는 큐에서 원소를 꺼내고, 해당 원소와 연결된 노드들의 진입차수를 -1 한다.

(나가는 간선들을 제거하는 행위)

제거 후, 새롭게 0이 되는 노드를 큐에 삽입한다. 결과적으로 큐에 삽입된 순서인 result 리스트를 출력한다.

...
    // 위상 정렬 함수
    public static void topologySort() {
        ...

        // 큐가 빌 때까지 반복
        while (!q.isEmpty()) {
            // 큐에서 원소 꺼내기
            int now = q.poll();

            result.add(now);

            // 해당 원소와 연결된 노드들의 진입차수에서 1 빼기
            for (int i = 0; i < graph.get(now).size(); i++) { // 연결된 노드만큼 반복
                indegree[graph.get(now).get(i)] -= 1;

                // 새롭게 진입차수가 0이 되는 노드를 큐에 삽입
                if (indegree[graph.get(now).get(i)] == 0) {
                    q.offer(graph.get(now).get(i));
                }
            }
        }

        // 위상 정렬을 수행한 결과 출력
        result.stream()
                .map(integer -> integer + " ")
                .forEach(System.out::print);
    }
...

 

 

 

위상 정렬의 특징

1) 위상 정렬은 DAG(Direct Acyclic Graph) 인 순환하지 않는 방향 그래프의 경우에 가능하다.

 

2) 여러가지 답이 존재할 수 있다. 큐에 들어갈 노드(원소)가 여러개일 경우를 말한다.

 

3) 모든 원소를 방문하기 전에 큐가 빈다면 사이클이 존재한다고 판단한다.

-> 따라서 while()문은 큐가 빌때까지 반복한다.

 

4) 스택을 활용한 DFS를 이용해 위상 정렬을 수행할 수 있다.

 

 

 

Reference

https://freedeveloper.tistory.com/390

 

 

'이것이 코딩테스트다' 교재 참고

 

 

 

반응형

Designed by JB FACTORY