정렬 알고리즘의 일종으로, 순서가 정해져 있는 일련의 작업을 차례대로 수행해야 할 때 사용하는 알고리즘입니다.
어떤 업무들에 선후관계가 있을 때 사용하는 알고리즘으로, 아래 그림과 같은 대학교 강의의 선수후수 과목을 예시로 들 수 있습니다.
위의 그림에 따르면 전자기학 과목을 듣기 위해서는 기초천문학과 미분적분학을 반드시 수강해야합니다.
선수과목들은 후수과목을 듣기 위한 조건이라고도 생각할 수 있습니다. 즉, 남아있는 선수과목이 하나도 없어야 후수과목을 수강할 수 있습니다.
- 진입하는 간선이 없는 노드를 큐에 담는다.
- 큐에서 노드를 꺼내고 노드에 연결된 간선을 없앤다.
- 1번과 2번을 반복한다.
여기서 큐에서 꺼내진 순서가 위상정렬을 수행한 결과가 됩니다.
그림을 통해 과정을 보겠습니다.
노드 | 진출차선이 향하는 노드 |
---|---|
1 | 2, 4 |
2 | 3 |
3 | |
4 | 5 |
5 | 3 |
6 | 4, 7 |
7 | 8 |
8 | 9 |
9 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 1 | 2 | 2 | 1 | 0 | 1 | 1 | 1 |
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 2 | 1 | 1 | 0 | 1 | 1 | 1 |
결과 : 1
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 2 | 0 | 1 | 0 | 0 | 1 | 1 |
결과 : 1, 6
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 1 | 0 | 1 | 0 | 0 | 1 | 1 |
결과 : 1, 6, 2
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 |
결과 : 1, 6, 2, 4
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 1 |
결과 : 1, 6, 2, 4, 7
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
결과 : 1, 6, 2, 4, 7, 5
1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
결과 : 1, 6, 2, 4, 7, 5, 8
결과 : 1, 6, 2, 4, 7, 5, 8, 3
결과 : 1, 6, 2, 4, 7, 5, 8, 3, 9
1 -> 6 -> 2 -> 4 -> 7 -> 5 -> 8 -> 3 -> 9
기초천문학 -> 미분적분학 -> 천체물리학 -> 전자기학 -> 선형대수학 -> 우주환경 -> 미분방정식 -> 우주플라즈마 유체역학 -> 해석학
이 결과를 보고 다른 경우도 답이 될 수 있지 않냐고 물을 수 있습니다. 그 말은 사실입니다.
가령 1 -> 2 -> 6 -> 4 -> 7 -> 8 -> 9 -> 5 -> 3 과 같이 조건만 만족한다면 특별한 규칙없이 낸 결론 또한 답이 될 수도 있습니다.
따라서 해당 알고리즘을 사용해서 코딩테스트 문제를 풀 때는, 문제의 조건을 잘 읽고 어떤 알고리즘을 통해 풀어야 문제에서 원하는 경로대로 결론을 낼 수 있는지 파악할 필요가 있습니다.
이 문제를 풀 때는 큐를 사용하여 풀었지만, 스택이나 힙을 사용해서도 풀 수 있습니다.
마지막으로 위상정렬 알고리즘을 사용하기 위해서는 그래프 내에 사이클이 존재해서는 안됩니다. 사이클이 존재한다면, 이미 지나간 경로를 다시 지나가는 경우가 발생하기 때문에 위상정렬 알고리즘을 사용할 수 없습니다.
입력예시 (알고리즘 설명의 예시와 동일한 예시입니다.)
9 9
1 2
2 3
1 4
4 5
5 3
6 4
6 7
7 8
8 9
queue로 구현했을 때는 앞서 말한대로 너비우선탐색과 비슷한 방식으로 정렬됩니다.
1, 6, 2, 4, 7, 5, 8, 3, 9
n, m = map(int, input().split())
indegree = [0]*(n+1) # 진입차수에 대한 정보를 담은 배열을 생성한다.
# 데이터가 1부터 시작하기 때문에 n+1개를 생성했다.
# 각각의 인덱스는 노드의 숫자에 해당하고 인덱스의 값은 진입차수의 개수를 의미한다.
graph = [[] for _ in range(n+1)] # 각 노드별로 나가는 간선의 위치를 저장한다.
# 각 인덱스는 노드의 숫자에 해당하고 인덱스의 값에 해당하는 배열으로 간선이 이어진다는 것을 의미한다.
# 아래의 반복문을 통해 채워질 것이다.
queue = [] # 진입차수가 없는 노드가 들어갈 queue 자료구조이다.
result = [] # queue에서 빠져나온 순서대로 값이 들어갈 것이며, 이것이 정렬의 결과물이다.
for _ in range(m):
prev, post = map(int, input().split()) # prev는 선행과목, post는 후수과목에 해당한다.
graph[prev].append(post) # graph의 prev인덱스에 해당하는 배열에 post를 추가한다. 즉, prev라는 과목이 수강되어야 post를 수강할 수 있다.
indegree[post]+= 1 # post의 선수과목이 지정되었으므로 진입차수를 1 추가한다.
for i in range(1, n+1): # 처음에는 먼저 진입차수가 0인 노드부터 찾습니다.
if indegree[i] == 0:
queue.append(i)
while queue: # queue가 비게 된다면, 정렬이 모두 끝납니다. 그래프 내에 사이클이 존재한다면, queue 가 비지 않게 되고 무한루프를 돕니다.
value = queue.pop(0) # queue에서 값을 꺼냅니다.
result.append(value) # 결과값으로 저장합니다.
for i in graph[value]: # queue에서 꺼내진 값의 다음 노드로 연결된 노드들을 찾습니다.
indegree[i] -= 1 # queue에서 꺼내진 값에서 나오는 진출간선을 없애줍니다.
if indegree[i] == 0: # 이후 진입차수가 0이 된 노드를 찾고
queue.append(i) # 해당 노드를 queue에 넣고 queue가 빌 때까지 위의 내용들을 반복합니다.
print(result)
stack로 구현했을 때는 앞서 말한대로 깊이우선탐색과 비슷한 방식으로 정렬됩니다.
6, 7, 8, 9, 1, 4, 5, 2, 3
n, m = map(int, input().split())
indegree = [0]*(n+1) # 진입차수에 대한 정보를 담은 배열을 생성한다.
# 데이터가 1부터 시작하기 때문에 n+1개를 생성했다.
# 각각의 인덱스는 노드의 숫자에 해당하고 인덱스의 값은 진입차수의 개수를 의미한다.
graph = [[] for _ in range(n+1)] # 각 노드별로 나가는 간선의 위치를 저장한다.
# 각 인덱스는 노드의 숫자에 해당하고 인덱스의 값에 해당하는 배열으로 간선이 이어진다는 것을 의미한다.
# 아래의 반복문을 통해 채워질 것이다.
stack = [] # 진입차수가 없는 노드가 들어갈 stack 자료구조이다.
result = [] # stack에서 빠져나온 순서대로 값이 들어갈 것이며, 이것이 정렬의 결과물이다.
for _ in range(m):
prev, post = map(int, input().split()) # prev는 선행과목, post는 후수과목에 해당한다.
graph[prev].append(post) # graph의 prev인덱스에 해당하는 배열에 post를 추가한다. 즉, prev라는 과목이 수강되어야 post를 수강할 수 있다.
indegree[post]+= 1 # post의 선수과목이 지정되었으므로 진입차수를 1 추가한다.
for i in range(1, n+1): # 처음에는 먼저 진입차수가 0인 노드부터 찾습니다.
if indegree[i] == 0:
stack.append(i)
while stack: # stack 비게 된다면, 정렬이 모두 끝납니다. 그래프 내에 사이클이 존재한다면, stack dl 비지 않게 되고 무한루프를 돕니다.
value = stack.pop() # stack에서 값을 꺼냅니다.
result.append(value) # 결과값으로 저장합니다.
for i in graph[value]: # stack에서 꺼내진 값의 다음 노드로 연결된 노드들을 찾습니다.
indegree[i] -= 1 # stack에서 꺼내진 값에서 나오는 진출간선을 없애줍니다.
if indegree[i] == 0: # 이후 진입차수가 0이 된 노드를 찾고
stack.append(i) # 해당 노드를 stack에 넣고 stack가 빌 때까지 위의 내용들을 반복합니다.
print(result)
heap으로 구현하면 pop할때의 값이 자료구조에 담긴 값중에 가장 작은 값이 됩니다.
즉, 조건을 만족하는 내에서 가장 숫자가 작은 노드부터 정렬됩니다.
이 예시에서는 queue와 결과값이 같습니다.
heap을 반드시 사용해야 하는 경우는 백준 1776번 문제집에서 확인해볼 수 있습니다.
import heapq
n, m = map(int, input().split())
indegree = [0]*(n+1)
graph = [[] for _ in range(n+1)]
heap = []
result = []
for _ in range(m):
prev, post = map(int, input().split())
graph[prev].append(post)
indegree[post] += 1
for i in range(1,n+1):
if indegree[i]==0:
heapq.heappush(heap, i)
while heap:
value = heapq.heappop(heap)
result.append(value)
for i in graph[value]:
indegree[i] -= 1
if indegree[i]==0:
heapq.heappush(heap, i)
for i in result:
print(i, end=" ")