Cherry & Cherish
그래프 알고리즘 (서로소 집합, 크루스칼, 위상 정렬) 본문
그래프 알고리즘은 코딩 테스트 단골 출제 문제는 아니다. 하지만 꼭 알아야 하는 알고리즘 중 하나라고 할 수 있다.
그래프 알고리즘에 대한 팁은 “서로 다른 객체가 연결되어 있다(여러 개의 도시가 연결되어 있다)”라는 표현이 들어가면 대체로 그래프 알고리즘을 활용해야 하는 문제일 가능성이 높다.
또, 트리 자료구조는 다양한 알고리즘에서 사용되므로 꼭 암기해야 한다. 트리 자료구조는 부모에서 자식으로 내려오는 계층적인 모델에 속한다. 최소힙은 항상 부모 노드가 자식 노드보다 크기가 작은 자료구조로서 트리 자료구조에 속한다.
그래프의 구현 방법은 2가지 방식이 존재한다.
- 인접 행렬 : 2차원 배열을 사용하는 방식
- 인접 리스트 : 리스트를 사용하는 방식
2가지 모두 사용된다.
1. 서로소 집합
1-1. 서로소 집합 자료구조 개념
💡 서로소 집합 자료구조 - “서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조”
수학에서 서로소 집합이란 공통원소가 없는 두 집합을 의미한다.
그래프 알고리즘이론에서의 서로소 집합 자료구조란 “서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조”라고 볼 수 있다. 서로소 집합 자료구조는 union과 find 이 2개의 연산으로 조작할 수 있다.
union(합집합) 연산은 2개의 원소가 포함된 집합을 하나의 집합으로 합치는 연산이다.
find 연산은 특정한 원소가 속한 집합이 어떤 집합인지 알려주는 연산이다. 스택과 큐가 각각 push 와 pop 연산으로 이루어졌던 것 처럼, 서로소 집합 자료구조는 합집합과 찾기 연산으로 구성된다.
서로소 집합 자료구조는 union-find 자료구조라고 불리기도 한다.
1-2. 서로소 집합 자료구조 원리
- union(합집합) 연산을 확인하여, 서로 연결된 두 노드 A, B를 확인한다.
- A와 B의 루트 노드 A’, B’를 각각 찾는다.
- A’를 B’의 부모 노드로 설정한다. (B’가 A’를 가리키도록 한다)
- 모든 union(합집합) 연산을 처리할 때까지 1번 과정을 반복한다.
1-3. 기본적인 서로소 집합 알고리즘 구현
# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
#루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
if parent[x] != x:
return find_parent(parent, parent[x])
return 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
#노드의 개수와 간선(union 연산)의 개수 입력받기
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)
#각 원소가 속한 집합 출력
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=' ')-
- 시간복잡도 : O(VM) ⇒ V는 노드의 개수, M은 연산의 개수
1-4. 개선된 서로소 집합 알고리즘 구현
위의 서로소 집합 알고리즘은 비효율적인 시간복잡도를 가지고 있다. 이 과정을 find 함수를 통해 아주 간단한 과정으로 최적화할 수 있다.
1-4-1. 경로압축 기법
경로 압축 기법은 find함수로 최적화하는 알고리즘이다. find 함수를 재귀적으로 호출한 뒤에 부모 테이블값을 갱신한다. 기존의 find 함수를 아래와 같이 변경한다.
def find_parent(parent, x):
if parent[x] != x:
parent[x] = find_parent(parent, parent[x]_
return parent[x]
1-4-2 경로압축 기법을 적용한 서로소 집합 알고리즘
#특정 원소가 속한 집합을 찾기
def find_parent(parnet, 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
#노드의 개수와 간선(union 연산)의 개수 입력받기
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)
#각 원소가 속한 집합 출력
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=' ')
- 시간 복잡도 : O(V + M(1 + log V)) (로그 2-M/V를 밑으로 한 V)
2. 신장트리
💡 신장트리 - “하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프”
신장트리는 그래프 알고리즘으로 자주 출제되는 유형이다.
2-1. 크루스칼 알고리즘 개념
신장 트리 중에서 최소 비용으로 만들 수 있는 신장 트리를 찾는 알고리즘을 ‘최소 신장 트리 알고리즘’이라고 한다. 대표적인 신장 트리 알고리즘에는 크루스칼 알고리즘이 있다.
크루스칼 알고리즘을 사용하면 가장 적은 비용으로 모든 노드를 연결할 수 있다.
크루스칼 알고리즘은 그리디 알고리즘으로 분류된다.
2-2. 크루스칼 알고리즘 원리
- 간선 데이터를 비용에 따라 오름차순으로 정렬한다.
- 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인한다.
- 사이클이 발생하지 않는 경우 최소 신장 트리에 포함시킨다.
- 사이클이 발생하는 경우 최소 신장 트리에 포함시키지 않는다.
- 모든 간선에 대하여 2번 과정을 반복한다.
💡 핵심 원리 : 정렬 후 가장 거리가 짧은 간선부터 차례대로 집합에 추가하면 된다 (사이클 발생시키는 간선 제외)
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_parnet(parent, b)
if a < b:
parent[b] = a
else :
parent[a] = b
#노드의 개수가 간선(union 연산)의 개수 입력받기
v, e = map(int, input().split())
parent = [0] * (v + 1) #부모 테이블 초기화
#모든 간선을 담을 리스트와 최종 비용을 담을 변수
edges = []
result = 0
#부모 테이블 상에서, 부모를 자기 자신으로 초기화
for i in range(1, v + 1):
parent[i] = i
#모든 간선에 대한 정보를 입력받기
for _ 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)
- 시간복잡도 : O(ElogE) ⇒ E는 간선의 개수
3. 위상 정렬
💡 위상 정렬 : “방향 그래프의 모든 노드를 ‘방향성에 거스르지 않도록 순서대로 나열하는 것’”
위상 정렬은 정렬 알고리즘의 일종이다. 순서가 정해져있는 일련의 작업을 차례대로 수행해야 할 때 사용할 수 있는 알고리즘이다.
3-1. 진입차수
진입차수란 특정한 노드로 ‘들어오는’ 간선의 개수를 의미한다. 그래프의 간선 중 방향이 나에게 2개 지목되는 것이 있다면, 진입차수는 2라고 할 수 있다.
이 진입 차수를 고려하여 주어진 방향 그래프에서 정렬을 수행하는 것이 위상정렬의 개념이다.
3-2. 위상정렬 동작 원리
- 진입차수가 0인 노드를 큐에 넣는다.
- 큐가 빌 때까지 다음의 과정을 반복한다.
- 큐에서 원소를꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거한다.
- 새롭게 진입차수가 0이 된 노드를 큐에 넣는다.
만약 큐가 비었는데 모든 원소를 방문하지 않았다면 사이클이 존재한다고 할 수 있다.
사이클이 존재하는 경우 사이클에 포함되어 있는 원소 중에서 어떤 원소도 큐에 들어가지 못하기 때문이다.
다만, 일반적으로 위상 정렬 문제에서는 사이클이 발생하지 않는다고 명시하는 경우가 더 많다.
3-3. 위상정렬 코드 구현
from collections import deque
#노드의 개수와 간선의 개수 입력받기
v, e = map(int, input().split())
#모든 노드에 대한 진입차수는 0으로 초기화
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) #정점 A에서 B로 이동가능
#진입차수를 1 증가
indgree += 1
#위상 정렬 함수
def topology_sort():
result = [] #알고리즘 수행 결과를 담을 리스트
q = deque() #큐 기능을 위한 deque 라이브러리 사용
#처음 시작할 때는 진입 차수가 0인 노드를 큐에 삽입
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이 되는 노드를 큐에 삽입
indegree[i] == 0:
q.qppend(i)
#위상 정렬을 수행한 겨로가 출력
for i in result :
print(i, end=' ')
topology_sort()
- 시간복잡도 : O(V + E)
4. 연관 문제
이제 그래프 알고리즘에 대한 개념 학습은 끝났다. 바로 실제 문제를 풀어보자!
[백준]
- 바이러스(2606) - https://www.acmicpc.net/problem/2606
- 유기농 배추(1012) - https://www.acmicpc.net/problem/1012
- 숨바꼭질(1917) - https://www.acmicpc.net/problem/1697
[프로그래머스]
[SWEA]
'Algorithm > Learning' 카테고리의 다른 글
[Algo] dfs 연습 (0) | 2023.02.14 |
---|---|
다이나믹 프로그래밍(DP, 동적계획법) 알고리즘 (0) | 2023.02.03 |
최단경로 알고리즘(다익스트라, 플로이드 워셜) (1) | 2023.02.02 |
이진 탐색 알고리즘 (0) | 2023.02.01 |
정렬 알고리즘 (선택정렬, 삽입정렬, 퀵정렬, 계수정렬, 버블정렬, 합병정렬, 힙정렬, 정렬 라이브러리) (1) | 2023.01.31 |