일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 백준
- Doitvue.js입문
- 리액트
- 프로그래밍
- dp
- web
- 웹프로그래밍
- LeetCode
- C++
- typescript
- Level2
- 프로그래머스
- 카카오
- 자바스크립트
- python
- 배열
- 파이썬
- 동적계획법
- javascript
- VUE
- Medium
- sql
- Level3
- 코테연습
- Level1
- 고득점Kit
- 리트코드
- react
- OS
- CS
- Today
- Total
그래프 이론 (크루스칼, 프림, 위상정렬 알고리즘) - python 본문
그래프 이론
서로소 집합
공통 원소가 없는, 상호 배타적인 부분 집합들로 나눠진 원소들에 대한 정보를 저장하고 조작하는 자료 구조가 바로 유니온- 파인드 자료구조이다.
서로소 집합 자료구조
- union
- 2개의 원소가 포함된 집합을 하나의 집합으로 합치는 연산
- find
- 특정 한 원소가 속한 집합이 어떤 집합인지 알려주는 연산
서로소 집합 정보가 주어졌을때 트리 자료 구조를 이용해서 집합을 표현한다.
서로소 집합 알고리즘
- 각 노드는 자기 자신을 부모 노드로 갖도록 초기화 한다.
- union을 확인하여, 서로 연결된 두 노드 A,B를 확인한다.
- A와 B의 루트 노트 A'와 B'를 각각 찾는다.
- A'를 B'의 부모 노드로 설정한다. (B'가 A'를 가르키도록 한다.)
- 트리가 한쪽으로 기울어지는 것을 방지하기 위해서 항상 높이가 더 낮은 트리를 더 높은 트리 밑에 집어 넣으므로써 최적화 하기도 한다 (랭크에 의한 합치기 최적화)
- 랭크는 어떤 노드가 해당 트리의 루트인 경우 해당 트리의 높이를 저장한다.
- 두 노드를 합칠 때 높이를 비교해서 낮은 쪽을 높은 트리의 서브트리로 포함하되, 두 트리의 높이가 같은 경우에만 결과 트리의 높이를 1 늘려 준다.
- 트리가 한쪽으로 기울어지는 것을 방지하기 위해서 항상 높이가 더 낮은 트리를 더 높은 트리 밑에 집어 넣으므로써 최적화 하기도 한다 (랭크에 의한 합치기 최적화)
- 더 번호가 작은 원소가 부모 노드가 되도록 구현한다.
- 모든 union(합집합) 연산을 처리할 때까지 1번 과정을 반복한다.
#특정 원소가 속한 집합 찾기
def find_parent(parent,x):
if parent[x] != x : # 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
return 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
#union 연산을 각각 수행
for i in range(e):
a,b = map(int,input().split())
union_parent(parent,a,b)
서로소 집합을 활용한 사이클 판별
무방향 그래프 내에서 사이클을 판별할 때 사용할 수 있다. (방향 그래프일 경우 DFS 사용)
- 각 간선을 확인하며 두 노드의 루트 노드를 확인한다.
- 루트 노드가 서로 다르다면 두 노드에 대하여 union 연산을 수행한다.
- 루트 노드가 서로 같다면 사이클이 발생한 것이다.
- 그래프에 포함되어 있는 모든 간선에 대하여 1번 과정을 반복한다.
예제 : 그래프의 연결성 확인하기
서로소 집합의 고전적인 사례로 그래프의 연결성을 확인하는 문제가 있다.
N개의 도시가 도로망으로 연결되어 있는데, 각 도로는 정확히 두 개의 도시를 연결한다. 그런데 이들이 폭설로 인해 모두 마비되었고 시간이 지남에 따라 하나 하나 도로들이 복구되는데, 도로가 복구될 때마다 임의의 두 도시 간에 서로 왕래가 가능한지를 알고 싶다고 한다.
- 각 도시를 원소로 하고, 서로 왕래 가능한 도시들을 하나의 집합으로 표시한다.
- 두 도시 a,b를 연결하는 도로가 복구 되었으면 원래 a에 연결되어 있던 도시들과 b에 연결되어 있던 도시들 도한 이 도로를 통해 왕래가 가능하므로 두 집합을 합친다.
예제: 가장 큰 집합 추적하기
각 집합에 속한 원소의 수를 추적할 수도 있다. 각 트리의 개수를 담은 배열을 추가한 뒤 두 집합이 합쳐질 때마다 이 값을 갱신해주면 된다.
아래와 같은 작업들이 가능해진다.
- 집합들이 합쳐지는 과정에서 가장 큰 집합의 크기가 어떻게 변하는지 추적할 수 있다.
- 과반수가 출현하는 시점을 찾을 수 있다.
신장 트리 (스패닝 트리)
어떤 무향 그래프의 신장 트리는
- 원래 그래프의 정점 전부와 간선의 부분 점들을 트리 형태로 전부 연결해야한다.
- 트리 형태 : 선택된 간선들이 사이클을 이루지 않는다
가중치 그래프의 신장 트리 중 가중치의 합이 가장 작은 트리를 찾는 문제를 최소 신장트리 문제라고 한다.
최소 신장 알고리즘
가장 최소한의 비용으로 사이클 없이 모든 노드를 연결해야할 때
=> 최소 비용으로 신장 트리를 만들어야할 때
크루스칼 알고리즘
- 그래프의 모든 간선을 가중치의 오름차순으로 정렬한 뒤 신장 트리에 하나씩 추가해 간다.
- 간선을 트리에 추가했을 때 이미 추가한 간선들과 합쳐 사이클을 이루는지 여부를 판단한다.
- 두 정점이 주어졌을 때 이들이 같은 컴포넌트에 속하는지 확인하고, 아닐 경우 두 컴포넌트를 합친다.
- 서로소 집합 자료구조의 find, union 연산을 사용한다.
- 서로소 집합을 쓰지 않고 DFS 할 경우 DFS의 시간 복잡도인 O(|V|+|E|) 에 E를 곱해 O(E^2)이 된다
- 두 정점이 주어졌을 때 이들이 같은 컴포넌트에 속하는지 확인하고, 아닐 경우 두 컴포넌트를 합친다.
- 최소 신장 트리에 포함되어 있는 간선의 비용만 모두 더 하면 그 값이 최종 비용이 된다.
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
parnet= [0] * (v+1)
#모든 간선을 담을 리스트와 최종 비용을 담을 변수
edges = []
result = []
#부모 테이블 상에서, 부모를 자기 자신으로 초기화
for i in range(1,v+1):
parent[i] = i
for i in range(e):
a,b,c = map(int,input().split())
edges.append((cost,a,b)) # 비용 순으로 정렬하기 위해서
edges.sort()
for edge in edges:
const, a, b = edge
#사이클이 발생하지 않는 경우에만 집합에 포함
if find_parent(parent,a) != find_parent(parent,b):
union_parent(parent,a,b)
result += cost
print(resut)
시간 복잡도
간선의 개수가 E개일 때, O(ElogE)이다. 가장 오래 시간이 걸리는 부분이 정렬이고 정렬은 E개의 데이터를 정렬할 때 O(ElogE)이기 때문이다.
프림 알고리즘
하나의 시작점으로 구성된 트리에 간선을 하나씩 추가하며 스패닝 트리가 될 때까지 키워 간다.
이미 만들어진 트리에 인접한 간선만을 고려한다는 점을 제외하면 프림 알고리즘은 크루스칼 알고리즘과 완전히 똑같다.
- 각 정점이 트리에 포함되었는지 여부를 나타내는 배열을 둔다
- 각 차례마다 트리에 속하지 않은 정점에 대해 트리와 이 정점을 연결하는 가장 짧은 간선 정보를 저장한다.
- 각 정점을 순회하면서 다음에 추가할 정점을 찾는다.
import heapq
def prim(src,edges):
mst = []
graph = [[] for _ in range(v)]
for weight,n1,n2 in edges:
graph[n1].append((weight,n1,n2))
graph[n2].append((weight,n2,n1))
connected_nodes = set(src)
candidate_edge_list = graph[src]
heaplify(candidate_edge_list)
while candidate_edge_list :
weight,n1,n2 = heapq.heappop(candidate_edge_list)
if n2 not in connected_nodes :
connected_nodes.append(n2)
mst.append((weight,n1,n2))
for edge in graph[n2]:
if edge[2] not in connected_nodes:
heapq.heappush(candidate_edge_list,edge)
return mst
위상 정렬 알고리즘
순서가 정해져 있는 일련의 작업을 차례대로 수행해야할 때 사용할 수 있는 알고리즘
방향 그래프의 모든 노드를 방향성에 거스르지 않도록 순서대로 나열하는 것
선수과목을 고려한 학습 순서 설정을 생각해보면된다.
- 진입 차수가 0인 노드를 큐에 넣는다.
- 진입차수 : 특정한 노드로 들어오는 간선의 개수
- 큐가 빌 때까지 다음의 과정을 반복한다.
- 큐에서 원소를 꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거 한다.
- 새롭게 진입 차수가 0이 된 노드를 큐에 넣는다.
- 과정을 수행하는 동안 큐에서 빠져나간 노드를 순서대로 출력하면 위상 정렬을 수행한 결과가 된다.
위상 정렬의 특이 케이스
- 사이클이 발생하는 경우 일반적인 위상 정렬의 경우 정확히 N개의 노드가 큐에서 출력이 된다. 노드가 N번 나오기 전에 큐가 비면 사이클이 발생한 것으로 이해할 수 있다.
- 위상 정렬 결과가 1개가 아니라 여러 가지인 경우 특정 시점에 2개 이상의 노드가 큐에 한꺼번에 들어갈 때는 가능한 정렬 결과가 여러가지라는 의미가 된다.
from collection import deque
#진입 차수 초기화
indegree = [0]*(v+1)
graph[[] for _ in range(v+1)]
# 간선 정보 입력
for _ in range(e):
graph[a].append(b) #A에서 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)
# 인접한 노드들의 진입 차수 -1
for i in graph[now]:
indegree[i] -= 1
#새롭게 진입 차수가 0이 되는 노드를 큐에 삽입
if indegree[i] == 0:
q.append(i)
# 위상 정렬을 수행한 결과 출력
for i in result:
print(i, end= ' ')
topology_sort()
위상 정렬의 시간복잡도
차례대로 모든 노드를 확인하면서, 해당 노드에서 출발하는 간선을 차례대로 제거해야하므로 결과적으로 노드와 간선을 모두 확인해야한다는 측면에서 O(V+E)의 시간이 소요된다
'CS > 알고리즘' 카테고리의 다른 글
동적계획법 (Dynamic Programming) (0) | 2022.03.25 |
---|