본 게시글은 권오흠 교수님의 영상을 공부하고 정리한 글 입니다.

[ Kruskal ]

 

1. 그래프의 간선들을 가중치의 오름차순으로 정렬한다.
2. 정렬된 간선 리스트에서 순서대로 사이클을 형성하지 않는 간선을 선택한다.
  - 가장 낮은 가중치를 먼저 선택한다.
  - 사이클을 형성하는 간선을 제외한다. ( 사이클이 형성된다면 무시하고 다음 간선을 검색한다. ) ( Find ) 한다.
3. 해당 간선을 현재의 MST(최소 비용 신장 트리)의 집합에 추가한다. ( Union ) 한다.
4. 2~3 과정을 간선의 수가 n-1 개가 될 때까지 반복한다. ( 모든 정점의 개수는 n-1 개 )

 

그렇다면 우리가 알아야할 함수는 

 

Q. ) 어떤 엣지가 추가될 때 사이클을 형성하는 간선인지 아닌지 어떻게 알 것인가? => Find

Q. ) 해당 간선을 어떻게 MST 집합에 추가 할 것인가? => Union

적은 숫자의 간선을 가지는 그래프라면 크루스칼 알고리즘으로 MST를 만드는게 적합하다.

크루스칼 알고리즘은 간선을 정렬하는 시간에 좌우되기 때문이다.

Union - Find는 거의 O(1) 에 가능하다.


 

 

어떠한 엣지가 사이클을 형성하는지 아닌지 알기 위해 우리는 각 정점을 서로소인 집합들로 표현해야한다.

- disjoint Set

엣지 ( h, g ) 를 선택하고자 할 때 집합 { h } 와 집합 { g } 는 서로 다른 집합에 속함으로 선택을 해도 사이클을 발생시키지 않는다.

엣지 ( h,g ) 를 선택하고 집합 { h } U { g } 를 합해준다. { h, g } .

...

가중치가 가장 작은 엣지들을 선택하다 보면

 

 

엣지 ( i ,g ) 를 선택하려고 할때 g 와 i 는 같은 집합에 속해 있으므로 선택하면 사이클을 형성하게 된다.

이 경우, 엣지 ( i, g ) 를 선택하지 않는다.

위와 같은 과정을 Union - Find 알고리즘이라고 한다.

물론 구현은 이처럼 간단하지 않다.

그렇다고 다르지도 않다.


[ Union- Find ]

 

그렇다면, 필요한것은 각각 노드(정점)가 서로소인 집합임을 표현하면서, Union 과 Find 이 두 가지 연산을 제공하는 자료구조 이다.

현재 까지 알려진 방법중에 가장 최선의 솔루션은 각 집합을 하나의 트리로 표현 하는 방법이다.

- 다만 , 우리가 아는 트리와는 다르게 트리의 각 노드는 자식노드가 아니라 부모 노드의 주소를 가진다. ( 상향식 트리 )

- 집합의 각 원소들이 트리의 노드가 되고, 누가 루트이고, 누가 누구의 부모인지는 중요하지 않다. 중요한것은 서로 연결되어 있다는것만 알면 된다.

 

이렇게 트리로 구현하는것이 뭔가 배보다 배꼽이 큰거 같아 거부감이 들 수 있으나,

사실 상향식 트리는 배열로 표현 가능하다.

배열로 표현 가능한 이유는 하향식 트리와 달리 각 노드는 부모의 노드 만 저장하기 때문이다.

 

 


이 때쯤 해서 한번 Kruskal 알고리즘 pseudo 코드로 정리해보자.

 

   1   : A 를 공집합으로 시작해서

2 ~ 3 : 각각의 노드들을 서로소인 집합으로 만들어라(  배열로 표현 )

   4   : 모든 엣지를 오름차순으로 정렬하고,

   5   : for문의 반복 횟수가 모든 엣지라고 나와있지만 (  n-1 번이면 충분하다. MST에서 엣지의 개수는 n-1 )

   6   : if 엣지 (u , v ) 에 대해서 u와 v 가 다른 집합에 있다면

7 ~ 8 : then 두 집합을 union 해줘라.

   9   : 최종적으로 집합 A 를 return.

 

 

 


[ Find - Set ]

 

recursive 하게 구현할 수 있으며, 시간 복잡도는 O(h) 이고, 트리가 모두 일렬로 되어 있을땐 O(n)의 시간 복잡도를 가진다.

( Path Compression 을 통해서 트리의 높이를 낮게하는 알고리즘도 있어서 이를 통해 시간 복잡도를 거의 선형시간 O ( 1) 비슷하게 만들수 있다. )

여기서 parent[10] 배열은 각 노드의 집합을 트리로 구현한 배열.

int findSet(int (&parent)[10], int x) {
	if (parent[x] != x) {
		parent[x] = findSet(parent, parent[x]);
	}
	return parent[x];
}

[ Union ]

 

어떠한 두 트리를 합하는 가장 쉬운 방법은 한 트리의 루트를 다른 트리의 자식 노드로 만드는 것이다.

 

