728x90

그래프(graph)자료구조의 개념부터 해서 이와 관련된 disjoint-set 등의 다양한 알고리즘까지 쭉 정리해보려 한다.

하나의 게시글에 정리하려고 하니 양이 너무 많을 듯해서, 시리즈로 쪼개서 정리해보려 한다.

1. 그래프의 개념과 특징
2. Graph_disjoint set(현재글)
3. Union Find 최적화


Disjoint Set의 개념

서로소 집합알고리즘으로서 서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조이다. 

Union-find로도 알려진 해당 자료구조는, 그래프 자료구조에서 주어진 노드간의 연결성을 효율적으로 파악할 수 있게 해준다. 즉 네트워크에서 두 요소간의 연결성을 보여주는 용도로 사용되는 것이다. 

예를 들어 사회적 관계에서 두 사람이 서로 같은 조상을 가지고 있는지, 혈연관계인지에 대한 관계성을 측정할 수 있게 된다. 

 

각 노드들의 부모와 루트 노드를 찾아서(find), 합쳐주고(union) 연결관계를 찾는 것이다.

 

서로소 집합에서는 각 원소들이 하나의 집합에만 속하게 된다. 

이러한 특징을 통한 자료구조를 사용하면 역으로 서로 다른 두개 이상의 원소가 같은 집합에 속해있는 지 아닌 지를 판별할 수 있다. 

그래서 특히 그래프 형태의 자료구조에서 두개의 요소가 연결되어있는지 아닌지를 판단할 수 있는 방법으로 활용될 수 있는 것이다.

 

우선 어떻게 disjoint set data structure을 활용할 수 있는지에 앞서서 Disjoin set자료구조가 무엇이고 관련된 자료구조들은 어떤게 있을까?

 

해당 자료구조를 활용하기 위해서는 disjoint set union 자료구조를 만들수 있어야 하는데, disjoint set자료구조의 정의에 의하면 해당 특성을 가진 두 집합 사이에는 교집합이 없으므로, 합연산을 구하기 위해서 그냥 두 집합을 더하기만 하면 된다. 

이렇게 두 집합을 합하여서 판단할 수 있다보니 Union-Find 자료구조라고도 불린다.

 

* Union(합치기) 연산 : 두 원소 a,b가 주어질 때 이들이 속한 두 집합을 하나로 합친다.

* Find(찾기)연산 : 어떤 원소 a가 주어질 때 이 원소가 속한 집합을 반환한다. 

 

주요 용어

  • parent node : 주어진 노드가 연결한 노드(방향을 가르키고 있는노드)이다. 위의 예제에서 노드 3의 부모 노드는 1이다. 
  • root node : parent node가 없는 노드이다. 

How do "disjoint set" work

위의 그림과 같이 몇개의 그래프만 있는 경우에는 바로 관계성을 파악할 수 있다. 하지만 그래프가 너무 많은 경우에는?

 옆의 그림과 같이 각 요소가 주어진 경우, 오른쪽의 union에 따라서 왼쪽의 각 요소들을 묶어보자. 

 

 

 

 

 

 

그리고 하나라도 엮여있으면 다 같은 하나로 묶자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

해당 자료구조는 트리구조를 이용해서 표현되는데 배열이 아니라 트리를 사용하는 이유는 무엇일까??

배열을 활용하게 되면, Union 즉 합치는 과정에서 모든 원소를 순회해야 하므로 시간 복잡도가 O(N)이 된다. 물론 이후에 특정 원소가 어느 집합에 속하는지를 찾는 Find연산은 빠르지만 실제로 Union 수행횟수가 더 많기 때문에 Unoin연산이 더 빨라야 한다.

이 때문에 배열 보다 트리 구조가 더 효율적이다. 그렇다면 트리구조로 Union-Find 연산을 어떻게 수행할 수 있을까?

disjoint set을 배열로 표현할 지, 트리로 표현할 지에 대해서는 아래에 좀 더 자세하게 설명할 예정이다

 

