위상 정렬이란 사이클이 없는 방향그래프(Direct Acyclic Graph; DAG)의 모든 노드를 방향성에 거스르지 않도록 순서대로 나열하는 것이다.
방향그래프에서의 진입차수(In-Degree)와 진출차수(Out-Degree)
•
진입차수 (In-Degree) : 특정한 노드로 들어오는 간선의 개수
•
진출차수 (Out-Degree) : 특정한 노드에서 나가는 간선의 개수
위상 정렬 알고리즘
위상정렬은 큐를 이용하거나 DFS를 이용해서 구현할 수 있다.
위상 정렬 특징
•
DAG에 대해서만 수행 가능
•
여러가지 답 존재 가능
◦
한 단계에서 큐에 새롭게 들어가는 원소가 2개 이상인 경우가 발생하면 큐에서 어떤 것을 먼저 빼느냐에 따라 달라지지만 정렬에 오류는 없으므로 여러 답 존재가 가능하다.
•
모든 원소 방문 전 큐가 비면 사이클 존재한다고 판단 → 수행 불가
◦
사이클에 포함된 원소들은 모두 큐에 들어가지 못함
•
큐를 이용한 BFS 구현과 스택을 이용한 DFS 구현이 가능함
큐를 이용한 구현
1.
진입차수가 0인 모든 노드를 큐에 넣는다
2.
큐가 빌 때 까지 다음 과정을 반복한다.
a.
큐에서 원소를 꺼내 해당 노드에서 나가는 간선을 그래프에서 제거한다.
b.
새롭게 진입차수가 0이 된 노드를 큐에 넣는다
⇒ 각 노드가 큐에 들어온 순서가 위상 정렬을 수행한 결과가 된다.
예시
다음과 같은 그래프가 주어진다고 하자.
문제에서 주어질 때는
[[1, 2], [1, 5], [2, 6], [2, 3], [3, 5], [4, 7], [5, 6], [6, 4]]
이렇게 주어질 것이다.
1번 과정을 수행하기 위해 각 노드의 진입차수를 구해본다
노드 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
진입차수 | 0 | 1 | 1 | 2 | 1 | 2 | 1 |
1단계. 진입차수가 0인 노드를 큐에 넣는다
Queue : []
진입차수가 0인 1노드를 큐에 넣는다
Queue : [1]
answer = [1]
2단계. 큐가 빌 때까지 큐에서 원소를 꺼내 해당 노드에서 ‘나가는’ 간선을 그래프에서 제거하고 진입차수가 0이된 노드를 큐에 넣는다.
반복 1
Queue : [] → 1노드 꺼냄
1노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2 | 3 | 4 | 5 | 6 | 7 |
진입차수 | 0 | 0 | 1 | 2 | 0 | 2 | 1 |
진입차수 0이 된 노드 2, 5큐에 넣음
Queue : [2, 5]
answer = [1, 2, 5]
반복 2
Queue : [5] → 2노드 꺼냄
2노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2
(처리 완료) | 3 | 4 | 5 | 6 | 7 |
진입차수 | 0 | 0 | 0 | 2 | 0 | 1 | 1 |
진입차수 0이 된 노드 3 큐에 넣음
Queue : [5, 3]
answer = [1, 2, 5, 3]
반복 3
Queue : [3] → 노드 5 꺼냄
5 노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2
(처리 완료) | 3 | 4 | 5
(처리 완료) | 6 | 7 |
진입차수 | 0 | 0 | 0 | 2 | 0 | 0 | 1 |
진입차수 0이 된 노드 6 큐에 넣음
Queue : [3, 6]
answer = [1, 2, 5, 3, 6]
반복 4
Queue : [6] → 노드 3 꺼냄
3 노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2
(처리 완료) | 3
(처리 완료) | 4 | 5
(처리 완료) | 6 | 7 |
진입차수 | 0 | 0 | 0 | 1 | 0 | 0 | 1 |
진입차수가 새롭게 0이 된 노드 없음
반복 5
Queue : [] → 노드 6 꺼냄
6 노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2
(처리 완료) | 3
(처리 완료) | 4 | 5
(처리 완료) | 6
(처리 완료) | 7 |
진입차수 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
진입차수가 0이 된 노드 4 큐에 추가
Queue : [4]
answer = [1, 2, 5, 3, 6, 4]
반복 6
Queue : [] → 노드 4 꺼냄
4 노드에서 나가는 간선 제거
노드 | 1
(처리 완료) | 2
(처리 완료) | 3
(처리 완료) | 4
(처리 완료) | 5
(처리 완료) | 6
(처리 완료) | 7 |
진입차수 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Queue : [7]
answer = [1, 2, 5, 3, 6, 4, 7]
반복 7
Queue: [] → 노드 7 꺼냄
노드 | 1
(처리 완료) | 2
(처리 완료) | 3
(처리 완료) | 4
(처리 완료) | 5
(처리 완료) | 6
(처리 완료) | 7
(처리완료) |
진입차수 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
반복 8
Queue: [] → 꺼낼 노드가 없음 → 종료
answer = [1, 2, 5, 3, 6, 4, 7]
코드
from collections import deque
def solution(v, graph, indegree):
"""
v: 노드 개수
e: 간선 개수
graph : a번째 리스트에 a노드에서 이동 가능한 노드들이 담겨있음 (2차원 리스트)
indegree: 진입차수를 담은 리스트
"""
answer = []
q = deque() # 큐 기능 위한 deque 라이브러리
# 1. 진입차수 0인 노드 큐에 삽입
for i in range(1, v+1):
if indegree[i]==0:
q.append(i)
# 2. 큐가 빌 때까지 큐에서 원소 꺼내고 새롭게 진입차수가 0이 된 노드 큐에 넣기
while q:
# 큐에서 원소 꺼내기
now = q.popleft()
answer.append(str(now)) # 나중에 join연산으로 string으로 리턴하기 위해 str로 추가
# 해당 노드와 연결된 노드들의 진입차수에서 1 빼기
# 간선 제거 과정
for i in graph[now]:
indegree[i] -= 1
# 이 과정에서 진입차수 0이 되는 노드를 큐에 삽입하면
# 이전에 이미 0이 된 노드 말고 이번 반복에서 0이 되는 노드만 큐에 추가 가늗
if indegree[i] == 0:
q.append(i)
answer = " ".join(answer)
return answer
# 노드의 개수와 간선의 개수를 입력 받기
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트 초기화
graph = [[] for i in range(v + 1)]
# 방향 그래프의 모든 간선 정보를 입력 받기
for _ in range(e):
a, b = map(int, input().split())
graph[a].append(b) # 정점 A에서 B로 이동 가능
# 진입 차수를 1 증가
indegree[b] += 1
print(solution(v, graph, indegree))
Python
복사
DFS를 이용한 구현 (stack 사용)
노드 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
진출차수 | 2 | 2 | 1 | 1 | 1 | 1 | 0 |
dfs에서는 진출차수를 이용하게 된다. 일단 dfs를 수행하면 진출차수가 0인 노드를 만날 때 까지 stack에 값이 추가되지 않는다.
진출 차수 0인 노드를 만났을 때부터 stack에 값이 추가되기 시작한다.
이 과정에서 stack에 마지막 노드부터 추가되게 되므로 stack값을 reverse해주면 답이 된다.
stack = [7, 4, 3, 6, 2, 5, 1] → reverse → [1, 5, 2, 6, 3, 4, 7]
큐로 풀이한 방법과 다른 답이 나오지만, 답에 오류는 없다. 여러 답이 가능한 위상 정렬의 특성을 확인할 수 있다.
코드
def dfs(node, graph, visited, stack):
visited[node] = True # 현재 노드 방문했기에 visited값을 True로 바꿔주고
adj_v = graph[node] # 현재 노드에서 진출 가능 노드를 adv_j라 함
for u in adj_v: # 진출 가능한 노드들에 대해서
if not visited[u]: # 이 노드에 방문 아직 안했다면
dfs(u, graph, visited, stack) # 방문
stack.append(str(node))
def solution(v, graph):
visited = [False for _ in range(v+1)] # 노드 방문 여부
stack = [] # 정렬 결과를 담을 stack
for i in range(1, v+1): # 1노드부터 시작
if not visited[i]:# 이 노드를 방문하지 않았다면
dfs(i, graph, visited, stack) # dfs 수행
stack.reverse()
stack = " ".join(stack)
return stack
# 노드의 개수와 간선의 개수를 입력 받기
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트 초기화
graph = [[] for i in range(v + 1)]
# 방향 그래프의 모든 간선 정보를 입력 받기
for _ in range(e):
a, b = map(int, input().split())
graph[a].append(b) # 정점 A에서 B로 이동 가능
# 진입 차수를 1 증가
indegree[b] += 1
print(solution(v, graph, indegree))
Python
복사