기초 Graph 알고리즘
Disjoint Set
DFS 랑 Union Find 가 자주 쓰임.
- 뭐가 더 나을까
- DFS 가 더 빠르지만 Graph가 동적으로 변하는 상황에선 Union Find 가 나음.
- Directed Graph 에서는 Union Find 를 쓸 수 없음.
- Cycle 탐지도 가능하지만, 가장 짧은 Cycle 탐지 는 불가능함.
DFS
코드
설명
O(\(N\)) 에 끝나며 정적인 그래프에서 사용가능하다.
DFS 에서 Edge 는 4가지 종류가 있다.
- DFS Search Tree 구성을 위해 사용된 Edge,
- Backward Edge : DFS Search Tree 상에서 Parent Node 로 향하는 Edge
- Forward Edge : DFS Search Tree 상에서 Child Node 로 향하는 Edge
- Cross Edge : 서로 다른 DFS Search Tree 를 연결하거나 Parent/Child 가 아닌 Node 간을 연결하는 Edge
Backward Edge 가 있으면 Cycle 이 있으며 그 역도 성립한다.
stTB[] == true && edTB[] == false
의 조건이 바로 그것- 가장 짧은 Cycle 을 탐색하는건 보장되지 않는다. Forward Edge 를 거쳐서 Backward Edge 를 지날 가능성이 있으나 이 경우는 탐색하지 않기 때문이다.
DFS 를 단순히 Visit 체크로 구현하는 경우도 있다.
- Birected Graph 의 경우는 방문체크로도 Cycle 을 찾아낼 수 있다.
- Directed Graph 인 경우는 Visit 된 노드가 꼭 Cycle 을 보장하지 않는다.
Union Find
코드
설명
여기서 시간복잡도는 보통 \(N\) 번의 Union()
후에 Find()
연산을 할 때의 Amortized Time Complexity 를 구한다.
총 시간복잡도는 아래의 두 최적화를 모두 하면 O(\(\alpha(N)\)), 모두 안하면 O(\(N\)) 이 된다.
- Rank Optimization
- Rank 가 합쳐진 Tree 의 최대 깊이를 의미함.
- Append a root of shallow tree to a root of deeper one in
Union()
. - 알고리즘 특성 상 최소 이진트리의 크기를 유지하게 되어 Amortized O(\(\log{\mathrm{N}}\)) 을 만듬
- Path Compression
- 각 정점의
rootTB[]
는 현재 구성한 Tree 의 Root 만을 저장시키는 것. - 이것만 사용시 O(\(M\log(N)\)) 이라함. SE, Prinston 수업 PPT
- 각 정점의
-
자세한건 Samsung SW 블로그 , Wiki 참고
- 근데 둘다 하면 오버헤드 때문에 더 느려질 가능성이 커지고, 대체로 더 빠른 Path Compression 만 적용하는 경우가 많음.
MST
Minimum Spanning Tree 의 약자.
아래 코드는 이 문제 를 푸는 것임.
아래 두 알고리즘은 시간복잡도가 보여주듯 Graph 의 간선의 수에 따라 선호되는게 다름
- Sparse Graph => Kruskal
- Dense Graph => Prim
Kruskal Algorithms
코드
가장 weight 가 낮은 edge 부터 사용해 tree 를 만드는 Gridy Algorithms.
- 보통 UnionFind 를 사용해 tree 를 구성함
- 그래서 Spanning Forest 에서도 가능함.
시간 복잡도는 퀵소트 사용시 O(\(E\log{E}\))
Prim’s Algorithms
- 작동원리는 Graph 를 A(임의의 정점 1개로 시작), B 로 나누고
- A 에서 가장 가까운 B 의 한 노드를 A 로 넣는걸 B 가 소멸될때까지 반복하는 것임.
- 가장 가까운 정점을 찾는 방법에 따라서 시간복잡도가 갈림
-
수단 복잡도 배열 + 루프 O(\(\vert V \vert ^ 2\)) 우선순위 큐 + Edge O(\(\vert E \vert \log{\vert E \vert }\)) 우선순위 큐 + Vertex O(\(\vert E \vert \log{\vert V \vert }\)) 피보나치 큐 O(\(\vert E \vert + \vert V \vert \log{\vert V \vert }\)) - 우선순위 큐의 경우 O(\(\log{E}\)) 는 O(\(\log{V}\)) 와 같아서 거기서 거기임.
- 완전그래프에선 우선순위 큐보다 배열과 루프를 쓴 \(\vert V^2 \vert\) 가 더 빠르다는 것에 주의
- 자세한건 Wiki 참고
O(\(\vert V \vert ^2\)) 버전
코드
설명
완전 그래프에 가까울수록 큐를 쓰는 버전보다 이 버전이 더 빠름에 주의하자.
O(\(\vert E \vert \log{\vert E \vert }\)) 버전
코드
설명
Min Priority Queue 에 Edge 를 저장함
- 선택된 vertex 마다 priority queue 에 연결된 edge 를 넣음.
- queue 의 값을 꺼내면서 지금까지 합치지 않은 가장 가까운 vertex 를 추가함.
사소한 팁으로
- 이때 이전 vertex 에서 넣은 edge 가 계속 누적되기 때문에 중복여부를 꼭 체크해야함.
- edge 를 넣을때 중복여부를 체크하면 약간 빨라짐.
- 정점 수만큼 루프를 도는건 priority queue 에 남은걸 다 비울 필요 없게하는 용도임.
시간복잡도는 queue 를 완전히 비우고, edge 를 넣을 때 중복여부를 체크하지 않는다면, \(\vert E \vert\) 번 queue 에서 넣고 빼야함. 그러므로 O(\(\vert E \vert \log{\vert E \vert }\))
O(\(\vert E \vert \log{\vert V \vert }\)) 버전
코드
설명
PQ
는 여기 에서 DecreaseKey()
를 하도록 Max -> Min 으로 교체한 것임.
시간복잡도 설명은 유튜브 에서 잘 설명해줌.
- Min Priority Queue 에 Vertex 를 넣고, 하나 빼고 대응되는 Weight 를 최대로 넣어 초기화.
- Queue 가 빌 때까지 가장 가까운 정점을 꺼내고,
- 여기와 연결된 간선의 가중치가 기존보다 더 작으면
DecreaseKey()
함. - 모든 간선에 대해서
DecreaseKey()
를 수행하므로 O(\(\vert E \vert \times \log{\vert V \vert}\)) 가 됨. - Queue 내에 공간을 전의 방법보다 덜먹는다는게 장점?
Shortest Path
가중치가 없는 경우 BFS 로 해결이 되지만, 아닌 경우 적절한 알고리즘을 사용해야 한다.
Floyd Warshall
코드
설명
모든 가능한 경로에 대해서 가장 짧은 비용을 얻어냄.
- 경로추적은
dp[s][e]
를 업데이트 시d
값을 넣어주면 됨. - 이후 DFS 를 Inorder 로 해주면 됨.
시간복잡도는 O(\(\vert N^3 \vert\)) 가 자명함
Dijkstra
코드
설명
Dijkstra 를 수행하면 시작점으로부터 나머지 모든 정점에 대한 거리가 구해짐
작동 방법은 Gridy + DP 가 같이 사용됨
- 시작점을 제외한 모든 점까지의 거리(
dp[]
)를 무한대로 지정함 - 방문하지 않은 모든 정점 중에서 가까운 정점
s
을 남은 정점이 없을 때까지 선택함.s
와 연결된 정점t
마다,dp[t] = min(dp[t], dp[s] + weight of s->t )
를 수행함.- 그러면
dp
는 지금까지 방문한 정점들에 대해서 나머지 정점들의 가장 짧은 길이를 저장하고 있음. 이 중 가장 가까운 정점은 지금까지 선택된 정점 다음으로 가까운 정점이 되며 Gridy 를 반복하게 됨.
가까운 정점 s
를 선택하는 방법을 최적화 하는 과정에서 Prim’s Algorithms 과 같은 이슈를 가짐.
- 위 코드는 Priority Queue 에서 현재까지 연결된 모든 간선을 관리한 것.
- 위에서 살펴보았듯 Priority Queue 에서 모든 정점을 관리하며
DecreaseKey()
를 수행할 수도 있음.
A*
Dijkstra 는 시작점에서 가장 가까운 정점들을 차례로 추가해나감.
- 추가해나가는 순서대로 가는건 전혀 최단거리가 아님.
A* 는 연결한 정점들로부터 기존 거리 + Huristic 거리
의 합의 값이 제일 낮은 것부터 추가함.
Bellman-Ford
코드
설명
시간복잡도는 \(O(\vert V \vert \vert E \vert)\) 이다.
다익스트라에서는 사이클 내의 거리가 음수가 되면 무한루프를 돌게된다. 벨만 포드 알고리즘은 그렇지 않으며 그러한 사이클을 탐지해낼 수도 있다.
아이디어는 Edge Relaxation 이 가능한 경우의 수가 최대 \(V-1\) 번이 정상적이고 그 이상이 되면 무한루프라는 것이다. 왜냐하면 다른 정점을 경유해서 거리가 변할 수 있는 경우의 수는 정점 간 간선의 갯수이기 때문이다. 그래서 정점을 점차 추가하면서 추가된 정점과 연결된 모든 간선에 대해서 Edge Relaxation 을 하고, 마지막으로 정점을 추가했을 때도 Edge Relaxation 이 일어나면 Cycle 이라고 알린다.
시작점이 없이 그냥 그래프에서 음수 사이클을 찾을 때도 사용할 수 있다. 이땐 시작점의 거리만 0 으로 초기화 하는게 아니라 모든 정점의 거리를 똑같이 초기화 한 뒤 탐색을 시작하면 된다. 문제, 풀이
DAG 에서 Negative Weight 가 없는 경우 경우 Topological Sort 에 따른 순서로 Edge Relaxation 을 하면 \(O(\vert E \vert)\) 에서 끝낼 수 있다.
SPFA
코드
설명
벨만포드 알고리즘의 빠른버전. bellman-ford 의 가장 바깥쪽 루프 한번은 queue 에 size 를 저장한 후 그 횟수만큼 dequeue 한 것과 같다.
최악의 시간복잡도는 \(O(\vert V \vert \vert E \vert)\) 으로 동일하지만 평균 시간복잡도가 실험 결과 상 \(O(\vert E \vert)\) 이라고 한다. 위키가 그렇게 어려운 내용도 없으니 한번 읽어보자.
댓글남기기