Union 알고리즘은 X, Y에다 Find-Set 을 해서 부모 위치만 바꿔주는거기 때문에

결국 Union의 시간 복잡도는 루트를 찾는 Find-set() 과 같다.

그렇다면 두 집합을 Union 할 때 집합의 높이 h를 작게 할 수 있다면, 시간 복잡도가 줄어들 수 있다. ( weighted Union )

- 노드 개수가 작은 트리의 루트를 노드 개수가 큰 트리의 자식으로 만들면 트리의 높이가 적어도 커지지는 않을 것이다.

( 물론 노드 개수가 많아도 높이가 작은 트리도 있지만, 너무 극단적인 예시이니 신경쓰지 않도록 한다. )

여기서 size 배열은 각 트리의 노드 개수를 위해 따로 카운트 하고 있는 배열.

void Union(int(&parent)[10], int a, int b) {
	a = findSet(parent, a);
	b = findSet(parent, b);
	if (::size[a] < ::size[b]) { 
		parent[a] = b; 
		::size[b] = ::size[a] + ::size[b];
	}
	else { 
		parent[b] = a;
		::size[a] = ::size[a] + ::size[b];
	}
}

[ 최종 구현 ]

 

ref :: http://https://m.blog.naver.com/ndb796/221230994142

동빈나 님의 블로그 글을 보고 참고한 Kruskal 알고리즘.

- 각 SET 별로 size 배열에 size를 저장하여 union 시 size가 큰 집합 밑으로 size가 작은 집합이 들어갈수 있도록 하였다.

 

[ 최종 구현 ]

더보기
#include<iostream>
#include<vector>
#include<algorithm>

int parent[1001];
int p_size[1001];

class Edge {
public:
	int node[2];  // 엣지를 구성하고 있는 두 
	int distance;
	int p;

	Edge(int a, int b, int _distance): distance(_distance) {
		node[0] = a;
		node[1] = b;
	};
	bool operator <(Edge& edge) {
		return distance < edge.distance;
	}
};

int findSet(int* parent, int x) {
	if (parent[x] != x) {
		parent[x] = findSet(parent, parent[x]);
	}
	return parent[x];
}
void Union(int* parent, int a, int b) {
	a = findSet(parent, a);
	b = findSet(parent, b);
	if (p_size[a] > p_size[b]) {
		parent[b] = a;
		p_size[a] = p_size[a] +p_size[b];
	}
	else {
		parent[a] = b;
		p_size[b] = p_size[b] + p_size[b];
	}
}

int main() {
	int n = 7;
	int m = 11;

	std::vector<Edge> v;
	v.push_back(Edge(1, 7, 12));
	v.push_back(Edge(5, 7, 73));
	v.push_back(Edge(4, 7, 13));
	v.push_back(Edge(1, 4, 28));
	v.push_back(Edge(1, 2, 67));
	v.push_back(Edge(1, 5, 17));
	v.push_back(Edge(4, 2, 24));
	v.push_back(Edge(2, 5, 62));
	v.push_back(Edge(5, 6, 45));
	v.push_back(Edge(5, 3, 20));
	v.push_back(Edge(6, 3, 37));
	v.push_back(Edge(1, 7, 12));
	v.push_back(Edge(1, 7, 12));


	// 간선의 비용을 기준으로 오름차순 정렬
	std::sort(v.begin(), v.end());

	// 각 노드의 부모 노드를 배열로 나타냄.
	// 초기에는 자기 자신을 부모 노드로 설정.
	for (int i = 0; i < 7; i++) {
		parent[i] = i;
	}
	std::fill_n(p_size, 7, 1);

	int sum = 0; // 거리의 합.

	//모든 간선에 대하여 사이클이 발생하지 않는 경우 그래프를 합쳐준다.
	for (int i = 0; i < v.size(); i++) {
		
		int a = v[i].node[0];
		int b = v[i].node[1];

	// 간선의 끝점이 서로 다른 집합의 노드일때 두 노드의 집합 합쳐줌.
		if (findSet(parent, a) != findSet(parent, b) ){
			Union(parent,a,b);
			sum += v[i].distance;
		}
	}
	std::cout <<"최소 길이는 :: "<< sum;
	return 0;
}

관련 알고리즘 문제 .

 

< Kruskal 문제 >

프로그래머스 섬 연결하기 문제. https://blog.naver.com/wldlf94/222526662101

백준 최소 스패닝 트리 - 1197번 https://www.acmicpc.net/problem/1197

백준 네트워크 연결 - 1922번 https://www.acmicpc.net/problem/1922

'CS > Algorithm' 카테고리의 다른 글

최단 경로 ② ( Floyd - Warshall )  (0) 2021.11.17
최단 경로 & 그래프 문제 유형  (0) 2021.11.17
MST ( Minimum Spanning Tree ) - Kruskal , Prim  (0) 2021.11.11
BFS ( Breadth First Search )  (0) 2021.11.10
DFS ( Depth First Search )  (0) 2021.11.10

+ Recent posts