공통원소가 존재하지 않는 집합, 그러한 관계를 의미한다.
이 서로소 집합에 대해 Union(개별적인 두 노드(원소)에 대해 동일한 부모노드를 설정하여 하나의 집합(합집합)으로 합치는 과정)연산을 거치면 동일한 하나의 집합으로 만들 수 있다.
이 후 Find 연산으로, 각 원소들의 부모노드를 확인하여 부모노드가 같을 경우 같은 집합으로 취급하여 합집합인지 서로소집합인지 판별한다.
Union(1,4), Union(2,3), Union(2,4), Union(5,6)의 연산으로 합집합을 구성한다고 해보자.
최초 과정은 노드 개수만큼 배열 및 부모테이블을 구성하는 것이며, 이때 처음 상태의 부모테이블은 각각 자기 자신이 된다(자기자신 = 부모노드).
노드1과 노드4를 합쳐 합집합으로 만드는 과정을 진행한다.
부모노드는 두 노드 중 숫자가 작은 노드로 설정되며, Union 연산이후엔 두 노드는 더이상 서로소 집합이 아니다.
마찬가지로 노드2와 노드3에 대한 합집합 연산을 수행한다.
노드2와 노드4에 대해 합집합 연산을 수행해보자.
결과적으로 각 노드(원소)들을 하나의 집합으로 구성할 수 있고, find 연산을 통해 두 노드의 연결관계를 정의할 수 있다.
합집합 관계에 있는 노드들에 대해서, 루트 노드가 단계별(계단과 같이)로 구성되어 있는 경우엔 바로 접근해서 알 수는 없고 재귀적으로(순차적으로) 루트 노드를 파악하여 최종적인 부모노드를 파악할 수 있다.
즉 위와 같이 노드3의 부모노드를 확인하기 위해선, 노드3 > 노드2 > 노드1의 순으로 단계적으로(재귀적으로) 부모노드를 파악해야 알 수 있다(거슬러 올라가야 한다).
노드 개수 v와 간선 개수 e를 입력받은 후, 각 노드의 루트노드를 구성하고(같은 집합으로 합치고) 이를 탐색하는(find) 알고리즘을 구현해 본다.
#특정 노드에 대한 루트노드 찾기
def findParent(parent, x):
if parent[x] != x: #자기 자신이 아니라면, 해당 루트 노드의 루트노드를 찾는다.
return findParent(parent, parent[x])
#parent[x], 즉 해당 루트노드의 루트노드를 찾는다.
#parent는 부모테이블
return x
#union 연산
def unionParent(parent, a, b):
a = findParent(parent, a)
b = findParent(parent, b)
if a < b: #a의 루트노드가 최종적인 루트노드가 되도록 노드 b를 설정
parent[b] = a
else :
parent[a] = b
#입력
#노드개수와 간선개수
import sys
v, e = map(int, sys.stdin.readline().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, sys.stdin.readline().split())
unionParent(a, b) #a와 b를 합치는 과정
#각 원소의 소속집합(루트노드)
for i in range(1, v+1):
print(findParent(parent, i), end=' ')
print()
#부모테이블 내용
##부모테이블과 각자 속한 루트노드는 다른 내용일 수 있다(실제 루트노드는 거쳐거쳐 확인하므로)
for i in range(1, v+1):
print(parent[i], end=' ')
위 방법과 같이 재귀적으로 union find 호출 시, 거쳐거쳐서 루트노드를 확인한다는 점에서 단계가 늘어날 수록 그만큼 find연산이 늘어나고 비효율적일 수 있다.
위와 같이 단계구성이 많아진다면, 최악의 경우 노드개수만큼 시간복잡도가 늘어난다(O(V)).
따라서 개선된 형태인 경로압축(path compression)을 활용하는데, 이때 찾기함수를 호출하였을 때 부모값을 바로 갱신한다.
def findParent(parent, x):
if parent[x] != x: #자기 자신이 아니라면, 해당 루트 노드의 루트노드를 찾는다.
return findParent(parent, parent[x])
#parent[x], 즉 해당 루트노드의 루트노드를 찾는다.
#parent는 부모테이블
return x
즉 위의 재귀적으로 탐색하는 부분을 아래와 같이 그때그때 찾을때마다 부모노드를 갱신하는 형태로 바꾼다.
마치 Topdown 하듯이 각 단계마다 부모노드 값을 저장한다.
def findParent(parent, x):
if parent[x] != x: #자기 자신이 아니라면, 해당 루트 노드의 루트노드를 찾는다.
parent[x] = findParent(parent, parent[x]) #재귀호출이 아니라, TOpDown 방식으로 값을 저장한다.
return parent[x]
graph 내부에서 cycle, 즉 두 노드에 대해 양방향 탐색이 가능한 경로가 존재하는지 판별하는 도구로 union find 연산을 사용할 수 있다.
다만 무향 graph에서 union find연산을 활용하고, 방향 graph에서는 DFS를 이용하여 cycle을 확인할 수 있다.
위와 같은 graph가 있을때 union find 연산을 이용해서 cycle을 어떻게 판별할 수 있을까?
일단 기본적으로 cycle은 같은 부모노드를 바라본다라는 시점에서 출발하는데, cycle이 발생하지 않는다면 cycle이 발생하는 알고리즘을 구성할 수 있다.
각각의 간선을 확인해가면서 각 노드의 루트노드를 확인한다.
간선 1,2의 노드들의 루트노드가 최초 1,2인 상태에서 → 더 작은 루트노드인 1을 기준으로 union연산을 진행한다.
위 과정을 반복 수행한다.
최종적으로 동일한 루트노드를 구성하였으며, cycle이 발생하는 graph를 구성하게 되었다.
이를 알고리즘으로 구성하면 아래와 같다.
#기존 union find를 개선한 형태의 알고리즘
#특정 노드에 대한 루트노드 찾기
def findParent(parent, x):
if parent[x] != x: #자기 자신이 아니라면, 해당 루트 노드의 루트노드를 찾는다.
parent[x] = findParent(parent, parent[x]) #재귀호출이 아니라, TOpDown 방식으로 값을 저장한다.
return parent[x]
#union 연산
def unionParent(parent, a, b):
a = findParent(parent, a)
b = findParent(parent, b)
if a < b: #a의 루트노드가 최종적인 루트노드가 되도록 노드 b를 설정
parent[b] = a
else :
parent[a] = b
#입력
#노드개수와 간선개수
import sys
v, e = map(int, sys.stdin.readline().split())
#부모테이블
parent = [0] * (v+1)
for i in range(1, v+1):
parent[i] = i
#cycle판별 알고리즘 : cycle이 아닐 경우 union 과정 수행
cycle = False
for i in range(e):
a, b = map(int, sys.stdin.readline().split())
if findParent(parent, a) == findParent(parent, b):
cycle = True #루트노드가 같아서 cycle이 발생하였다면 즉시 반복문 종료
break
else:
unionParent(parent, a, b)
if cycle:
print("사이클이 존재하는 무향 그래프")
else:
print("사이클이 존재하지 않은 무향 그래프")
이코테 2021 - union find
https://www.youtube.com/watch?v=aOhhNFTIeFI&list=PLRx0vPvlEmdAghTr5mXQxGpHjWqSz0dgC&index=8