- 트리 자료구조는 최소 힙으로 항상 부모 노드가 자식 노드보다 크기가 작은 자료구조이다.
@ 서로소 집합
서로소 집합이란?
- 공통 원소가 없는 두 집합
서로소 집합 자료구조(union-find 자료구조)란?
- 서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조
- union : 2개의 원소가 포함된 집합을 하나의 집합으로 합치는 연산
- find : 특정한 원소가 속한 집합이 어떤 집합인지 알려주는 연산
[트리를 이용해 서로소 집합을 계산하는 알고리즘]
- union 연산을 확인하여, 서로 연결된 두 노드 A, B를 확인한다.
- A와 B의 루트 노드 A', B'를 각각 찾는다.
- 보통 작은 번호가 부모, 큰 번호가 자식이 된다.
- A'를 B'의 부모 노드로 설정한다.(B'가 A'를 가리키도록 한다.)
- 모든 union 연산을 처리할 때까지 1번 과정을 반복한다.
#개선된 서로소 집합 알고리즘
#시간복잡도 : O(V+M(1+log_{2-M/V}V) : 노드가 1000개 일 때, 1000만 번 연산
'''
6 4
1 4
2 3
2 4
5 6
'''
def find_parent(parent, x): #특정 원소가 속한 집합을 찾기
if parent[x] != x: #루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
parent[x] = find_parent(parent, parent[x])
return parent[x] #바뀐 곳. 경로 압축 기법 : 여러 개의 원소가 모두 같은 집합에 속하는 경우
def union_parent(parent, a, b): #두 원소가 속한 집합을 합치기
a = find_parent(parent, a)
b = find_parent(parent, b)
if a<b:
parent[b] = a
else:
parent[a] = b
v, e = map(int, input().split())
parent = [0] * (v+1) #부모 테이블 초기화
for i in range(1, v+1): #부모 테이블 상에서, 부모를 자기 자신으로 초기화
parent[i] = i
for i in range(e):
a,b = map(int, input().split())
union_parent(parent, a, b) #union 연산 각각 수행
print('각 원소가 속한 집합: ', end='')
for i in range(1, v+1):
print(find_parent(parent, i), end=' ')
print()
print('부모 테이블: ', end='')
for i in range(1, v+1):
print(parent[i], end=' ')
[서로소 집합을 활용한 사이클 판별]
- 무향 그래프에서만 적용 가능
- 각 간선을 확인하며 두 노드의 루트 노드를 확인한다.
- 루트 노드가 서로 다르다면 두 노드에 대하여 union 연산을 수행한다.
- 루트 노드가 서로 같다면 사이클이 발생한 것이다.
- 그래프에 포함되어 있는 모든 간선에 대하여 1번 과정을 반복한다.
'''
3 3
1 2
1 3
2 3
'''
def find_parent(parent, x):
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
def union_parent(parent, a, b):
a = find_parent(parent, a)
b = find_parent(parent, b)
if a<b:
parent[b] = a
else:
parent[a] = b
v, e = map(int, input().split())
parent = [0] * (v+1)
for i in range(1, v+1):
parent[i] = i
cycle = False
for i in range(e):
a,b = map(int, input().split())
if find_parent(parent,a) == find_parent(parent,b):
cycle = True
break
else:
union_parent(parent, a, b)
if cycle:
print("사이클이 발생했습니다.")
else:
print('사이클이 발생하지 않았습니다.')
@ 신장 트리
- Spanning Tree, 또는 신장 트리 라고 불리움 (Spanning Tree가 보다 자연스러워 보임)
- 원래의 그래프의 모든 노드가 연결되어 있으면서 트리의 속성을 만족하는 그래프
- 신장 트리의 조건
- 본래의 그래프의 모든 노드를 포함해야 함
- 모든 노드가 서로 연결
- 트리의 속성을 만족시킴 (사이클이 존재하지 않음) - 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프
최소신장트리란?
- Minimum Spanning Tree, MST 라고 불리움
- 가능한 Spanning Tree 중에서, 간선의 가중치 합이 최소인 Spanning Tree를 지칭함
- 그래프에서 최소 신장 트리를 찾을 수 있는 알고리즘이 존재함
- 대표적인 최소 신장 트리 알고리즘
- Kruskal’s algorithm (크루스칼 알고리즘), Prim's algorithm (프림 알고리즘)
크루스칼 알고리즘이란?
- 모든 정점을 독립적인 집합으로 만든다.
- 모든 간선을 비용을 기준으로 정렬하고, 비용이 작은 간선부터 양 끝의 두 정점을 비교한다.
- 두 정점의 최상위 정점을 확인하고, 서로 다를 경우 두 정점을 연결한다. (최소 신장 트리는 사이클이 없으므로, 사이클이 생기지 않도록 하는 것임)
> 탐욕 알고리즘을 기초로 하고 있음 (당장 눈 앞의 최소 비용을 선택해서, 결과적으로 최적의 솔루션을 찾음)
- 신장 트리 중에서 최소 비용으로 만들 수 있는 신장 트리를 찾는 대표적인 알고리즘
- 시간 복잡도 : O(E log E)
- - 다음 단계에서 2번, 간선을 비용 기준으로 정렬하는 시간에 좌우됨 (즉 간선을 비용 기준으로 정렬하는 시간이 가장 큼)
- 1. 모든 정점을 독립적인 집합으로 만든다.
- 2. 모든 간선을 비용을 기준으로 정렬하고, 비용이 작은 간선부터 양 끝의 두 정점을 비교한다.
- - 퀵소트를 사용한다면 시간 복잡도는 O(n log n) 이며, 간선이 n 이므로 O(E log E)
- 3. 두 정점의 최상위 정점을 확인하고, 서로 다를 경우 두 정점을 연결한다. (최소 신장 트리는 사이클이 없으므로, 사이클이 생기지 않도록 하는 것임)
- - union-by-rank 와 path compression 기법 사용시 시간 복잡도가 결국 상수값에 가까움, O(1)
- 간선 데이터를 비용에 따라 오름차순으로 정렬한다.
- 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인한다.
- 사이클이 발생하지 않는 경우 최소 신장 트리에 포함시킨다.
- 사이클이 발생하는 경우 최소 신장 트리에 포함시키지 않는다.
'''
7 9
1 2 29
1 5 75
2 3 35
2 6 34
3 4 7
4 6 23
4 7 13
5 6 53
6 7 25
'''
def find_parent(parent, x):
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
def union_parent(parent, a, b):
a = find_parent(parent, a)
b = find_parent(parent, b)
if a<b:
parent[b] = a
else:
parent[a] = b
v, e = map(int, input().split())
parent = [0] * (v+1)
edges = [] #모든 간선을 담을 리스트
result = 0 #최종 비용을 담을 변수
for i in range(1, v+1):
parent[i] = i
for i in range(e):
a,b,cost = map(int, input().split())
edges.append((cost,a,b)) #비용순으로 정렬하기 위해서 튜플의 첫 번째 원소를 비용으로 설정
edges.sort() #간선을 비용순으로 정렬
for edge in edges: #간선을 하나씩 확인하며
cost, a, b = edge
if find_parent(parent, a) != find_parent(parent, b): #사이클이 발생하지 않는 경우에만 집합에 포함
union_parent(parent, a, b)
result += cost
print(result)
#패스트캠퍼스
mygraph = {
'vertices': ['A', 'B', 'C', 'D', 'E', 'F', 'G'],
'edges': [
(7, 'A', 'B'),
(5, 'A', 'D'),
(7, 'B', 'A'),
(8, 'B', 'C'),
(9, 'B', 'D'),
(7, 'B', 'E'),
(8, 'C', 'B'),
(5, 'C', 'E'),
(5, 'D', 'A'),
(9, 'D', 'B'),
(7, 'D', 'E'),
(6, 'D', 'F'),
(7, 'E', 'B'),
(5, 'E', 'C'),
(7, 'E', 'D'),
(8, 'E', 'F'),
(9, 'E', 'G'),
(6, 'F', 'D'),
(8, 'F', 'E'),
(11, 'F', 'G'),
(9, 'G', 'E'),
(11, 'G', 'F')
]
}
parent = dict()
rank = dict()
def find(node):
# path compression 기법
if parent[node] != node:
parent[node] = find(parent[node])
return parent[node]
def union(node_v, node_u):
root1 = find(node_v)
root2 = find(node_u)
# union-by-rank 기법
if rank[root1] > rank[root2]:
parent[root2] = root1
else:
parent[root1] = root2
if rank[root1] == rank[root2]:
rank[root2] += 1
def make_set(node):
parent[node] = node
rank[node] = 0
def kruskal(graph):
mst = list()
# 1. 초기화
for node in graph['vertices']:
make_set(node)
# 2. 간선 weight 기반 sorting
edges = graph['edges']
edges.sort()
# 3. 간선 연결 (사이클 없는)
for edge in edges:
weight, node_v, node_u = edge
if find(node_v) != find(node_u):
union(node_v, node_u)
mst.append(edge)
return mst
프림 알고리즘이란?
- 시작 정점을 선택한 후, 정점에 인접한 간선중 최소 간선으로 연결된 정점을 선택하고, 해당 정점에서 다시 최소 간선으로 연결된 정점을 선택하는 방식으로 최소 신장 트리를 확장해가는 방식
- Kruskal's algorithm 과 Prim's algorithm 비교
- 둘다, 탐욕 알고리즘을 기초로 하고 있음 (당장 눈 앞의 최소 비용을 선택해서, 결과적으로 최적의 솔루션을 찾음)
- Kruskal's algorithm은 가장 가중치가 작은 간선부터 선택하면서 MST를 구함
- Prim's algorithm은 특정 정점에서 시작, 해당 정점에 연결된 가장 가중치가 작은 간선을 선택, 간선으로 연결된 정점들에 연결된 간선 중에서 가장 가중치가 작은 간선을 택하는 방식으로 MST를 구함
- 임의의 정점을 선택, '연결된 노드 집합'에 삽입
- 선택된 정점에 연결된 간선들을 간선 리스트에 삽입
- 간선 리스트에서 최소 가중치를 가지는 간선부터 추출해서,
- 해당 간선에 연결된 인접 정점이 '연결된 노드 집합'에 이미 들어 있다면, 스킵함(cycle 발생을 막기 위함)
- 해당 간선에 연결된 인접 정점이 '연결된 노드 집합'에 들어 있지 않으면, 해당 간선을 선택하고, 해당 간선 정보를 '최소 신장 트리'에 삽입 - 추출한 간선은 간선 리스트에서 제거
- 간선 리스트에 더 이상의 간선이 없을 때까지 3-4번을 반복
- 최악의 경우, while 구문에서 모든 간선에 대해 반복하고, 최소 힙 구조를 사용하므로 O($ElogE$) 시간 복잡도를 가짐
myedges = [
(7, 'A', 'B'), (5, 'A', 'D'),
(8, 'B', 'C'), (9, 'B', 'D'), (7, 'B', 'E'),
(5, 'C', 'E'),
(7, 'D', 'E'), (6, 'D', 'F'),
(8, 'E', 'F'), (9, 'E', 'G'),
(11, 'F', 'G')
]
from collections import defaultdict
from heapq import *
def prim(start_node, edges):
mst = list()
adjacent_edges = defaultdict(list)
for weight, n1, n2 in edges:
adjacent_edges[n1].append((weight, n1, n2))
adjacent_edges[n2].append((weight, n2, n1))
connected_nodes = set(start_node)
candidate_edge_list = adjacent_edges[start_node]
heapify(candidate_edge_list)
while candidate_edge_list:
weight, n1, n2 = heappop(candidate_edge_list)
if n2 not in connected_nodes:
connected_nodes.add(n2)
mst.append((weight, n1, n2))
for edge in adjacent_edges[n2]:
if edge[2] not in connected_nodes:
heappush(candidate_edge_list, edge)
return mst
개선된 프림 알고리즘이란?
- 간선이 아닌 노드를 중심으로 우선순위 큐를 적용하는 방식
- 초기화 - 정점:key 구조를 만들어놓고, 특정 정점의 key값은 0, 이외의 정점들의 key값은 무한대로 놓음. 모든 정점:key 값은 우선순위 큐에 넣음
- 가장 key값이 적은 정점:key를 추출한 후(pop 하므로 해당 정점:key 정보는 우선순위 큐에서 삭제됨), (extract min 로직이라고 부름)
- 해당 정점의 인접한 정점들에 대해 key 값과 연결된 가중치 값을 비교하여 key값이 작으면 해당 정점:key 값을 갱신
- 정점:key 값 갱신시, 우선순위 큐는 최소 key값을 가지는 정점:key 를 루트노드로 올려놓도록 재구성함 (decrease key 로직이라고 부름)
- 개선된 프림 알고리즘 구현시 고려 사항
- 우선순위 큐(최소힙) 구조에서, 이미 들어가 있는 데이터의 값 변경시, 최소값을 가지는 데이터를 루트노드로 올려놓도록 재구성하는 기능이 필요함
- 구현 복잡도를 줄이기 위해, heapdict 라이브러리를 통해, 해당 기능을 간단히 구현
- 개선된 프림 알고리즘의 시간 복잡도: O(ElogV)
from heapdict import heapdict
def prim(graph, start):
mst, keys, pi, total_weight = list(), heapdict(), dict(), 0
for node in graph.keys():
keys[node] = float('inf')
pi[node] = None
keys[start], pi[start] = 0, start
while keys:
current_node, current_key = keys.popitem()
mst.append([pi[current_node], current_node, current_key])
total_weight += current_key
for adjacent, weight in mygraph[current_node].items():
if adjacent in keys and weight < keys[adjacent]:
keys[adjacent] = weight
pi[adjacent] = current_node
return mst, total_weight
mygraph = {
'A': {'B': 7, 'D': 5},
'B': {'A': 7, 'D': 9, 'C': 8, 'E': 7},
'C': {'B': 8, 'E': 5},
'D': {'A': 5, 'B': 9, 'E': 7, 'F': 6},
'E': {'B': 7, 'C': 5, 'D': 7, 'F': 8, 'G': 9},
'F': {'D': 6, 'E': 8, 'G': 11},
'G': {'E': 9, 'F': 11}
}
mst, total_weight = prim(mygraph, 'A')
print ('MST:', mst)
print ('Total Weight:', total_weight)
@위상 정렬
- 방향 그래프의 모든 노드를 '방향성에 거스르지 않도록 순서대로 나열하는 것'
- 진입 차수(Indegree) : 특정한 노드로 들어오는 간선의 개수
- 시간 복잡도 : O(V+E)
- 진입 차수가 0인 노드를 큐에 넣는다.
- 큐가 빌 때까지 다음의 과정을 반복한다.
- 큐에서 원소를 꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거한다.
- 새롭게 진입차수가 0이 된 노드를 큐에 넣는다.
'''
7 8
1 2
1 5
2 3
2 6
3 4
4 7
5 6
6 4
'''
from collections import deque
v,e = map(int, input().split())
indegree = [0] * (v+1)
graph = [[] for i in range(v+1)]
for _ in range(e):
a,b = map(int, input().split())
graph[a].append(b)
indegree[b] += 1
def topology_sort():
result = []
q = deque()
for i in range(1, v+1):
if indegree[i]==0:
q.append(i)
while q:
now = q.popleft()
result.append(now)
for i in graph[now]:
indegree[i] -= 1
if indegree[i] == 0:
q.append(i)
for i in result:
print(i, end=' ')
topology_sort()
'두두의 알고리즘 > 공부' 카테고리의 다른 글
[알고리즘] 소수의 판별 / 투 포인터 / 구간 합 계산 / 순열과 조합 (0) | 2021.12.22 |
---|---|
[복잡도] 시간 복잡도 / 공간 복잡도 (0) | 2021.12.21 |
[알고리즘] 그래프 개념, 그래프 vs 트리 (0) | 2021.11.29 |
[알고리즘] 동적 계획법/다이나믹 프로그래밍(DP) - 점화식 (0) | 2021.11.29 |
[알고리즘] 구현 (0) | 2021.11.25 |