본문 바로가기

알고리즘/개념

DISJOINT-SETS, MST

반응형

MST(최소 신장 트리)

MST의 목적

  • 낭비를 줄이고 최적의 비용으로 모두를 연결하기 위함
  • 그리디한 기법을 쓴다.
  • 팩토리얼 보다 적게 만들기 위해서 쓴다.
  • 모든 경로의 합이 최소를 찾기 위해서 쓴다.(나중에 나올 다익스트라와 용도가 다르다.)

MST를 만들기 위한 기법은 두가지

  • 프림, 크루스칼
  • 프림
    • disjoint-sets을 기반으로 깔고 들어간다.
  • 크루스칼
    • 크루스칼에서 다음 노드를 선택했을 때 그 노드를 union한다. 또한 사이클이 발생할 수 있는 경우를 방지한다.
    • 트리이기 때문에 사이클이 발생하지 않아야 한다.
    • 간선이 많으면 터진다.

간선이 적으면 크루스칼, 간선이 많으면 프림

 

 


 

 

DISJOINT-SETS

 

서로소 집합을 의미한다.

각 집합의 부모 노드를 가지고 있고 부모 노드가 다르면 다른 집합을 의미한다.

 

 

  • 그룹의 대표자를 만드는 Union
void union(int x, int y) {
    int a = find(x);
    int b = find(y);

    if(a!=b)
        parent[b] = a;
}

union을 조금 더 효율적으로 사용하고 싶다면 size를 도입하여 size 작은 것을 size큰 쪽에 합치는 형식으로 사용하면 된다.

// 사용하주기 전에 먼저 모든 사이즈를 1로 초기화해준다.
for(int i = 0; i <= n; i++) size[i] = 1;

void union(int x, int y) {
  int a = find(x);
  int b = find(y);

  if(size[a] < size[b]) swap(a,b);
  size[a] += size[b];
  parent[b] = a;
}

 

  • 대표자를 찾는 Find

    int find(int x) {
      while(x != parent[x])
        x = parent[x];
      return x;
    }
    
    // 경로 압축(Path compression)
    static int find(int x) {
          // 배열칸에 들어있는 값이 자신과 같으면 대표.
          if(x == disjoint[x]) return x; 
          // 아니면 최종 대표의 값을 배열에 넣어준다.
          return disjoint[x] = find(disjoint[x]); 
      }

     

 

약간 다른 방법으로 하는 union-find

void makeSet() {
	for(int i = 0; i <= N; i++) {
    	p[i] = -1;
    }
}

int find(int x) {
	if(p[x] < 0) return x;
    return p[x] = find(p[x]);
}

void union(int x, int y) {
	int a = find(x);
    int b = find(y);
    if(a==b) return;
    p[a]+=p[b];
    p[b]=a;
}

이렇게 하면 부모의 값에 크기가 들어가 있고 그 외에는 부모를 가리키는 값을 가진다.

 

 


 

 

union find를 이용한 크루스칼 알고리즘

  • 크루스칼 알고리즘은 탐욕법 기반으로 간선을 추가해 가면서 최소 신장 트리를 만드는 방법이다.
  • 처음에는 그래프의 노드만 포함하고 간선이 하나도 포함되어 있지 않은 상태로 시작한다.
  • 그런 다음 간선을 가중치 순으로 하나씩 살펴보면서, 간선을 추가해도 사이클이 생기지 않으면(find) 이를 신장 트리에 추가하는 과정을 반복한다.
  • 간선을 하나씩 추가할 때마다 컴포넌트 두 개를 합친다.(union)

  • 구현

    • 크루스칼 알고리즘을 구현할 때는 그래프의 표현 방식 중 간선 리스트를 사용하는 것이 편리하다.
    • 첫 번째 단계에서는 간선을 O(mlogm) 시간에 정렬한다.(정렬을 하지 않고 하는 방법에는 우선순위 큐를 사용하는 방법이 있다. 우선순위 큐에 넣고 신장 트리를 만들 때 간선의 개수가 n-1개가 되는 순간 큐를 비우고 종료하면 된다. 이렇게 해서 시간을 줄일 수도 있다.)
    • 두번째 단계에서는 다음 코드와 같이 최소 신장 트리를 만든다.
    for(...) {
      if(!same(a,b)) union(a,b); // 이때 간선을 가중치를 더해주면 최소 값을 구할 수 있다.
    }
    • same과 union함수를 유니온-파인드를 이용하면 크루스칼 알고리즘의 시간 복잡도는 O(mlogn)이 된다.
    boolean same(int a, int b) {
      return find(a) == find(b);
    }

     


 

 

프림 알고리즘

  • 임의의 노드를 트리에 추가하고, 새로운 노드와 트리를 연결하는 노드 중 가중치가 가장 작은 간선을 선택한다.
  • 우선순위 큐를 이용하여 효율적으로 구현할 수 있다.
  • 프림 알고리즘의 시간 복잡도는 O(n+mlogm)으로 다익스트라 알고리즘과 같다.
  • 다수의 경진 프로그래머가 크루스칼 알고리즘을 선호한다.
  • 간선이 많을 때 사용한다.
boolean[] check = new boolean[V];
int[] key = new int[V]; // 현재 선택된 정점들로부터 도달할 수 있는 최소거리
int[] p = new int[V]; // 최소신장트리의 구조를 저장할 배열
Arrays.fill(key, Integer.MAX_VALUE);

// 아무거나 하나 선택! (처음선택되는 친구가 루트이므로, 부모는 없는걸로, 거리는 0인걸로)
p[0] = -1;
key[0] = 0;

// 이미 하나를 골랐으므로 V-1를 고른다.
for(int i = 0; i < V-1 ; i++) {
	int min = Integer.MAX_VALUE;
	int idx = -1;
	// 안골라진 친구들 중에서 key가 가장 작은 친구를 뽑는다.
	for(int j = 0; j < V; j++) {
		if(!check[j] && key[j] < min) {
			idx = j;
			min = key[j];
		}
	}
	// idx: 선택이 안된 정점 중 key가 제일 작은 친구가 들어있다.
	check[idx] = true;
	// idx에서 출발하는 모든 간선에 대해서 key를 업데이트
	for(int j = 0; j < V; j++) {
		// check가 안되어있으면서 간선은 존재하고 그 간선이 key값보다 작다면 key값을 업데이트
		if(!check[j] && adj[idx][j] != 0 && key[j] > adj[idx][j]) {
			key[j] = adj[idx][j];
			p[j] = idx;
		}
	}
}

'알고리즘 > 개념' 카테고리의 다른 글

투 포인터  (0) 2020.05.03
구간 트리(세그먼트 트리)  (0) 2020.04.18
다익스트라,벨만포드,플로이드 워샬  (0) 2020.04.16
Permutation & Combination  (0) 2020.02.18