"disjoint-set" 이행하기

그렇다면 실제로 disjoint-set은 어떻게 이행되는 걸까? 

프로그램으로 작성하여 컴퓨터가 이해하도록 하여 disjoint-set을 어떻게 실행할 수 있을까?

서로소집합 자료구조는 find function/ union function 이 두개의 중요한 함수의 개념을 알아야 한다.

이 두개의 함수로 서로소 집합자료구조를 조작할 수 있는 것이다.

 root array에는 각 부모노드의 루트 노드를 저장할 예정이다. 

index가 각 노드이며, parent 노드에는 각 노드의 실제 부모 노드를 기재한다. 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • find function : 주어진 노드의 루트 노드를 찾는 함수. 
  • union function : 루트 노드가 같은 두개의 노드를 하나로 합치는 함수. 

disjoint-set을 실행하는 연산

  • 초기화 : N 개의 원소가 각각의 집합에 포함되어 있도록 초기화 합니다.
  • quick find 실행 : 시간복잡도는 O(1)
  • quick union 실행 : 시간복잡도는 O(N)으로 시간이 더 걸리지만, 실행하는데 있어서 더 효율적이다. 

Union 연산(합치기) : 각자 다른 집합 트리의 루트를 찾은뒤, 그 루트가 다르다면, 하나를 다른 한쪽의 자손으로 넣어 두 트리를 합친다

Find 연산(찾기) : 노드에 저장된 포인터 정보를 따라가 주어진 원소가 포함된 트리의 루트 노드를 찾는다. (이때 트리의 높이와 시간 복잡도가 비례한다.)

 

Union-find 연산이란

Disjoint Set을 표현할 때 사용하는 독특한 형태의 자료구조이다.

위에서 Disjoint set실행을 위한 두 연산인 union-find 연산에 대해서 언급했고 간략하게 설명했다. 그리고 이를 구현하기 위해서 배열구조보다는 트리 구조를 표현한다고 하는데 이에 대해서 좀 더 자세하게 다뤄보고자 한다.

 

배열로 표현하기

가장 간단한 방법으로 1차원 배열로 집합을 표현한다.

예를 들어

 번 원소가 속하는 집합의 번호

 

Union-Find 지원 연산

  1. 초기화 : 
     위와 같이 각자 다른 집합 번호로 초기화
  2. Union (합치기) 연산 : 두 집합을 합치기 위해 배열의 모든 원소를 순회 하면서 하나의 집합 번호를 나머지 한개의 집합 번호로 교체. 아래의 그림은 3번 집합을 2번 집합으로 합치는 과정.  
     
 

Find (찾기) 연산 : 한 번만에 원소가 속하는 집합 번호를 알 수 있다.  

 

배열로 Union-Find를 구현 할 수는 있지만, 위에서 보았듯 Union 연산 수행 시 배열의 모든 원소를 순회해야 하기 때문에 시간복잡도가 

 이 된다.
물론 Find 연산은 빠르지만, Union 연산 수행 횟수가 많기 때문에 우리는 더 빠른 Union 연산을 원한다.

 

트리로 표현하기

바로 트리  Union-Find 를 구현하면 배열보다 빠르게 Union 연산을 수행할 수 있다.

Base 1
한 집합에 속하는 원소들을 하나의 트리로 묶어주기 때문에, 자료구조는 아래 그림과 같이 트리 들의 집합으로 표현된다.

Base 2
트리 구조 에는 트리의 대표 노드 라고도 볼 수 있는 루트 노드 가 존재 하므로, 각 원소가 속하는 집합 번호를 바로 이 루트 노드의 원소로 정함.

 

Base 3
Union 연산을 수행하기 위해서는 두 원소가 같은 집합에 속하는지를 먼저 확인한 후, 다른 집합에 속할 때만 합쳐야 한다.
같은 집합에 속한다는 뜻은, 같은 루트 노드를 가진다는 말과 대응되므로 어떤 원소의 루트 노드 를 찾는 Find 연산을 지원해야 한다.

 

