Minimum Spanning Tree
Kruskal Algorithm
서로소인 집합들의 표현
- 각 집합을 하나의 트리로 표현한다.
- disjoint 특성(두 집합이 공집합)을 가진 sets - find(u)=u가 속한 집합 찾기, union(u, v)=두 집합을 하나로 합치기
- Ex) 2개의 집합
- 리프 노드로부터 루트 노드까지 따라 올라가면서 연산을 수행한다. 이 때 각 노드는 자식 노드가 아닌 부모 노드의 주소를 가지기 때문에 링크 구조를 쓰지 않아도 좋다. 또한 부모 노드의 주소가 자신의 주소와 동일하다면 그 노드는 루트 노드라는 것을 뜻한다.
- 트리의 모양은 크루스칼의 진행에 따라 얼마든지 바뀔 수 있다.
- 여러 트리가 있어도 모두 한 개의 배열로 표현이 가능하다. 이때 배열은 상향식으로 표현하며 각 인덱스의 데이터에는 부모 노드를 표시하게 된다. 이것이 가능한 이유는 모든 노드의 부모가 유일하기 때문이다. (자식을 표시하는 하향식으로 구성할 경우 자식이 유일하지 않기 때문에 하나의 배열로 표현하는 것이 불가능해진다.)
Find-Set (v)
- 자신이 속한 트리의 루트를 찾는 메소드
- 슈도 코드
FIND-SET (x) // x가 속해있는 트리의 루트 노드 이름을 반환하는 것이 목표이다.
if x != p[x]
p[x] = FIND-SET(p[x]) // 순환적으로 구성
return p[x];
-> 시간 복잡도는 O(h)가 된다. h는 트리의 높이이며 최악의 경우 높이를 구하는데 O(n)의 시간이 걸린다.
while x != p[x]
x = p[x];
return p[x];
union (u, v)
- 한 트리의 루트를 다른 트리 루트의 자식 노드로 만드는 메소드
- 둘 중 하나를 다른 쪽에 붙이면 되기 때문에 위 그림에서 c를 f에 붙이는 것이 아니라 f를 c에 붙이는 것도 가능하다.
- 슈도 코드
UNION (u, v)
x = FIND-SET(u);
y = FIND-SET(v);
p[x] = y;
-> 루트 노드를 찾은 이후에는 O(1)의 시간만이 걸리지만, 루트를 찾는 데 O(h)만큼의 시간이 소요된다.
weighted-union (u, v)
- 두 집합을 결합할 때 작은 트리의 루트를 큰 트리 루트의 자식으로 만든다. (크기는 노드의 개수를 의미한다.)
- 각 트리의 크기를 꾸준히 세주고 있어야 노드의 개수를 알기 쉽다.
- 슈도 코드
WEIGHTED-UNION (u, v)
x = FIND-SET (u);
y = FIND-SET (v);
if (size[x] > size[y])
p[y] = x; // 작은 트리를 큰 트리에 넣는다 (작은 트리의 부모 노드를 큰 노드의 루트 노드로 지정하여 실현)
size[x] += size[y];
else
p[x] = y;
size[y] += size[x];
Worst와 Weighted Union
- weighted union의 효과를 쉽게 알 수 있는 그림
- weighted union을 사용하면 트리의 높이가 $log{n}$을 넘지 않는다.
Path Compression (경로 압축)
- find 메소드에서 행하는 트릭이다. 트리를 루트까지 되짚어 올라가는 동안 부모 노드들을 각각 떼어내 루트 노드에 따로 붙이게 되는 기술이다.
- 지나가지 않는 노드들은 건들이지 않고 오직 지나갔던 조상 노드들만 따로따로 분해한다.
- 슈도 코드
FIND-SET-PC (x)
while x != p[x]
p[x] = p[p[x]]; // 완전히 압축되는 결과는 없지만 절차를 절반정도 줄여주는 효과를 준다.
x = p[x];
return p[x];
-> 시간 복잡도의 측면에서 봤을때는 p[p[x]]를 통해 두 레벨씩 올라가는 것보다는 while을 두 번 사용하여 압축을 수행하는 것이 더 효율적이다.
Weight Union with Path Compression (WUPC)
- M번의 union-find연산의 총 시간 복잡도는 O(N + Mlog*N)이 된다. (N은 원소의 개수를 뜻하며 log*n은 1이 될때까지 log를 반복하여 취하는 횟수를 뜻한다.)
- 거의 선형 시간을 소요하는 작업이다. 즉 한 번의 find 혹은 union이 거의 O(1) 시간만 소요한다.
Kruskal알고리즘의 시간 복잡도
- 실질적으로 엣지들을 가중치순으로 정렬하는 데 걸리는 시간이 가장 오래 걸린다.
- m은 $O(n^2)$이다.
- Initialize A : O(1)
- First for loop : |V| MAKE-SETs => O(n)
- Sort E : $O(|E|log_2{|E|})$ => $O(mlogm)$
- Second for loop : O(|E|) 2 FIND-SET and 1 UNION (둘 다 O(1)에 가까운 시간) => O(m)
- 총 시간 복잡도 : $O(|E|log_2{|E|})$ => $O(mlogm)$ = $O(mlogn)$
Prim
Prim Algorithm
- 임의의 노드를 출발노드로 선택한다.
- 출발 노드를 포함하는 트리를 점점 키워나간다.
- 각 단계에서 이미 트리에 포함된 노드와 포함되지 않은 노드를 연결하는 엣지들 중에 가장 가중치가 작은 엣지를 선택한다.
- $V_A$ : 현재 엣지들에 의해 연결되며 만들어진 트리에 속한 노드
- |$V_A$| = n이면 모든 노드들이 연결되었다는 것을 뜻하기 때문에 프로그램을 종료하게 된다.
- 최악의 경우 각 노드를 구하는 데 소요되는 시간은 O($n^2$) X (n - 1) = O($n^3$)이다. 단순하게 생각하고 구현했을 경우 효율이 떨어지는 결과가 나오기 때문에 프림 알고리즘은 시간 복잡도를 줄이는 데 무게를 많이 두어야 한다.
Prim을 통해 MST가 찾아지는 이유
- Prim 알고리즘의 임의 한 단계를 본다.
- A를 현재까지 알고리즘을 진행하며 선택한 엣지들의 집합이라 하며 A를 포함하는 MST가 존재한다고 하자. (이 가정이 가능한 이유는 처음 A가 공집합으로 시작하기 때문이다.)
- S와 V-S 두개의 컷이 있다고 할 때 어느 엣지들도 이 두 컷을 모두 지나는 상태는 없다. 프림 알고리즘이 선택한 엣지는 두 컷을 통과하는 엣지들 중 가중치가 가장 작은 엣지이며 또한 이는 집합 A에 대해 안전한 엣지이기도 하다. 그렇기 때문에 알고리즘이 선택한 엣지를 포함하더라도 MST를 벗어나지 않게 된다.
- 엣지 (u, v)는 출발 노드에 이미 연결된 노드와 그렇지 않은 노드를 연결하는 엣지들 중 가장 가중치가 작은 엣지이다.
가중치가 최소인 엣지 찾기
- $V_A$ : 이미 트리에 포함된 노드들
- $V_A$에 아직 속하지 않은 각 노드 v에 대해 아래와 같은 값을 유지한다.
1) key(v) : 이미 $V_A$에 속한 노드와 자신을 연결하는 엣지들 중 가중치가 최소인 엣지 (u, v)의 가중치값
2) π(v) : 선택한 엣지 (u, v)의 끝점인 u
- 이 방식을 통해 프림 알고리즘을 수행하게 된다면 키값이 최소인 노드를 찾는데 O(n)시간만이 소요되게 된다는 장점이 있다.
- 키 값을 계산하는 데 걸리는 시간또한 구해야 한다. 각 키의 값들은 출발점과 연결하는 엣지가 있으면 그 엣지로 초기화하며, 없다면 무한대로 초기화해놓는다.
- 새 엣지를 등록하면 주변 노드들의 키값을 갱신하게 되는데 노드들의 키값을 완전히 버리는 것이 아니라 새로 등록된 노드와 연결된 노드들의 키값만 새로 비교한다. 더 작은 가중치값이라면 교체하면 된다.
- 노드들의 가중치들을 갱신하는 시간 O(n) X 모든 노드들을 연결시키기 위한 반복 (n - 1) = O($n^2$)의 시간이 소요된다.
프림 알고리즘의 슈도 코드
MST-PRIM (G, w, r) // r은 출발점을 뜻한다.
for each u ∈ V
key[u] = ∞;
π[u] = null;
$V_A$ = {r};
key[r] = 0;
while $V_A$ < n // while문은 n-1번 반복한다.
find u $V_A$ with the minimum key value; // 최소값을 찾는 과정이며 O(n)시간이 걸린다.
$V_A$ = $V_A$ ∪ {u};
for each v $V_A$ adjacent to u // degree(u) = O(n)
if key[v] > w(u, v)
key[v] = w(u,v);
π[u] = u;
-> 이 코드의 시간 복잡도는 O($n^2$)가 된다.
key값이 최소인 노드 찾기
- 우선순위 큐 (Minimum Priority Queue)를 이용한다.
$V-V_A$에 속한 노드들을 저장한다.
Extract-Min : key값이 최소인 노드들을 삭제하고 반환한다.
- O(logn)시간 내에 최소값을 찾을 수 있게 된다.
- 슈도 코드
MST-PRIM (G, w, r)
for each u ∈ V[G]
key[u] = ∞;
π[u] = null;
key[r] = 0;
Q = V[G];
while Q != {} // 공집합이 아닐때
u = EXTRACT-MIN (Q) // 최소값을 찾는 과정으로 O(logn)의 시간이 걸린다.
for each v ∈ Adj[u]
if v ∈ Q and w(u, v) < key[v]
π[v] = u;
key[v] = w(u, v); //우선순위 큐에서 키값을 감소시키는 명령으로 O(logn)시간이 걸린다.
프림 알고리즘의 시간 복잡도
- 이진 힙을 이용하여 우선순위 큐를 구현할 경우
- while 반복
1) n번의 Extract-Min 연산 : O(nlogn)
2) m번의 Ddecrease-Key 연산 : O(mlogn)
- 시간 복잡도는 1) + 2) = O(mlogn)
- 우선순위 큐를 사용하지 않고 단순히 구현할 경우 : O($n^2$)
- 피보나치 힙을 사용하여 구현한다면 O(m + nlogn)의 시간으로도 구현할 수 있다.
'프로그래밍 > 알고리즘' 카테고리의 다른 글
최단 경로 (2) (0) | 2019.06.22 |
---|---|
최단 경로 (1) (0) | 2019.06.20 |
최소 비용 신장 트리 (1) (0) | 2019.06.12 |
DAG와 위상 순서 (0) | 2019.06.11 |
그래프에서의 DFS (0) | 2019.06.10 |