====== 위상 정렬 (Topological Sorting) ======
* 기본 개념과 설명은 위키피디아에 잘 나와있다 - [[https://en.wikipedia.org/wiki/Topological_sorting|Topological Sorting]]
* 위상정렬 결과를 구하는 알고리즘은 [[https://en.wikipedia.org/wiki/Topological_sorting#Kahn's_algorithm|Kahn's algorithm]]과 DFS를 이용하는 방법이 있다. 인접리스트로 그래프가 주어질때, 수행시간은 둘 다 O(|V|+|E|)이다.
* 종만북에서는 Kahn's algorithm은 가볍게 언급만 하고 DFS 방법만 설명한다. [[https://blog.encrypted.gg/910?category=773649|바킹독의 실전 알고리즘 강좌]]에서는 Kahn's algorithm을 설명한다
* 여러 가지의 가능한 정렬 결과들 중, 특정한 순서를 만족하는 결과를 얻으려면 Kahn's algorithm 을 써야 한다.
* 결과를 generator 형태로 얻으려면 Kahn's algorithm 을 써야 한다.
* DFS에서는 위상정렬의 결과를 역순으로 구한 뒤에 reverse를 해서 최종 결과를 얻는 방식이라서 불가능하다.
* DFS를 이용하는 방법을 구현할 때, 재귀 버전의 DFS을 사용한다면 간단하게 구현할수 있지만, 비재귀 버전의 DFS라면 좀 더 테크닉이 필요하다. 아래에 언급하는 python graphlib의 _find_cycle() 함수 구현을 참고하자.
* 방향그래프에서 사이클을 찾는 문제도 위상 정렬 알고리즘에 기반하여 해결할 수 있다.
* Kahn's algorithm을 돌리다가, 모든 노드를 포함하는 위상정렬 결과가 만들어지기 전에 indegree가 0인 노드가 남아있지 않게 되면, 사이클이 있는 것이다
* DFS에서는 탐색중인 노드들과, 탐색이 완료된 노드들을 따로 저장하면서 탐색하는 방식으로, 사이클이 발견되면 바로 찾을 수 있다.
* 사이클의 존재 여부뿐 아니라, 그 사이클에 어떤 노드가 포함되었는지 정보까지 알기 위해서는 DFS 방식을 써야 한다
* Python 3.9에서는 TopologicalSorter 클래스를 포함하는 [[https://docs.python.org/3.9/library/graphlib.html|graphlib 모듈]]이 추가되었다.
* 위상정렬 결과를 리턴해주고, 사이클이 있는 경우에는 그 사이클의 정보를 포함하는 예외를 발생시킨다.
* 그러나, 아직 대부분의 PS사이트에서는 python 3.8을 사용하고 있기 때문에 사용이 가능해지려면 좀 기다려야 할 듯..
* BOJ에서는 python 3.9를 쓰니까 사용 가능하다. 그리고 이미 Python 3.10의 릴리즈를 코앞에 둔 상황인데, 이쯤 되면 다른 사이트에서도 3.8은 이제 버리지 않을까 하는 기대를 해보면서, BOJ에서는 TopologicalSorter를 사용하기로 결정했다.
* PS 사이트에서 못 쓰더라도, [[https://github.com/python/cpython/blob/3.9/Lib/graphlib.py|소스코드]]가 순수 파이썬으로 작성되어 있어서 참고할만 하다. 위상정렬 코드는 패러럴 컴퓨팅까지 고려해서 좀 복잡하게 구현되어있으나, 비재귀 DFS에 기반한 사이클 디텍션은 따로 함수로 빠져있고, 구현도 심플해서 그대로 조금 변형해서 갖다 쓰는것도 가능할 듯.
===== 코드 =====
* graphlib.TopologicalSorter 가 사용 가능한 환경에서는 가능한 그것을 이용해서 작성하자. 아래의 코드는 graphlib.TopologicalSorter를 쓸수 없는 환경(Python 3.8이하만 지원하는 사이트) 에서만 사용하도록 한다.
from typing import AbstractSet, List, Sequence
def topological_sort(graph: Sequence[AbstractSet[int]]) -> List[int]:
"""Returns a list of nodes in topologically sorted order."""
indegrees = [0] * len(graph)
for successors in graph:
for v in successors:
indegrees[v] += 1
stack = [u for u, indegree in enumerate(indegrees) if indegree == 0]
result = []
while stack:
u = stack.pop()
result.append(u)
for v in graph[u]:
indegrees[v] -= 1
if indegrees[v] == 0:
stack.append(v)
if len(result) != len(graph):
raise ValueError('found a cycle')
return result
===== TopologicalSorter =====
* graphlib.TopologicalSorter 를 쓰면 거의 모든 활용 문제를 모두 처리할 수 있다.
* 전에는 python 3.9를 지원하는 사이트가 거의 없어서 안쓰려고 했었는데, 가능하면 무조건 쓰는 것으로 정책을 바꿨다. 코드량이 확 줄어든다.
* 범용적이다보니 속도는 직접 짠 알고리즘보다 오히려 좀 느리다. 먼저 cycle 체크를 한번 하고, 다시 위상 정렬을 돌리는 식으로 동작하므로 벌써 느리다. 하지만 큰 차이가 나는 것은 아니다. 열심히 사용하자
* 초기화하는 방식에는, 디펜던시 그래프를 만들어서 생성자로 넘기는 방법과, 그냥 디펜던시를 하나하나 add 메소드를 이용해서 추가하는 방법이 있다.
* 그래프는 dict of iterable 형식이다. 튜토리얼 문서의 예시에는 dict of set 으로 되어있어서 꼭 그렇게만 만들어야 하는 것으로 오해할 수 있는데, dict of list 도 지원된다.
* 초기화하는 것만 봤을때에는, 엣지들로 그래프를 만들어서 넘겨주는 것보다 그냥 엣지들을 바로 add 메소드로 추가해 주는 것이 더 효율적이다. 그러나 위상정렬 다음 단계의 작업에서 그래프가 어차피 필요한 경우도 종종 있는데, 이럴때는 어차피 그래프를 만들어야 하니, 이것으로 TopologicalSorter도 초기화해버리는 것이 낫다
* TopologicalSorter를 초기화 한 이후에는, 대부분의 경우는 static_order()을 돌려서 위상정렬 결과를 얻는 것으로 충분하다. 몇몇 경우에는 루프 안에서 get_ready()와 done()를 반복하면서 작업해야 하는 경우가 있긴 한데, 아래의 [[#활용 문제 유형]]에서 설명한다. 또한 사이클 디텍션만이 목표라면 prepare() 만 돌려보면 된다.
===== 활용 문제 유형 =====
==== 기본적 ====
* 위상 정렬 순서대로 출력하기
* => 가장 기본. 노코멘트
* 사이클 존재 여부 찾기 (=가능한 정렬 순서가 존재하지 않는지 찾기)
* => 위에서 설명한대로,
* 위상 정렬 순서가 유일한지 확인하기
==== 여러개의 가능한 위상 정렬 순서 중에서 특정 순서상 가장 빠른 것을 찾기 (e.g. 사전순으로 빠른 경로)====
* 위에서 언급했던대로, DFS기반으로는 처리할수 없고, Kahn's algorithm 의 경우에는 큐 대신 우선순위큐를 쓰는 방법으로 해결 가능하다.
* TopologicalSorter을 쓸 경우에는 static_order() 함수를 쓰는 대신에, 직접 루프를 돌면서 get_ready()와 done()를 반복해서 호출하는 식으로 처리하면 해결 가능하다.
* get_ready()를 호출해서 얻은 다음 방문 후보 노드들을 우선순위큐에 모두 저장하고, 우선순위큐에서 최소 노드를 꺼낸다음에 그 노드에 대해서 done()를 호출하는 것을 반복하는 방식으로 구현이 가능하다.
* 그냥 하나의 함수 안에서 이 작업을 같이 처리하는 것에 비하면 속도면에서 조금 비효율적일것 같지만, TopologicalSorter를 쓰는 것 자체가 이미 구현의 편의성을 위해서 속도상으로 페널티를 안고 가기로 한것이니만큼 별로 신경쓸필요 없다.
* 코드는 이런식이다
*
# graph = ...
ts = graphlib.TopologicalSorter(graph)
answer = []
heap = []
ts.prepare()
while ts:
for node in ts.get_ready():
heapq.heappush(heap, node)
cur_node = heapq.heappop(heap)
answer.append(cur_node)
ts.done(cur_node)
return answer
==== 위상 정렬 순서에 기반한 DP ====
* 임계 경로 (=최장 경로=선행 작업을 모두 처리한 후 특정 작업을 마치는 최단 시간)
* 최단경로 (DAG에서의 최단 경로는 다익스트라보다도 이 방법이 빠르다)
* 그외 온갖 잡다한 것들
===== 관련 문제 =====
* 위상 정렬
* [[:ps:problems:boj:2623|음악프로그램]]
* 사이클 존재 여부 확인
* [[:ps:problems:leetcode:207|Course Schedule]]
* 여러개의 가능한 위상 정렬 순서 중에서 특정 순서상 가장 빠른 것을 찾기 (e.g. 사전순으로 빠른 경로)
* [[:ps:problems:boj:1766]]
* 위상 정렬 순서에 기반한 DP
* 임계경로 - [[:ps:problems:boj:1948]]