Ch10. 그래프 이론
Ch10. 그래프 이론
1.다양한 그래프 알고리즘(비중은 낮음)
이미 배운 내용을 훑어보자
- 그래프 알고리즘의 한 유형: ‘DFS/BFS’와 ‘최단 경로’
- 크루스칼 알고리즘 -> 그리디 알고리즘
- 위상 정렬 알고리즘 -> 큐 자료 or 스택 자료구조 활용해 구현
- 그래프: 노드와 노드 사이에 연결된 간선의 정보를 가진 자료구조
- 트리: 그래프 자료구조 중 하나
⭐알고리즘 문제를 접했을 때 ‘서로 다른 개체(혹은 객체)가 연결되어 있다’는 이야기 들으면 그래프!
그래프 VS 트리
| 그래프 | 트리 | |
|---|---|---|
| 방향성 | 방향 그래프 혹은 무방향 그래프 | 방향 그래프 |
| 순환성 | 순환 및 비순환 | 비순환 |
| 루트 노드 존재 여부 | 루트 노드가 없음 | 루트 노드가 존재 |
| 노드간 관계성 | 부모와 자식 관계 없음 | 부모와 자식 관계 |
| 모델의 종류 | 네트워크 모델 | 계층 모델 |
그래프의 구현 방식
- 인접 행렬: 2차원 배열을 사용하는 방식
- 간선 정보 저장: O(V^2) 만큼의 메모리 공간 필요
- 특정 노드 A에서 다른 특정 노드 B로 이어진 간선의 비용을 O(1)의 시간으로 즉시 알 수 있다.
- 예시) 플루이드 워셜 알고리즘
- 인접 리스트: 리스트를 사용하는 방식
- 간선 정보 저장: O(E) 만큼의 메모리 공간 필요
- 특정 노드 A에서 다른 특정 노드 B로 이어진 간선의 비용을 O(V)의 시간으로 즉시 알 수 있다.
⭐메모리와 시간을 염두에 두고 알고리즘 선택!!
ex) 노드의 개수가 적은 경우에는 플로이드 워셜 알고리즘 이용 vs 노드와 간선의 개수가 모두 많으면 우선순위 큐를 이용하는 다익스트라 알고리즘 이용
서로소 집합
- 서로소 집합: 공통 원소가 없는 두 집합을 의미
서로소 집합 자료구조
- 서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조(그래프 알고리즘에서 매우 중요)
- 연산:
- union: 2개의 원소가 포함된 집합을 하나의 집합으로 합치는 연산
- find: 특정한 원소가 속한 집합이 어떤 집합인지 알려주는 연산
구현
- 트리 자료구조를 이용하여 집합 표현.
- 번호가 큰 노드가 번호가 작은 노드를 가리키도록
- 서로소 집합 정보(합집합 연산)가 주어졌을 때 트리 자료구조를 이용해서 집합을 표현하는 서로소 집합 계산 알고리즘은 다음과 같다.
- union(합집합) 연산을 확인하여, 서로 연결된 두 노드 A, B를 확인한다.
I. A와 B의 루트노드 A’, B’를 각각 찾는다.
II. A’를 B’의 부모 노드로 설정한다(B’가 A’를 가리키도록 한다.) ->작은 원소가 부모 노드가 되도록 설정. - 모든 union 연산을 처리할 때까지 1번 과정을 반복한다.
- union(합집합) 연산을 확인하여, 서로 연결된 두 노드 A, B를 확인한다.
- union 연산들은 그래프 형태로 표현 가능
- 노드: 각 원소
- 간선: ‘같은 집합에 속한다’는 정보를 담은 union 연산
예시
union 연산을 하나씩 확인하면서 서로 다른 두 원소에 대해 합집합을 수행해야 할 때는, 각각 루트 노드를 찾아서 더 큰 로투 노드가 더 작은 루트 노드를 가리키도록 하면 된다.
- 실제로 루트를 확인하고자 할 때는 재귀적으로 부모를 거슬러 올라가서 최종적인 루트 노드를 찾아야한다.
기본적인 서로소 집합 알고리즘 소스코드
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
32
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
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)
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='')
- find 함수의 최악 시간 복잡도: O(V)
- 전체 최악의 시간 복잡도: O(VM)
=> 경로 압축 기법을 이용하면 개선 가능
=> 경로 압축?: find 함수를 재귀적으로 호출한 뒤에 부모 테이블값을 갱신하는 기법! 기존의 find 함수를 다음과 같이 변경!
1
2
3
4
def find_parent(parent, x):
if parent[x] != x:
parent[x] = find_parent(parent, parent[x])
return parent[x]
개선된 서로소 집합 알고리즘 소스코드
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
32
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)
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[1], end=' ')
서로소 집합 알고리즘의 시간 복잡도
- 경로 압축 바법 이용: O(V + M(1 + log2-M/V V))
- 노드의 개수: V
- 최대 V-1 개의 union 연산과 M개의 find 연산이 가능할때
서로소 집합을 이용한 사이클 판별
- 서로소 집합은 무방향 그래프 내에서의 사이클을 판별할 때 이용 가능.
- 참고: 방향 그래프에서의 사이클 여부는 DFS를 이용하여 판별 가능
- 간선을 하나씩 확인하면서 두 노드가 포함되어 있는 집합을 합치는 과정을 반복하는 것만으로도 사이클 판별 가능
알고리즘
- 각 간선을 확인하며 두 노드의 루트 노드를 확인한다.
I. 루트 노드가 서로 다르다면 두 노드에 대하여 union 연산 수행
II. 루트 노드가 서로 같아면 사이클이 발생한 것이다. - 그래프에 포함되어 있는 모든 간선에 대하여 1번 과정 반복
=> 사이클 판별 알고리즘은 그래프에 포함되어 있는 간선의 개수가 E개일 때 모든 간선을 하나씩 확인하며, 매 간선에 대하여 union 및 find 함수를 호출하는 방식으로 동작한다. 이 알고리즘은 간선에 방향성이 없는 무향 그래프에서만 적용 가능.
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
32
33
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, a)
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("사이클이 발생하지 않았습니다.")
신장 트리
- 신장 트리: 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프(모든 노드가 포함되어 서로 연결되면서 사이클 존재X)
크루스칼 알고리즘
- 크루스칼 알고리즘: 가장 적은 비용으로 모든 노드 연결(그리디 알고리즘)
ex) 모든 도시를 연결할 때, 최소한의 비용으로 연결하려면? - 최소 신장 트리: 트리의 일종이므로 간선의 개수가 “노드의 개수 - 1”과 같다.
구체적인 알고리즘
- 간선 데이터를 비용에 따라 오름차순으로 정렬한다.
- 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인한다.
I. 사이클이 발생하지 않는 경우 최소 신장 트리에 포함(union)시킨다.
II. 사이클이 발생하는 경우 최소 신장 트리에 포함X - 모든 간선에 대하여 2번의 과정 반복
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
32
33
34
35
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 _ 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) : 정렬 알고리즘의 시간 복잡도가 젤 큼!
위상 정렬
- 위상 정렬: 정렬 알고리즘의 일종. 순서가 정해져 있는 일련의 작업을 차례대로 수행해야 할 때 이용.
- 방향 그래프의 모든 노드를 ‘방향성에 거스르지 않도록 순서대로 나열하는 것’
ex) 선수과목을 고려한 학습 순서 설정.
- 방향 그래프의 모든 노드를 ‘방향성에 거스르지 않도록 순서대로 나열하는 것’
- 진입차수: 특정한 노드로 ‘들어오는’ 간선의 개수를 의미.
위상 정렬의 알고리즘
- 진입 차수가 0인 노드를 큐에 넣는다.
- 큐가 빌 때까지 다음의 과정을 반복한다.
I. 큐에서 원소를 꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거한다. II. 새롭게 진입차수가 0이 된 노드를 큐에 넣는다.
=> 이때, 모든 원소에 방문하기 전 큐가 빈다면 사이클이 존재한다고 판단(큐에서 원소가 V번 추출되기 전에..)
위상 정렬 소스코드
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
32
33
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 topoloty_sort():
result = []
q = deque()
for i in range(1, v+1):
if indegree[i] == 0:
q.appned(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()
위상 정렬의 시간 복잡도
- O(V + E)
This post is licensed under CC BY 4.0 by the author.






