본문 바로가기

카테고리 없음

자료구조) 프림,크루스칼 알고리즘

최소 신장 트리 문제(MST; Minimum Spanning Tree)

하나의 연결 성분으로 이루어진 무방향 가중치 그래프에서 간선의 가중치 합이 최소인 신장 트리

더보기
    • 즉, 주어진 가중치 그래프에 존재하는 모든 정점을 연결되게끔 하는 트리 가운데, 가중치의 합이 최소
    • 간선에 가중치가 있는 그래프 = 가중치 그래프

 

 
 
 
 
왜 트리인가?

더보기

트리 조건

  • 주어진 그래프는 연결 그래프
  • 주어진 그래프는 Cycle을 가지지 않음

Cycle을 이루면 가중치 최소가 절대 되지 않는다.

  • C, E, D로 이루어지는 Cycle에서 하나의 간선을 제외해도 각 정점은 이어짐

-> 불필요한 간선 추가되었다는 뜻

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Greedy Algorithm(그리디 알고리즘)

  • 최적해를 구하는 데에 사용되는 근사적인 방법
  • 여러 경우 중 하나를 결정해야 할 때마다 그 순간에 최적이라고 생각되는 것으로 선택해 나가는 방식

 
 
 

Prim Algorithm(프림 알고리즘)

  • 임의의 정점에서 가장 적은 비용으로 이을 수 있는 정점 추가
  • 만들어진 트리에서 가장 적은 비용으로 트리에 이을 수 있는 정점 선택
  • 과정 반복하여 MST 생성

(접은 글) 예제

더보기
이 가중 그래프에서 최소 비용 신장 트리를 구성했을 때, 가중치의 합을 구해보자.

 

 

 

 

이 가중 그래프에서 최소 비용 신장 트리를 구성했을 때,

가중치의 합을 구해보자.

 

 

 

 

 

 

  1.  4를 임의의 정점으로 잡았다. 가장 적은 비용(1)으로 이을 수 있는 6을 추가한다.
  2.  4와 6으로 이어진 트리에서 가장 적은 비용(3)으로 이을 수 있는 정점 1을 추가한다.
  3.  4,6,1로 이어진 트리에서 가장 적은 비용(6)로 이을 수 있는 정점 5를 추가한다.   
  4.  4,6,1,5로 이어진 트리에서 가장 적은 비용(2)로 이을 수 있는 정점 2를 추가한다.
  5.  4,6,1,5,2로 이어진 트리에서 가장 적은 비용(4)로 이을 수 있는 정점 3을 추가한다.
  6.  4,6,1,5,2,3으로 이어진 트리에서 가장 적은 비용(9)로 이을 수 있는 정점 0을 추가한다.

따라서, 가중치의 합은 1+3+6+2+4+9 = 25이다.

풀이과정 ( 노란색 -> 만들어진 트리, 빨간색 -> 가장 적은 비용으로 이을 수 있는 정점)

 

 

결과

 
 
 
 
Prim Algorithm 구현

  • 아직 연결되지 않은 노드들에 대해서 연결 비용 업데이트
  • 그 중 최솟값 취함

(접은 글) 예제

더보기
구현 과정

 

 

결과

 
 
 
 
Prim Algorithm 시간복잡도
O(2(V-1) * V) = O(V^2)

더보기

정점의 개수를 V라고 가정.

1. 하나의 임의의 정점을 고르고, 이와 연결되지 않은 정점들과의 가중치 업데이트 : V-1번 수행

2. 그 중 최솟값 찾기 : V-1번 비교 수행

3. 위 과정 모든 정점 V에 대해 수행

 
 
 
Prim Algorithm 성능 향상
-> 우선순위 큐 이용하여 데이터 추가하면서 계속 최솟값 찾기

더보기

우선 순위 큐(Priority Queue)

- 들어온 순서와 무관하게 우선 순위가 높은 원소부터 나감

- 맥스힙을 이용하여 구현

  • 힙에서 데이터 추가 및 루트 노드 삭제 시, O(logN)의 시간복잡도를 가짐
  • 따라서, 힙 이용하여 우선 순위 큐 구현하면 빠르게 수행 가능

 
 
 
 
우선 순위 큐를 이용한 후 총 시간복잡도 (간선의 수는 E, 정점의 개수는 V)
 
O(ElogE + ElogE) = O(2ElogE)
이 때, 간선의 수는 최대 V(V-1)/2개
즉, E <= V^2
 
따라서, O(ElogV^2) = O(2ElogV) = O(ElogV) 
-> 개선됨
그러나.. 모든 간선이 다 연결되어 있으면 O(V^2logV)이므로 성능 더 나빠짐...
그치만... 모든 간선이 다 연결되어 있을 경우가 적기 때문에 성능이 좋아졌다고 하는거
 
접은 글 참고

더보기

큐를 이용한 후 성능 향상

1. heappush 한 번 할 때의 시간복잡도는?

