본문 바로가기

알고리즘 & 자료구조

Topological Sort (위상 정렬)

안녕하세요.

Topological Sort (위상 정렬)에 대해 알아보도록 하겠습니다.


간선의 방향에 따라 노드를 정렬

위상 정렬은 DAG (Directed Acyclic Graph) 에서 작동하는 알고리즘 입니다.

Directed : 간선에 방향성이 있음
Acyclic : 순환 경로가 없는 그래프
=> DAG란, 간선에 방향성이 있고 사이클이 존재하지 않는 그래프

[그림 1] DAG 예시

 

노드를 간선의 방향을 거스르지 않고 정렬하는 알고리즘 입니다.

간선 (u -> v)가 있다고 가정하면, 노드를 정렬했을 때 u가 v보다 먼저 나타나야 합니다.

 

위의 [그림 1]에서 위상 정렬을 하면 아래와 같이 나올 수 있습니다.

(위상 정렬은 정답이 여러 개가 나올 수 있습니다.)

  • 1 -> 2 -> 3 -> 4 -> 5
  • 2 -> 1 -> 3 -> 4 -> 5

 

아래 정렬 결과로는 나오지 않습니다.

  • 1 -> 2 -> 3 -> 5 -> 4
  • 간선 (4 -> 5) 가 존재하기 때문에 4는 5보다 먼저 나타나야 합니다.

방법

위상 정렬을 구하는 방법으로는 2가지가 있습니다.

  • 큐와 BFS를 사용하는 방법 1
  • 스택과 DFS를 사용하는 방법 2

두 가지 방법 모두 알아보도록 하겠습니다.


1) 큐와 BFS를 사용

이 방법은 진입 차수를 사용합니다.

진입 차수 (InDegree) 란?
노드로 들어오는 간선의 개수를 의미

[그림 2] 모든 노드에 대한 진입 차수

 

  1. 진입 차수가 0인 모든 노드를 큐에 넣습니다.
  2. 큐가 빌 때 까지 아래 두 과정을 반복합니다.
    1. 큐에서 원소를 꺼내고 해당 노드에서 나가는 간선을 제거합니다.
    2. 진입 차수가 0이 된 노드가 있다면 큐에 넣습니다.

 

그림과 함께 자세히 알아보겠습니다.

 

과정

[그림 3] 진입 차수가 0인 노드를 큐에 넣기

진입 차수가 0인 노드를 전부 큐에 담습니다.

그러면 위의 그림처럼 큐에는 1번과 2번 노드가 담겨있게 됩니다.

 

이 후에는 큐에 들어온 순서대로 해당 노드에서 나가는 간선을 제거시켜주면 됩니다.

(아래 그림에서는 간선을 지웠지만, 실제 코드에서는 inDegree 값만 1만큼 감소시켜주면 됩니다.)


[그림 4] 1번 노드와 연결된 간선 지우기

1번 노드와 연결된 간선을 지우겠습니다.

간선 (1 -> 3) 이 존재하고 이 간선을 지우게 되면, 3의 InDegree 값이 1만큼 감소해서 2 -> 1로 값이 변하게 됩니다.

InDegree[3]의 값은 0이 아니라서 큐에 집어 넣지 않고, 다음 단계로 넘어갑니다.


[그림 5] 2번 노드와 연결된 간선 지우기

간선 (2 -> 3) 이 존재하고 이 간선을 지우면 3의 InDegree 값이 1 -> 0 으로 값이 변하게 됩니다.

InDegree[3]의 값이 0이라서 큐에 집어 넣습니다.


[그림 6] 3번 노드와 연결된 간선 지우기

간선 (3 -> 4), 간선 (3 -> 5) 를 지웁니다.

4와 5의 InDegree 값이 각각 1씩 감소합니다.

InDegree[4]의 값이 0이라서 4를 큐에 집어 넣습니다.


[그림 7] 4번 노드와 연결된 간선 지우기

간선 (4 -> 5) 의 간선이 제거됩니다.

5의 InDegree 값이 0으로 감소하고 큐에 5를 집어 넣습니다.


[그림 8] 5번 노드와 연결된 간선 지우기

5번 노드와 연결된 간선을 제거합니다.

더 이상 간선이 존재하지 않고, 모든 노드를 살펴보았으므로 위상 정렬을 종료합니다.


위상 정렬 순서는 큐에 담긴 순서입니다.

즉, 1 -> 2 -> 3 -> 4 -> 5 가 됩니다.

 

시간 복잡도

이 알고리즘의 시간 복잡도는 O(V + E) 입니다.

  • V : 노드의 개수
  • E : 간선의 개수

모든 노드를 하나씩만 확인하기에 O(V) 만큼 걸립니다.

이와 동시에 모든 간선 또한 최대 한 번 확인하기 때문에 O(E) 만큼 걸립니다.

 

코드