Base 4
이러한 Find 연산을 지원하기 위해서 모든 자식 노드가 부모에 대한 포인터 정보를 가지고 있도록 한다. 이러한 설정은 가진 정보를 바탕으로 포인터를 따라가 결과적으로 최종 부모인 루트 노드 가 무었인지 찾을 수 있게 된다.
단, 부모 노드에서 자식 노드로 내려가는 일은 발생하지 않기 때문에, 부모가 자식에 대한 포인터 정보는 가질 필요가 없다.

 

Union-Find 지원 연산

  1. 초기화 : 모두 각자 다른 집합이 된다. 각 노드는 모두 루트 노드 가 되며 N 개의 루트 노드를 생성하고 자기 자신을 가리키는 포인터를 가지도록 설정합니다.
  2. Union (합치기) 연산 : 각 트리의 루트를 찾은 뒤, 다르다면 하나를 다른 한쪽의 자손으로 넣어 두 트리를 합칩니다.
  3. Find (찾기) 연산 : 각 노드에 저장된 포인터 정보를 따라가 주어진 원소가 포함된 트리의 루트 노드를 찾습니다. ( 트리의 높이와 시간복잡도가 비례 )

 

 Find 연산 수행 시 걸리는 시간이 트리의 높이 와 비례하게 되고, Union 연산 수행시간 역시 Find 연산 수행 시간이 지배하게 된다.
이처럼 Union 연산시간 복잡도가 배열로 나타내었을 때의 

보다 줄어 들었다.

 

Quick Find - Find 연산

union find를 구현한 가장 간단한 알고리즘이다. 노드가 연결될 때마다 모든 노드를 순회하면서 연결된 노드를 변경한다.

먼저 각 노드의 루트 노드들을 탐색해서 수정한 다음(union), 찾음(find - connected)

-> 노드 4의 루트 노드를 찾기 위해서 부모 노드인 1 -> 0의 방법으로 루트 노드를 찾음

-> 차례차례로 루트 노드를 찾음.

-> 그리고 각 노드의 부모노드가 아니라 루트 노드를 배열로 저장.

# UnionFind class
class UnionFind:
    def __init__(self, size):
        self.root = [i for i in range(size)]
        # self.root = [0,1,2,3,4,5,6,7,8,9]

    def find(self, x):
        return self.root[x]
		
    def union(self, x, y):
        rootX = self.find(x)
        # x=1, rootX = 1
        rootY = self.find(y)
        # y=2, rootY = 2 
        if rootX != rootY:
            for i in range(len(self.root)):
            # len(self.root) = 10
                if self.root[i] == rootY:
                # i = 2, self.root[2] = 2(rootY)
                    self.root[i] = rootX
                    # self.root[2] = 1
                    # self.root = [0,1,1,3,4,5,6,7,8,9]

    def connected(self, x, y):
        return self.find(x) == self.find(y)


# Test Case
uf = UnionFind(10)
# 1-2-5-6-7 3-8-9 4
uf.union(1, 2)
uf.union(2, 5)
uf.union(5, 6)
uf.union(6, 7)
uf.union(3, 8)
uf.union(8, 9)
#union함수는 연결되어있는 각 노드들을 실제로 연결해주는 역할을 한다. 1과2부터 먼저한 이유는,
#실제 연결된 서로소 집합에서 root노드가 1이므로 이를 중심으로 아래에 노드를 붙여주기 위함이다.
#위의 union함수를 다 거치고 나면
#self.root = [0,1,1,3,4,1,1,1,3,3] 이 된다.
print(uf.connected(1, 5))  # true
print(uf.connected(5, 7))  # true
print(uf.connected(4, 9))  # false
# 1-2-5-6-7 3-8-9-4
uf.union(9, 4)
print(uf.connected(4, 9))  # true

 

  • init: 모든 노드를 순회하면서 초기화하고, N개의 데이터를 저장할 수 있는 메모리가 필요하다. 시간 복잡도는 O(N)이다.
  • union: 모든 노드를 순회하면서 연결된 노드를 변경한다. 시간 복잡도는 O(N)이다.
  • 그러나 find 실행은 O(1)으로 빠르다.
  • connected: 두 노드의 연결 여부를 확인한다. 시간 복잡도는 O(1)이다.
  • union 연산이 모든 노드를 순회하기 때문에 비효율적이다.

 