**N개의 원소가 있을 때의 맥스힙의 시간복잡도: O(logN)

힙에는 최대 E개만큼 들어갈 수 있음 (즉, E개의 원소)

따라서 heappush의 시간복잡도는 O(logE)

 

2. (OlogE)의 시간복잡도를 가진 heappush를 E번 수행

 

따라서 heappush연산은 총 O(logE)만큼의 시간복잡도

 

heap에서 pop하면서 정점 연결 시에도 간선의 개수 E만큼 heappop 수행

1. heappop 한 번 할 때의 시간복잡도는

O(logE)

2. O(logE)의 시간복잡도를 가진 heappop을 E번 수행

 

따라서 heappop연산은 총 O(ElogE)만큼의 시간복잡도

 
 
 
Prim Algorithm 구현
- 큐 이용

더보기
from heapq import heappush, heappop

 

G = [[0, 30, 42, 0, 0],
    [30, 0, 17, 28, 0], 
    [42, 17, 0, 22, 19],
    [0, 28, 22, 0, 15],
    [0, 0, 19, 15, 0]]

V = ["A", "B", "C", "D", "E"]
def Prim_Algorithm(graph, graph_v, start):
    h = list() #힙 정의
    connect = list()
    for i in range(0, len(graph)):
        connect.append(False) #각 정점이 연결되었냐, 안 되었냐 표시, 
    heappush(h, (0,start)) #힙 h에 (0,start)를 push (start 연결하는데 비용 0)
    total_weight = 0 #가중치 합 저장
    vertex_count = 0 #정점이 모두 포함되면 끝이므로 개수 세기 위해 필요
    while vertex_count < len(graph): #정점 모두 포함되기 전까지 while문 반복
        pop_info = heappop(h) #가장 우선순위에 있는 데이터를 pop(가중치의 최솟값 반환)
        pop_weight = pop_info[0]
        pop_vertex = pop_info[1] #pop한 노드 정보 저장
        if connect[pop_vertex] == False:
            connect[pop_vertex] = True #pop한 노드 연결 안 되어있으면(False) 연결(True)
            total_weight = total_weight + pop_weight
            vertex_count = vertex_count + 1 #가중치 합에 추가하고, 정점 수에도 하나 추가
            print('새로 연결된 곳:', graph_v[pop_vertex])
            print('누적 가중치 합:', total_weight)
            for i in range(0, len(graph)): #아직 연결되지 않은 노드들에 대해서 연결 비용 업데이트
                if (graph[pop_vertex][i] != 0) and (connect[i] == False): #그래프 내 i번째 정점과 pop한 정잠과 연결되어 있고, 아직 연결되지 않았다면
                    heappush(h, (graph[pop_vertex][i], i)) #(해당 가중치, 해당 정점)을 push

 
 
 
ex)

 
 

Kruskal Algorithm(크루스칼 알고리즘)

  • 그래프에 존재하는 모든 간선들을 가중치 순으로 나열
  • 그래프 내 정점이 모두 이어질 때까지 가중치가 낮은 간선 순서로 선택
    • 가장 가중치가 작은 간선을 트리에 추가하여 사이클을 만들지 않으면 트리 간선으로 선택
    • 사이클을 만들면 버리는 것을 반복
    • n-1개의 간선이 선택되면 알고리즘 종료

Kruskal Algorithm 구현

  • 가중치 순서로 나열 -> sort() 함수 이용
  • cycle 형성 여부 어떻게 판단?

 

def Kruskal_Algorithm(edges, vertexes):
    edge_count = 0 #이어진 간선의 수 세기 (간선의 수가 정점 -1개이면 break)
    total_weight = 0 #최종 가중치
    union = dict() #딕셔너리 이용해서 집합 표기
    for each_vertex in vertexes:
        union[each_vertex] = each_vertex #초기값 셋팅 dict = {'A':'A','B':'B','C':'C','D':'D','E':'E'}
    edges.sort() #간선의 가중치 오름차순 순으로 정렬 O(ElogE) = O(ElogV)
    for each_edge in edges: #정렬된 edges 차례로 돌기
        if union[each_edge[1]] != union[each_edge[2]]: #each_edge 형태 = [weight, node1, node2] (node1과 node2가 같은 value 값을 가지는가?
            total_weight = total_weight + each_edge[0]
            edge_count = edge_count + 1 #같은 집합에 없으면 잇기. 최종 가중치에 추가, edge_count에도 1 추가
            print(each_edge[1], '과', each_edge[2], '이(가) 연결되었습니다.')
            print('누적 가중치 합은', total_weight, '입니다.')
            for each_vertex in vertexes:
                if union[each_vertex] == union[each_edge[2]]:
                    union[each_vertex] = union[each_edge[1]] #간선을 연결했으므로 정점들은 같은 집합이 됨. 해당 정점들의 value 값 일치시키기
        if edge_count >= len(vertexes) - 1:
            break #연결한 간선의 수가 정점의 수 -1 이면 break

 
 
ex)