반응형
Notice
Recent Posts
Recent Comments
Link
관리 메뉴

짧은코딩

그래프 이론, 서로소 집합 본문

코딩 테스트(Python)/이것이 취업을 위한 코딩 테스트다

그래프 이론, 서로소 집합

5_hyun 2022. 7. 6. 21:39
반응형

그래프 구현 방법

그래프 구현 방법은 2가지 방식이 존재한다. 하나는 2차원 배열을 사용하는 방식인 인접 행렬(Adjacency Matrix)이 있고 다른 하나는 리스트를 사용하는 방식인 인접 리스트(Adjacency List) 방식이 있다. 노드의 개수가 V개, 간선의 개수가 E인 그래프가 있다고 하고 두 방식을 비교한다.

 

1. 인접 행렬

메모리 공간: 간선의 정보를 저장하기 위해 O(v^2)의 메모리 공간 필요

다른 노드까지의 가중치를 알아내는 시간: O(1)의 시간으로 즉시 알아낼 수 있음

 

2. 인접 리스트

메모리 공간: 간선의 개수만큼인 O(E)의 메모리 공간이 필요

다른 노드까지의 가중치를 알아내는 시간: O(V)만큼의 시간이 요소

 

앞에서 배운 다익스트라 알고리즘은 인접 리스트를 이용하는 방식이고 플로이드 알고리즘은 인접 행렬을 이용하는 방식이다. 어떤 문제를 만나든 메리와 시간을 염두해서 알고리즘을 선택하여 구현해야한다. 예를 들면 최단 경로를 찾아야 하는 문제가 있을 때, 노드의 개수가 작으면 플로이드 알고리즘, 반면에 노드와 간선의 개수가 많으면 우선순위 큐를 이용하는 다익스트라 알고리즘을 이용하면 유리하다.


서로소 집합

수학에서 서로소 집합(Disjoint Sets)는 공통 원소가 없는 두 집합을 의미한다. 서로소 집합 자료 구조는 서로소 부분 집합들로 나누어진 원소들의 데이터를 처리하기 위한 자료구조이다. 서로소 집합 자료구조는 두 집합을 하나의 집합으로 합치는 union 연산과 특정 원소가 어떤 집합에 속해있는지 알려주는 find 연산이 있다.

 

-원리

서로소 집합 자료구조를 표현할 때는 트리 자료구조를 활용한다.

  1. union 연산을 해서 서로 연결된 두 노드 A, B를 확인한다. 이때 A와 B의 루트 노드를 찾고 A의 루트 노드를 B의 부모 노드로 설정한다. 즉, B의 루트 노드가 A의 루트 노드를 가리키도록 한다.(실제 구현할 땐 더 번호가 작은 원소가 부모 노드가 되도록 구현)
  2. 모든 union 연산을 처리할 때 까지 1번을 반복한다.

예시

이렇게 4개의 union 연산이 있다. 1과 4는 같은 집합, 2와 3은 같은 집합, 2와 4는 같은 집합, 5와 6은 같은 집합 이런 의미이다.

union 연산은 간선으로 표현할 수 있다. 따라서 6개의 노드가 있고 4개의 간선이 존재하는 그래프로 바꿀 수 있다.

 

이렇게 보면 {1, 2, 3, 4}와 {5, 6} 두 집합으로 나눠지는 것을 볼 수 있다.

 



이런 과정을 거치면서 각 노드의 부모 노드는 처음엔 자기 자신이었다가 화살표가 생기면 화살표가 가리키는 노드가 부모 노드가 된다. 3번 노드의 경우에는 2을 가리키지만 부모 노드는 1이라고 볼 수 있다. union 연산을 효과적으로 수행하기 위해서는 부모 테이블을 항상 가지고 있는 것이 좋다.

 

-코드

# 특정 원소가 속한 집합을 찾기
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=' ')

find_parent는 특정 원소가 속한 집합의 루트 노드를 찾는다. union_parent는 두 원소가 속한 집합을 찾아서 낮은 숫자 노드인 것을 큰 숫자의 부모 노드로 만들어준다.

 

하지만 이렇게 하면 find 함수가 모든 노드를 다 확인하기 때문에 최악의 경우 시간 복잡도가 O(V)이다. 그렇기에 find 함수에 경로 압축 기법을 적용하면 시간 복잡도를 개선시킬 수 있다. 부모 테이블 값을 갱신하는 기법이다.

# 특정 원소가 속한 집합을 찾기
def find_parent(parent, x):
    # 루트 노드가 아니라면, 루트 노드를 찾을 때까지 재귀적으로 호출
    if parent[x] != x:
        parent[x] = find_parent(parent, parent[x])
    return parent[x]

이렇게 수정하면 각 노드에 대해서 find 함수를 호출하고 해당 노드의 루트 노드가 바로 부모 노드가 된다. 이렇게 만들면 루트 노드에 더욱 빠르게 접근할 수 있어서 시간 복잡도가 개선된다.

 

-개선된 코드

# 특정 원소가 속한 집합을 찾기
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

# 노드의 개수와 간선(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=' ')

 

-서로소 집합 알고리즘의 시간 복잡도

서로소 집합 알고리즘을 구현할 때, 노드 V개, union 연산 최대 V - 1개, M개의 find 연산이 가능하고 경로 압축 방법만을 이용하면 시간 복잡도는

시간 복잡도

 

서로소 집합을 활용한 판별

서로소 집합은 무방향 그래프 내에서 사이클을 판별할 때 사용할 수 있다. 사이클은 DFS를 이용해서 판별할 수 있다. union 연산은 그래프에서 간선으로 표현할 수 있어서 간선을 하나씩 확인하면서 두 노드가 포함되어 있는 집합을 합치는 과정을 반복하여 사이클을 판별할 수 있다.

  1. 각 간선을 확인하면서 두 노드의 루트 노드를 확인, 1) 루트 노드가 서로 다르면 두 노드를 union, 2) 루트 노드가 다르면 사이클이 발생한 것
  2. 그래프에 포함되어 있는 모든 간선에 대해 1번 과정 반복

-과정

이렇게 모든 간선을 확인하면서 부모 테이블을 갱신한다. 사이클 판별 알고리즘은 간선의 개수가 E일 때 모든 간선을 확인하며 각 간선에 대해 union, find 함수를 호출하는 방식으로 동작한다. 무방향 그래프에서만 적용 가능하다.

 

-예시 코드

# 특정 원소가 속한 집합을 찾기
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

# 노드의 개수와 간선(Union 연산)의 개수 입력 받기
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
    # 사이클이 발생하지 않았다면 합집합(Union) 연산 수행
    else:
        union_parent(parent, a, b)

if cycle:
    print("사이클이 발생했습니다.")
else:
    print("사이클이 발생하지 않았습니다.")

-find_parent

재귀적과 경로 압축을 사용해서 루트 노드를 찾는다. 테이블에 자기와 같은 번호가 있으면 루트 노드이다.

 

-union_parent

더 작은 번호의 루트 노드를 가진 집합과 합쳐준다.

 

모든 간선을 돌면서 union 연산을 해주고 cycle 변수를 둬서 사이클이 발생해주면 종료한다.

 

반응형

'코딩 테스트(Python) > 이것이 취업을 위한 코딩 테스트다' 카테고리의 다른 글

위상 정렬(Topology Sort)  (0) 2022.07.10
신장 트리, 크루스칼 알고리즘  (0) 2022.07.09
전보  (0) 2022.07.05
미래 도시  (0) 2022.07.04
플로이드 워셜 알고리즘  (0) 2022.07.04
Comments