Search

위상정렬 (Topology sort)

위상 정렬이란 사이클이 없는 방향그래프(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
복사