struct TopologicalSort {
	int inDegree[SIZE];

	void putInDegree(int node) {
		++inDegree[node];
	}

	void solve(int N) {
		for (int i = 1; i <= N; i++) {
			if (!inDegree[i]) {
				queue.enQueue(i);
			}
		}

		while (!queue.empty()) {
			int top = queue.deQueue();
			printf("%d ", top);

			for (int i = 0; i < adj[top].getSize(); i++) {
				int next = adj[top][i];
				if (!--inDegree[next]) {
					queue.enQueue(next);
				}
			}
		}
	}
};

 

사이클이 있을 땐?

위상 정렬은 사이클이 없는 DAG 그래프에서 사용할 수 있다고 위에서 언급했습니다.

그래프에 사이클이 있으면 어떻게 되는 걸까요?

 

사이클이 있는 그래프에서 위상 정렬을 수행해보겠습니다.

[그림 9] 사이클이 있는 그래프에서 2번 노드까지 위상 정렬을 수행한 모습

 

위에서 설명한 방식대로 처음에 InDegree가 0인 노드를 찾아서 큐에 넣어주고 시작합니다.

1번 노드, 2번 노드까지 인접한 간선을 제거해보았습니다.

 

이미 큐에 담겨있는 노드를 제외하고는 inDegree가 0인 노드가 존재하지 않게 됩니다.

그래서 큐에 담기지 않고, 모든 노드를 확인 못한 뒤에 알고리즘이 종료됩니다.


이 방법을 사용하면 그래프에 사이클이 있는지 여부를 쉽게 확인할 수 있습니다.

 

큐에 들어온 노드의 수 ≠ 노드의 개수 => 사이클이 발생

 

위의 [그림 9]를 보면 이해가 쉽습니다.

 

큐에 들어온 노드의 수 = 2

총 노드의 수 = 5

 

두 숫자가 다르기에 사이클이 있다고 판단할 수 있습니다.


스택과 DFS를 사용

이번엔 진출 차수를 이용합니다.

진출 차수란?
노드에서 나가는 간선의 개수
[그림 10] 모든 노드에 대한 진출 차수

 

모든 노드가 방문될 때 까지 아래 두 과정을 반복합니다.

  1. 임의의 한 노드를 잡고 DFS를 수행합니다.
  2. 더 이상 방문할 노드가 존재하지 않는 노드는 스택에 넣습니다.

더 이상 방문할 노드가 존재하지 않는 것을 outDegree 값이 0인지 여부로 확인할 수 있습니다.

하지만 실제로 구현할 때는 outDegree 값을 따로 저장할 필요가 없습니다.

 

dfs 함수를 수행하면서 현재 노드와 연결된 간선을 전부 확인해봅니다.

연결된 간선을 전부 다 봤다면 해당 노드는 진출 차수가 0이 됩니다.

(간선을 봄과 동시에 해당 간선을 삭제하기 때문입니다.)

 

즉, dfs 함수 마지막 부분에 stack에 현재 노드를 넣어주면 됩니다.

 

그림과 함께 자세히 알아보겠습니다.

 

과정

[그림 11] 1번 노드부터 dfs 시작

임의의 노드 아무거나 두고 dfs를 수행합니다.

저는 1번 노드를 선택했습니다.


[그림 12] 더 이상 간선이 없을 때까지 dfs(1)을 수행한 모습

dfs(1)을 수행하게 되면 연결된 간선을 따라서 dfs(3), dfs(4), dfs(5)를 수행합니다.


[그림 13] 스택에 노드 넣기

더 이상 방문할 노드가 없는 노드부터 스택에 집어 넣습니다.

즉, dfs의 방문 순서 반대로 스택에 집어넣어 주면 됩니다.


[그림 14] dfs(2)를 수행하고 스택에 담긴 모습

이 후에 남은 정점도 dfs를 수행해줍니다.

2번 정점만 남았기에 dfs(2)를 시작합니다.

 

같은 정점은 더 이상 방문하지 않아도 되기에 dfs(2)만 수행하고 스택에 2번 노드를 담고 종료했습니다.


위상 정렬 순서는 스택에서 pop 한 순서입니다.

2 -> 1 -> 3 -> 4 -> 5


정당성 증명

위의 방법을 귀류법으로 간단하게 증명할 수 있습니다.

 

[그림 15] 위상 정렬 순서대로 나열한 그림

 

위의 그림에서 위상 정렬 순서에 반대되는 빨간색 간선 (5 -> 3) 을 그래프에서 추가해 보겠습니다.

 

dfs(5)에서는 이 함수가 종료되기 전에 간선 (5 -> 3)도 검사합니다.

 