Quick Union - Union 연산

quick find 알고리즘의 union 연산을 개선하기 위한 알고리즘이다. 노드가 속한 집합을 트리 구조로 형성하고, 노드가 속한 트리의 루트 노드와 연결된 노드만 변경한다.

 

 

 

 

-> quick find에서는 모든 노드를 순회해서  root node를 동일화 한다음 find해준다. 이 과정에서 union에 O(N) 시간 복잡도가 소요된다. 하지만 quick union의 경우에는 기존의 루트 노드만 저장해놓은 다음에 find를 위해서 경로를 추적한다(traverser)

 

 

 

 

-> 실제로 코드를 보면 find와 union의 차이를 느낄 수 있다. 

# UnionFind class
class UnionFind:
    def __init__(self, size):
        self.root = [i for i in range(size)]
        # self.root = [0,1,2,3,4,5,6,7,8,9]
        # self.root = [0,1,1,3,4,5,6,7,8,9]

    def find(self, x):
    	# x=1, self.root[1] = 1
        # x=2, self.root[2] = 1
        while x != self.root[x]:
        #이 부분이 quick find와 다른 점
        #한번의 union으로 root가 결정된 노드에 대해 find->union하려 할때,
        #quick find와 달리, 해당 node를 아예 root값으로 바꾸어주어서, union의 연산 시간을 줄여줌.
        #이를 통해, quick find에서는 매번 union할때 for반복문을 해주어야 했지만, quick union에서는
        #조건문 하나로 바로 union이 가능
            x = self.root[x]
        return x
		
    def union(self, x, y):
        rootX = self.find(x)
        # x=1, rootX=1
        # x=2, rootX=
        rootY = self.find(y)
        # y=2, rootY=2
        if rootX != rootY:
        # 즉 두개의 노드가 union되지 않은 경우,root를 기준으로 union해주기 위해
            self.root[rootY] = rootX
            # self.root[2] = 1, self.root=[0,1,1,3,4,5,6,7,8,9]

    def connected(self, x, y):
        return self.find(x) == self.find(y)


# Test Case
uf = UnionFind(10)
# 1-2-5-6-7 3-8-9 4
uf.union(1, 2)
uf.union(2, 5)
uf.union(5, 6)
uf.union(6, 7)
uf.union(3, 8)
uf.union(8, 9)
print(uf.connected(1, 5))  # true
print(uf.connected(5, 7))  # true
print(uf.connected(4, 9))  # false
# 1-2-5-6-7 3-8-9-4
uf.union(9, 4)
print(uf.connected(4, 9))  # true

  • init: 모든 노드를 순회하면서 초기화하고, N개의 데이터를 저장할 수 있는 메모리가 필요하다. 시간 복잡도는 O(N)이다.
  • root: 노드가 속한 트리의 루트 노드를 반환한다. 시간 복잡도는 O(N)이다.
  • union: 노드가 속한 트리의 루트 노드와 연결된 노드만을 변경한다. 시간 복잡도는 O(N)이다.
  • connected: 두 노드의 연결 여부를 확인한다. 시간 복잡도는 O(N)이다.
  • connected 연산의 시간 복잡도는 증가하였으나, union 연산이 모든 노드들을 순회하지 않고 변경이 필요한 노드만 변경한다.

 

 


Reference

https://www.secmem.org/blog/2021/03/21/Disjoint-Set-Union-find/

https://bowbowbow.tistory.com/26

https://velog.io/@nmrhtn7898/Union-Find

 

728x90

+ Recent posts