서로소 집합 자료구조를 표시할 때는 기본적으로 트리 자료구조를 이용한다. 트리 자료구조는 계층을 갖고 있다. 즉, 노드들 간의 부모-자식 관계가 있음을 가정한다.
1. 첫 번째 초기 단계
초기 단계에서 해야 할 가장 중요한 일은 노드의 개수만큼의 부모 테이블을 초기화 하는 것이다. 예를 들어, 노드의 개수가 N개 일 때, N+1 길이의 1차원 리스트를 초기화한다. 이 때, 리스트의 원소값은 해당 인덱스값을 넣어준다. 즉, 처음에는 모든 원소가 자기 자신을 부모로 가지도록 설정한다. 초기단계로 다음과 같은 데이터가 주어졌다.
2. 두 번째 단계 - union 첫 번째 연산 수행
union 첫 번째 연산으로 (1, 4)가 들어왔다. 앞에서 공부한 Union 연산을 수행하며 다음과 같이 부모 테이블의 값을 업데이트 해준다.
위 처럼 Union 연산 정보에 의해 4번 노드의 부모 노드는 1번 노드라는 것을 알게 되었다. 따라서 위와 같이 부모테이블의 4번 인덱스 값을 4 -> 1로 업데이트 한다.
3. 세 번째 단계 - union 두 번째 연산 수행
이번에 주어진 Union 연산 정보에 의해 3번 노드의 부모 노드는 2번 노드이다. 따라서 위와 같이 부모 테이블의 3번 인덱스 값이 2번으로 업데이트 된다.
4. 네 번째 단계 - union 세 번째 연산 수행
여기서 헷갈리지 말아야 한다. 주어진 Union 연산 정보는 (2, 4) 이다. 즉 부모 테이블 상에서 4번 노드의 부모 노드(부모 테이블 값)는 1이다. 반면, 2번 노드의 부모 노드(부모 테이블 값)는 2이다. 따라서 4번 노드의 부모 노드 값이 1로 더 작으므로 2번 노드의 부모노드 값인 2는 1로 업데이트 해야 한다. 그래서 자료구조 그림 상으로는 2 -> 1번 노드로 가는 간선이 생겨난다.
5. 다섯 번째 단계 - union 네 번째 연산 수행
이번에 주어진 연산 정보에 의해 6번 노드의 부모 테이블 값은 5로 업데이트 된다. 그리고 모든 Union 연산을 수행했으니 알고리즘이 종료되고 다음과 같이 최종 부모 테이블이 완성된다.
📌 위 부모 테이블을 해석하게 되면 6번 노드의 부모노드는 5번이고 5번의 부모노드는 5번으로 결국 하나의 서로소 집합으로 구성된다. 반면에 1,2,4번의 부모노드는 1이고 3번 노드의 부모노드는 2번 노드이지만 2번 노드의 부모노드는 재귀적으로 또 1번 노드이므로 결국 최종 루트 노드가 1번 노드로 또 하나의 서로소 집합으로 구성된다.
위 설명과 같이, 서로소 집합 자료구조에서는 연결성을 통해 손쉽게 집합의 형태를 확인할 수 있다.
기본적인 형태의 서로소 집합 자료구조에서는 루트 노드에 즉시 접근할 수 없다.
다음 예시에서 노드 3의 루트를 찾기 위해서는 노드 2를 거쳐 노드 1에 접근해야 한다.
import sys
v, e = map(int, input().split())
parent = [0] * (v+1)
for i in range(1, v+1) :
parent[i] = i
# find 연산
def find_parent(parent, x) :
# 부모노드와 자식노드가 같지 않다면, 부모노드가 따로 있다는 의미
if parent[x] != x :
return find_parent(parent, parent[x]) # 그 부모노드를 자식으로 하는 또 다른 부모노드를 재귀적으로 탐색
return x # 부모노드 == 자식 노드 -> 해당 노드 리턴
def union_parent(parent, a, b) :
# a, b 각 부모노드 탐색 - Find 연산
a = find_parent(parent, a)
b = find_parent(parent, b)
if a < b :
parent[b] = a
else :
parent[a] = b
for _ in range(e) :
a, b = map(int, input().split())
union_parent(parent, a, b)
# 각 원소의 루트 노드 출력
print('# 각 원소의 루트 노드들 : ', end='')
for i in range(1, v+1) :
root = find_parent(parent, i)
print(root, end=' ')
print()
# 부모 테이블 출력 (직계 부모노드를 의미)
print('# 각 원소의 직계 부모 노드들 : ', end='')
for i in range(1, v+1) :
print(parent[i], end=' ')
import sys
v, e = map(int, input().split())
parent = [0] * (v+1)
for i in range(1, v+1):
parent[i] = i
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
for _ in range(e):
a, b = map(int, input().split())
union_parent(parent, a, b)
print('# 각 원소의 루트노드 출력:', end='')
for i in range(1, v+1):
root = find_parent(parent, i)
print(root, end=' ')
print()
print('# 위 find 연산 후 업데이트된 부모 테이블 출력:', end='')
for i in range(1, v+1):
print(parent[i], end=' ')
1. 초기 단계
모든 노드에 대하여 자기 자신을 부모로 설정하는 형태로 부모 테이블을 초기화 한다.
2. Step 1
간선 (1,2)을 확인한다. 노드 1과 노드 2의 루트 노드는 각각 1과 2 이다. 따라서 더 큰 번호에 해당하는 노드 2의 부모노드를 1로 변경한다.
3. Step 2
간선 (1,3)을 확인한다. 노드 1과 노드 3의 루트 노드는 각각 1과 3 이다. 따라서 더 큰 번호에 해당하는 노드 3의 부모노드를 1로 변경한다.
4. Step 3
간선 (2,3)을 확인한다. 이미 노드 2와 노드 3의 루트 노드는 모두 1이다. 다시 말해 사이클이 발생한다는 것을 알 수 있다.
import sys
v, e = map(int, input().split())
parent = [0] * (v+1)
for i in range(1, v+1) :
parent[i] = i
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
cycle = False
for _ 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('사이클이 존재하지 않습니다.')
[ 참고 자료 ]
1. https://www.youtube.com/watch?v=aOhhNFTIeFI&list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&index=8
2. https://techblog-history-younghunjo1.tistory.com/257?category=1014800