그럼 visited[3]의 값에 따라서 케이스를 나눌 수 있습니다. (visited[3] : 3번 노드가 dfs를 수행했는지 여부)

  • visited[3]이 거짓이면,
    • dfs(5)는 dfs(3)을 호출하게 됩니다.
    • dfs(3)이 종료된 후에야 dfs(5)가 종료됨으로 이 경우 3은 5보다 왼쪽에 있을 수 없습니다.
  • visited[3]이 참이면,
    • dfs(3)은 이미 dfs를 한 번 실행되었다는 소리입니다.
    • 그런데도 dfs(3)이 dfs(5) 보다 늦게 끝났다는 것은 dfs(3)은 현재 스택에 담겨있지 않은, 아직 dfs(3) 함수가 종료되지 않았다는 의미입니다.
    • dfs(3) -> ... -> dfs(5) -> ... -> dfs(3)
    • 이 경우는 사이클이 되므로 DAG가 아닙니다. 즉, 위상 정렬이 불가능한 조건입니다.

그래서 dfs를 활용한 알고리즘이 정당합니다.

 

시간 복잡도

이 알고리즘 역시 시간 복잡도는 O(V + E) 입니다.

모든 정점을 한 번만 보기에 O(V) 만큼의 시간이 걸리고,

모든 간선을 한 번만 보기에 O(E) 만큼의 시간이 걸립니다.

 

코드

struct TopologicalSort {
	bool visit[SIZE];

	void dfs(int cur) {
		visit[cur] = true;

		for (int i = 0; i < adj[cur].getSize(); i++) {
			int next = adj[cur][i];
			if (!visit[next]) {
				dfs(next);
			}
		}

		stack.push(cur);
	}

	void solve(int N) {
		stack.init();

		for (int i = 1; i <= N; i++) {
			if (!visit[i]) dfs(i);
		}

		while (!stack.empty()) {
			int top = stack.pop();
			printf("%d ", top);
		}
	}
};

 

사이클이 있을 땐?

아까 위에서 귀류법으로 증명을 할 때 사이클에 관해서 얘기를 했습니다.

dfs를 활용한 알고리즘은 SCC 알고리즘을 사용해 사이클을 판별할 수 있습니다.

 

SCC 알고리즘으로 사이클을 가지는 정점 집합을 추출한 뒤 하나의 노드로 묶어서, 위상 정렬을 수행하면 됩니다.

이에 대한 자세한 설명이 궁금하시다면, 제가 이전에 작성했던 네이버 블로그를 확인해주세요.

https://blog.naver.com/kdr06006/222822604292

 

강한 연결 요소 (Strongly Connected Component)

안녕하세요. 오늘은 강한 연결 요소 (Strongly Connected Component)에 대해서 알아보겠습니다. 줄여서 ...

blog.naver.com


두 방법 비교

[그림 16] 두 가지 방법을 백준에 제출했을 때의 결과

위의 방법이 스택을 사용한 방식,

아래 방법이 큐를 사용한 방식입니다.

 

시간과 메모리 차이가 그렇게 많이 나지 않는 것을 확인할 수 있습니다.

  • 메모리 차이가 나는 이유는
    큐를 사용한 방법은 InDegree 라는 int 배열을 사용한 것에 반해,
    스택을 사용한 방법은 visit 배열의 boolean 배열만으로 충분했습니다.

    그래서 스택을 사용한 방법이 메모리가 더 적게 드는 것을 확인할 수 있습니다.

문제

기본 : https://www.acmicpc.net/problem/2252

 

2252번: 줄 세우기

첫째 줄에 N(1 ≤ N ≤ 32,000), M(1 ≤ M ≤ 100,000)이 주어진다. M은 키를 비교한 회수이다. 다음 M개의 줄에는 키를 비교한 두 학생의 번호 A, B가 주어진다. 이는 학생 A가 학생 B의 앞에 서야 한다는 의

www.acmicpc.net

응용 : https://www.acmicpc.net/problem/1948

 

1948번: 임계경로

첫째 줄에 도시의 개수 n(1 ≤ n ≤ 10,000)이 주어지고 둘째 줄에는 도로의 개수 m(1 ≤ m ≤ 100,000)이 주어진다. 그리고 셋째 줄부터 m+2줄까지 다음과 같은 도로의 정보가 주어진다. 처음에는 도로의

www.acmicpc.net


참고 자료

위상정렬 설명 : https://www.youtube.com/watch?v=HxHISRelNuM

큐를 이용한 방법 : https://ryu-e.tistory.com/100

스택을 이용한 방법 : https://sorjfkrh5078.tistory.com/36

SCC : https://blog.naver.com/kdr06006/222822604292

 

감사합니다.

'알고리즘 & 자료구조' 카테고리의 다른 글

2-SAT  (0) 2023.07.24
강한 연결 요소 (Strongly Connected Component)  (0) 2023.07.20
최소 스패닝 트리 (Minimun Spanning Tree)  (0) 2023.07.14
A* algorithm  (0) 2023.07.07
Disjoint-set  (0) 2023.06.27