정렬
페이지
미리보기
미분류
자세히 보기
기억하면 좋은 알고리즘 정리

GCD / LCM - 유클리드 호제법

최대공약수(GCD)와 최소공배수(LCM)를 구하는 알고리즘

def gcd(a, b): # a = min(a, b) / b = max(a, b)
	if b == 0:
		return b
	if a % b == 0:
		return b
	else:
		return gcd(b, a % b)

def lcm(a, b): # a = min(a, b) / b = max(a, b)
	return int(a * b / gcd(a, b))

소수 찾기- 에라토스테네스의 체

임의의 자연수 n에 대해 그 이하의 소수를 가장 간단하고 빠르게 찾는 알고리즘

  • 핵심 : n의 제곱근 까지만 확인
def eratosthenes(n, arr):
	for i in range(2, int(n ** 0.5) + 1):
		if arr[i]:
			j = 2
			while i * j <= n:
				arr[i * j] = False
				j += 1

# arr의 인덱스가 True이면 해당 인덱스의 수는 소수임
arr = [True for _ in range(n + 1)]

정렬

Untitled

선택 정렬

'''
선택 정렬 : 가장 작은 것을 선택해서 제일 앞으로 보내는 알고리즘
 - 가장 윈시적이고 기초적인 방법이며 비효율적임
시간 복잡도 : O(N^2)
'''
# 스왑 메소드
def swap(a, b):
    temp = a
    a = b
    b = temp
    return (a, b)
# 정렬할 리스트 입력
list = list(map(int, input("선택 정렬할 숫자를 띄어쓰기로 구분하여 입력 : ").split(' ')))
n = len(list)
# 선택 정렬
index = 0
for i in range(0, n):
    min = 9999
    for j in range(i, n):
        if min > list[j]:
            min = list[j]
            index = j
    list[i], list[index] = swap(list[i], list[index])
# 정렬된 리스트 출력
for i in list:
    print(i, end=' ')

버블정렬

'''
버블 정렬 : 옆에 있는 값과 비교하여 더 작은 값을 반복적으로 앞으로 보내는 알고리즘
            (앞에서 부터 큰 값을 뒤로 보내는 것과 같음)
 - 구현이 가장 쉽지만 가장 비효율적인 알고리즘 : 매번 스왑을 해야하기 때문
시간 복잡도 : O(N^2)
'''

# 스왑 메소드
def swap(a, b):
    temp = a
    a = b
    b = temp
    return (a, b)
# 정렬할 리스트 입력
list = list(map(int, input("버블 정렬할 숫자를 띄어쓰기로 구분하여 입력 : ").split(' ')))
n = len(list)
# 버블 정렬
for i in range(0, n):
    for j in range(1, n-i):
        if list[j-1] > list[j]:
            list[j - 1], list[j] = swap(list[j-1], list[j])
# 정렬된 리스트 출력
for i in list:
    print(i, end=' ')

삽입정렬

'''
삽입 정렬 : 각 숫자를 적절한 위치에 삽입하는 알고리즘
            (뒤로 가면서 앞의 숫자 크기를 보고 그 사이에 적절히 삽입)
 - 선택, 버블 정렬에 비해 필요한 경우에만 위치를 바꿔 좀 더 효율적임
 - 미리 어느정도 정렬이 되어 있다면 아주 빠르게 끝낼 수 있음
시간 복잡도 : O(N^2) (최악의 경우)
'''

# 스왑 메소드
def swap(a, b):
    temp = a
    a = b
    b = temp
    return (a, b)
# 정렬할 리스트 입력
list = list(map(int, input("삽입 정렬할 숫자를 띄어쓰기로 구분하여 입력 : ").split(' ')))
n = len(list)
# 삽입 정렬
    # while 문
for i in range(1, n):
    key = list[i] # key : 정렬할 원소
    j = i - 1
    while j >= 0 and list[j] > key:
        list[j+1] = list[j]
        j = j - 1
    list[j+1] = key
    # for 문
'''for i in range(0, n-1):
    #for j in range(0, i+1):
    #    print(list[j], end=' ')
    for j in range(0, i+1):
        if list[i-j] > list[i-j + 1]:
            list[i-j], list[i-j + 1] = swap(list[i-j], list[i-j + 1])
    #print("")'''
# 정렬된 리스트 출력
for i in list:
    print(i, end=' ')

퀵정렬

'''
퀵 정렬 : 대표적인 '분할 정복' 알고리즘(Divide and conquer algorithm)으로,
특정한 값을 기준으로 큰 숫자와 작은 숫자를 서로 교환한 뒤에 배열을 반으로 나누는 것을 반복 하는 알고리즘.
 [규칙]
 - 기준 값인 피벗(Pivot)이 필요 (보통 첫 번째 원소를 피벗으로 사용함)
 - 기준 값보다 큰 값을 배열의 왼쪽부터, 작은 값을 오른쪽부터 찾음.
 - 작은 값의 인덱스가 큰 값의 인덱스보다 크면, 작은 값과 큰 값을 교환.
 - 작은 값의 인덱스가 큰 값의 인덱스보다 작으면, 피벗 값과 작은 값을 교환.
 - 위 과정을 기준 값의 좌우측에 반복
시간 복잡도:    O(N * logN)   (평균)
               O(N^2)        (최악)         -> 피벗값에 따라 최악의 경우가 생길 수 있음 [ex) 이미 정렬된 배열]
'''
# 스왑 메소드
def swap(a, b):
    temp = a
    a = b
    b = temp
    return (a, b)
# 재귀를 이용하여 퀵 정렬 수행
def quickSort(start, end):
    if start>=end:  # 원소가 1개인 경우 그대로 둠
        return
    pivot = start   # 피벗은 첫번째 원소
    i = start + 1
    j = end
    while i <= j:   # 엇갈릴 때까지 반복
        while(i <= end and list[i] <= list[pivot]):     #끝에 도달하거나 피벗보다 큰 값을 찾을 때까지 반복
            i = i + 1
        while(j > start and list[j] >= list[pivot]):    #피벗에 도달하거나 피벗보다 작은 값을 찾을 때까지 반복
            j = j - 1
        if(i > j):  # 엇갈린 상태(작은 값의 인덱스가 큰 값의 인덱스보다 작으면, 피벗 값과 작은 값을 교환)
            list[pivot], list[j] = swap(list[pivot], list[j])
        else:       # 엇갈리지 않은 상태 (작은 값의 인덱스가 큰 값의 인덱스보다 크면, 작은 값과 큰 값을 교환)
            list[i], list[j] = swap(list[i], list[j])
    quickSort(start, j - 1); # 피벗보다 작은 값들에 대해서도 새로운 피벗을 설정하여 퀵정렬 실행
    quickSort(j + 1, end);     # 피벗보다 큰 값들에 대해서도 새로운 피벗을 설정하여 퀵정렬 실행
# 정렬할 리스트 입력
list = list(map(int, input("퀵 정렬할 숫자를 띄어쓰기로 구분하여 입력 : ").split(' ')))
n = len(list)
# 퀵 정렬
quickSort(0, n-1)
# 정렬된 리스트 출력
for i in list:
    print(i, end=' ')

병합정렬

'''
병합 정렬 : 퀵 정렬과 같은 분할 정복 알고리즘을 사용하는 정렬 알고리즘.
            일단 반으로 나누고 나중에 정렬하는 방식
 - 퀵 정렬과 다르게 피벗 값에 상관없이 정확히 반을 나누어 정렬하므로 최악의 경우에도 O(N * logN)의 시간 복잡도를 보장한다.
 [방법]
 - 다 쪼개진 원소들을 합치면서 정렬을 수행
 - 합쳐진 배열은 정렬이 됨
 - 최종적으로 합쳐지면 정렬이된 전체 배열을 얻음
시간 복잡도 : O(N * logN)
'''
# 스왑 메소드
def swap(a, b):
    temp = a
    a = b
    b = temp
    return (a, b)
# 재귀를 이용하여 퀵 정렬 수행
def mergeSort(list):
    if len(list) < 2:
        return list

    mid = len(list) // 2
    low_arr = mergeSort(list[:mid])
    high_arr = mergeSort(list[mid:])

    mergedList = []
    l = h = 0
    while l < len(low_arr) and h < len(high_arr):
        if low_arr[l] < high_arr[h]:
            mergedList.append(low_arr[l])
            l += 1
        else:
            mergedList.append(high_arr[h])
            h += 1
    mergedList += low_arr[l:]
    mergedList += high_arr[h:]
    return mergedList

# 정렬할 리스트 입력
list = list(map(int, input("병합 정렬할 숫자를 띄어쓰기로 구분하여 입력 : ").split(' ')))
n = len(list)
# 병합 정렬
print()
list = mergeSort(list)
# 정렬된 리스트 출력
for i in list:
    print(i, end=' ')

계수정렬

count = [0] * (max(arr) + 1)

for num in arr:
    count[num] += 1

DFS

def dfs(graph, node, visited):
    visited[node] = True
    #print(node, end=' ')
    for v in graph[node]:
        if not visited[v]:
            dfs(graph, v, visited)
	

BFS

from collections import deque

def bfs(graph, start, visited):
    q = deque()
    q.append(start)
    visited[start] = True

    while q:
        node = q.popleft()
        # print(node, end=' ')
        for v in graph[node]:
            if not visited[v]:
                visited[v] = True
                q.append(v)

이진 탐색

기본

def binary_search(array, target, start, end):
	while start <= end:
		mid = (start + end) // 2
		if array[mid] == target:
			return mid
		elif array[mid] > target:
			end = mid - 1
		else:
			start = mid + 1

파라메트릭 서치 - 떡볶이 떡 만들기 예

  • 이진 탐색을 이용한 최적화 (타겟에 가까운 값 구하기)

최단 경로 - 다익스트라

import heapq
INF = int(1e9) #무한을 의미하는 상수

# 노드의 개수 n, 간선의 개수 m
n, m = map(int, input().split())
# 시작 노드 번호 start
start = int(input())
# 각 노드에 연결되어 있는 노드에 대한 정보를 담는 리스트 만들기
graph = [[] for _ in range(n + 1)]
# 방문한 적이 있는지 체크하는 목적의 리스트 만들기
# visited = [False] * (n + 1) # 우선순위 큐를 통해 확인 가능해 졌으므로 제거
# 최단 거리 테이블을 모두 무한으로 초기화
distance = [INF] * (n + 1)

def dijkstra(graph, start, distance):
	q = []
	heapq.heappush(q, (0, start))
	distance[start] = 0
	while q:
		dist, now = heapq.heappop(q)
		if distance[now] < dist:
			continue
		for i in graph[now]:
			cost = distance[now] + i[1]
			if cost < distance[i[0]]:
				distance[i[0]] = cost
				heapq.heappush(q, (cost, i[0]))

최단 경로 - 플로이드 워셜

INF = int(1e9) # 무한을 의미하는 상수

# 노드의 개수 및 간선의 개수 입력
n = int(input())
m = int(input())
# 2차원 리스트(그래프 표현)를 만들고, 모든 값을 무한으로 초기화
graph = [[INF] * (n + 1) for _ in range(n + 1)]

# 자기 자신에서 자기 자신으로 가는 비용은 0으로 초기화
for a in range(1, n + 1):
	graph[a][a] = 0

# 각 간선에 대한 정보를 입력받아, 그 값으로 초기화
for _ in range(m):
	# A에서 B로 가는 비용은 C라고 설정
	a, b, c = map(int, input().split())
	graph[a][b] = c

def floyd_warshall(graph, n):
	for k in range(1, n + 1):
		for a in range(1, n + 1):
			for b in range(1, n + 1):
				graph[a][b] = min(graph[a][b], graph[a][k] + graph[k][b])

최소 신장 트리 - 크루스칼

# 특정 원소가 속한 집합을 찾기
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) # 부모 테이블 초기화

# 모든 간선을 담을 리스트와 최종 비용을 담을 변수
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)

위상 정렬

from collections import deque

# 노드의 개수와 간선의 개수 입력받기
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트(그래프) 초기화
graph = [[] for _ in range(v + 1)]

# 방향 그래프의 모든 간선 정보를 입력받기
for _ in range(e):
	a, b = map(int, input().split())
	graph[a].append(b) # 정점 A에서 B로 이동 가능
	# 진입차수를 1 증가
	indegree[b] += 1

# 위상 정렬 함수
def topology_sort():
	result = [] # 알고리즘 수행 결과를 담을 리스트
	q = deque() # 큐 기능을 위한 deque 라이브러리 사용
	for i in range(1, v + 1):
		if indegree[i] == 0:
			q.append(i)
	while q:
		now = q.popleft()
		result.append(now)
		for i in graph[now]:
			indegree[i] -= 1
			if indegree[i] == 0:
				q.append(i)

추가

최장 증가 부분 수열 (LIS)

# 시간복잡도: O(n^2)
def lis(p):
	n = len(p)
	dp_length = [1] * n
	for i in range(n):
		for j in range(i + 1, n):
			if p[i] < p[j]:
				dp_length[i] = max(dp_length[j], dp_length[i] + 1)
	return max(dp_length) # 가장 긴 값 반환

# 시간복잡도: O(n log n) -> 2진 탐색 활용
def binary_search(arr, target, start, end):
    while start <= end:
        mid = (start + end) // 2
        if arr[mid] == target:
            return mid
        elif arr[mid] > target:
            end = mid - 1
        else:
            start = mid + 1
    return start

def lis(p):
    dp = []
    for i in p:
        idx = binary_search(dp, i, 0, len(dp) - 1)

        if idx == len(dp):
            dp.append(i)
        else:
            dp[idx] = i
    return len(dp)
    
# bisect 모듈의 bisect_left 함수를 사용 => binary_search함수 대신 사용
from bisect import bisect_left
idx = bisect_left(dp, i)
그래프 이론
자세히 보기
그래프 이론 (Graph Theory)

지금까지의 그래프 알고리즘 및 이론 정리

그래프(Graph) 요약

  • 구성요소
    • 노드(Node) / 정점(Vertex)
    • 간선(Edge)
  • 프로그래밍 언어에서 그래프 표현 방법
    • 인접 행렬
    • 인접 리스트

DFS/BFS → DFS/BFS (Graph Traversal)

  • DSF
    • 스택(재귀)을 이용한 그래프 탐색
  • BFS
    • 큐를 이용한 그래프 탐색

최단 경로 → 최단 경로 (Shortest Path)

  • 단일 쌍의 최단거리
    • 다익스트라
      • 그리드
      • 인접 리스트
  • 모든 쌍의 최단거리
    • 플루이드 워셜
      • 다이나믹 프로그래밍
      • 인접 행렬

트리

  • 그래프의 한 종류
  • 부모에서 자식으로 내려오는 계층적 모델
  • 전통적인 수학에서는 무방향 그래프로 간주되지만, 컴퓨터공학 분야에서는 보통 방향 그래프라고 간주
  • $간선의 개수 = 노드의 개수-1$ 인 특징을 가짐

그래프와 트리 비교

Untitled

전체 윤곽 보고가기

서로소 집합 자료구조에 대해 알아봄

→ 무방향 그래프에서의 사이클(순환) 여부 판별에 활용할 수 있음

최소 신장 트리를 구하기 위한 크루스칼 알고리즘에 활용

위상 정렬에 대해 알아봄

서로소 집합 (Disjoint Sets) 자료구조

서로소 집합

  • 공통 원소가 없는 두 집합을 의미
    • 예) 집합 {1, 2}와 {3, 4}는 서로소 관계

서로소 집합 자료구조 (union-find)

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

연산 종류

  • union (합집합) : 2개의 원소가 포함된 집합을 하나의 집합으로 합치는 연산
  • find (찾기) : 특정한 원소가 속한 집합이 어떤 집합인지(루트 노드가 무엇인지) 알려주는 연산

구현 방법

1. union 연산을 확인하여, 서로 연결된 두 노드 A, B를 확인
    i . A와 B의 루트 노드 A’, B’를 각각 찾음
    ii. A’를 B’의 부모 노드로 설정 (B’가 A’를 가리키도록 함)
2. 모든 union 연산을 처리할 때까지 1번 과정을 반복
  • 트리 자료구조 이용
  • A’와 B’ 중에 더 번호가 작은 원소가 부모 노드가 되도록 구현하는 경우가 많음
  • ’가리킨다‘는 의미는 부모 노드로 설정한다는 것

동작 예시

  • 전체 집합 {1, 2, 3, 4, 5, 6}
  • 연산
    • union 1, 4 → 1과 4는 같은 집합 의미
    • union 2, 3 → 2과 3는 같은 집합 의미
    • union 2, 4 → 2과 4는 같은 집합 의미
    • union 5, 6 → 5과 6는 같은 집합 의미
  • 동작 방식
    • union연산을 하나씩 확인(루프) → 서로 다른 두 원소에 대해 합집합을 수행 (각각 루트 노드를 찾아서 더 큰 루트 노드가 더 작은 루트 노드를 가리키도록 설정)

가장 먼저 노드의 개수(V) 크기의 부모 테이블 초기화

  1. 자기 자신을 부모로 갖도록 초기화
  2. 부모 테이블해당 노드의 부모에 대한 정보만을 가짐
    1. 루트 노드 확인을 위해서는 재귀적으로 부모를 거슬러 올라가 최종적인 루트 노드를 찾아야함

union 1, 4 : 1과 4를 합침

1과 4의 루트 노드를 각각 찾아 (현재는 각각 1과 4) 더 큰 번호에 해당하는 루트 노드 4의 부모를 1로 설정

Untitled

union 2, 3 : 2와 3을 합침

2와 3의 루트 노드를 각각 찾아 (현재 각각 2와 3) 더 큰 번호에 해당하는 루트 노드 3의 부모를 2로 설정

Untitled

union 2, 4 : 2와 4를 합침

2와 4의 루트 노드를 각각 찾아 (현재 각각 2와 1) 더 큰 번호에 해당하는 루트 노드 2의 부모를 1로 설정

Untitled

Untitled

union 5, 6 : 5와 6을 합침

5와 6의 루트 노드를 각각 찾아 (현재 각각 5와 6) 더 큰 번호에 해당하는 루트 노드 6의 부모를 5로 설정

Untitled

union 연산 모두 완료

  • 위 과정에서 유의할 점
    • 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=' ')
print()

경로 압축 기법을 적용한 서로소 집합 알고리즘

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

기본적인 서로소 집합 알고리즘의 문제점

  • find 함수가 비효율적으로 동작함
    • find 시 최악의 경우 $O(V)$가 될 수 있음 → 전체 시간 복잡도가 $O(VM)$ (M: union 연산 개수) 가 될 수 있음

경로 압축 (Path Compression) 기법 적용

  • find 함수를 재귀적으로 호출한 뒤에 부모 테이블의 값을 갱신하는 기법
  • 각 노드에 대해 find 함수를 호출한 이후에, 해당 노드의 루트 노드가 바로 부모 노드가 되어 시간 복잡도가 개선됨
  • 시간 복잡도 : $O(V+M(1+log_{2-M/V}V))$
    • 노드 1000개에 약 100만번의 union/find연산 → 1000만번 가량 연산 필요

서로소 집합 활용 - 사이클 판별

  • 서로소 집합은 무방향 그래프 내에서의 사이클 판별 가능

<aside> 💡 DFS를 이용해서도 가능

</aside>

동작 과정

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

동작 예시

초기 단계 : 모든 노드에 대해 자기 자신을 부모로 설정하는 형태로 부모 테이블 초기화

Untitled

(1, 2) 간선 확인

Untitled

(1, 3) 간선 확인

Untitled

(2, 3) 간선 확인

Untitled

이 때 2와 3이 이미 루트 노드로 ‘노드 1’을 가지고 있음

사이클이 발생

구현

# 특정 원소가 속한 집합을 찾기
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
	# 사이클이 발생하지 않았다면 합집합 수행
	else:
		union_parent(parent, a, b)

print('사이클 발생') if cycle else print('사이클 미발생')

크루스칼(Kruskal) 알고리즘

신장 트리 (Spanning Tree)

  • 하나의 그래프가 있을 때 모든 노드를 포함하면서 사이클이 존재하지 않는 부분 그래프
  • 예)

기본 그래프

Untitled

신장 트리

Untitled

신장 트리가 아닌 것들

Untitled

  • 비포함 노드 존재
  • 비포함 노드 존재 / 사이클 존재
  • 최소 신장 트리 (MST; Mininum Spanning Tree)

신장 트리 중에서 최소 비용으로 만들 수 있는 신장 트리

예)

Untitled

개념 정리

  • 가장 적은 비용으로 모든 노드를 연결할 수 있게 해주는 알고리즘
    • 최소 신장 트리 알고리즘
    • 그리디 알고리즘
  • 시간 복잡도
    • $O(ElogE)$ (E = 간선의 개수)
      • E개의 간선을 정렬하는 작업의 시간 복잡도와 같다고 할 수 있음
      • 크루스칼 내부에서 사용되는 서로소 집합 알고리즘 복잡도는 작으므로 무시함

동작 과정

  • 모든 간선에 대해 정렬을 수행 → 가장 거리가 짧은 간선부터 집합에 포함 (그리디)
  • 이 때 사이클을 발생시킬 수 있는 간선의 경우, 집합에 포함시키지 않음
  1. 간선 데이터를 비용에 따라 오름차순 정렬
  2. 간선을 하나씩 확인하며 현재의 간선이 사이클을 발생시키는지 확인
    1. 사이클이 발생하지 않는 경우 최소 신장 트리에 포함시킴
    2. 사이클이 발생하는 경우 최소 신장 트리에 포함시키지 않음
  3. 모든 간선에 대해 2번의 과정을 반복

동작 예시

초기 단계 : 그래프의 모든 간선 정보만 빼내어 정렬 수행

Untitled

  • 실제로는 전체 간선 데이터를 리스트에 담은 뒤에 정렬
  • 여기서는 가독성을 위해 노드 데이터 순서에 따라 나열함

가장 짧은 간선(3, 4)을 선택해 집합에 포함 (union 실행)

Untitled

그 다음 작은 간선(4, 7)을 선택 → 4, 7이 같은 집합에 속하는지 확인 (사이클 확인) → union 실행

Untitled

그 다음 작은 간선(4, 6)을 선택 → 4, 6이 같은 집합에 속하는지 확인 (사이클 확인) → union 실행

Untitled

그 다음 작은 간선(6, 7)을 선택 → 4, 7이 같은 집합에 속하는지 확인 (사이클 확인) → 이미 동일한 집합에 포함 → union 미실행

Untitled

그 다음 작은 간선(1, 2)을 선택 → 1, 2이 같은 집합에 속하는지 확인 (사이클 확인) → union 실행

Untitled

그 다음 작은 간선(2, 6)을 선택 → 2, 6이 같은 집합에 속하는지 확인 (사이클 확인) → union 실행

Untitled

그 다음 작은 간선(2, 3)을 선택 → 2, 3이 같은 집합에 속하는지 확인 (사이클 확인) → 이미 동일한 집합에 포함 → union 미실행

Untitled

그 다음 작은 간선(5, 6)을 선택 → 5, 6이 같은 집합에 속하는지 확인 (사이클 확인) → union 실행

Untitled

그 다음 작은 간선(1, 5)을 선택 → 1, 5이 같은 집합에 속하는지 확인 (사이클 확인) → 이미 동일한 집합에 포함 → union 미실행

Untitled

최종 최소 신장 트리

Untitled

  • 총 비용 : 최소 신장 트리의 간선을 모두 더한 값

구현

# 특정 원소가 속한 집합을 찾기
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) # 부모 테이블 초기화

# 모든 간선을 담을 리스트와 최종 비용을 담을 변수
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)

입력

7 9
1 2 29
1 5 75
2 3 35
2 6 34
3 4 7
4 6 23
4 7 13
5 6 53
6 7 25

위상 정렬 (Topology Sort)

진입차수(Indegree)

  • 특정한 노드로 ‘들어오는’ 간선의 개수

개념 정리

순서가 정해져 있는 일련의 작업을 차례대로 수행해야 할 때 사용할 수 있는 알고리즘

방향 그래프의 모든 노드를 ‘방향성에 거스르지 않도록 순서대로 나열하는 것’을 의미

현실 세계에서의 위상 정렬 예시

선수과목을 고려한 학습 순서 설정

  • 컴공 커리큘럼 : 자료 구조 (선수과목) → 알고리즘 → 고급 알고리즘

그래프상에서 선후 관계가 있다면, 위상 정렬을 수행하여 모든 선후 관계를 지키는 전체 순서를 계산 가능

Untitled

진입차수

  • 자료구조 : 0
  • 알고리즘 : 1
  • 고급 알고리즘 : 2

시간 복잡도

  • $O(V+E)$
    • 차례대로 모든 노드를 확인하면서, 해당 노드에서 출발하는 간선을 차례로 제거하는 시간

동작 과정

  1. 진입차수가 0인 노드를 큐에 넣음
  2. 큐가 빌 때까지 다음의 과정을 반복
    1. 큐에서 원소를 꺼내 해당 노드에서 출발하는 간선을 그래프에서 제거
    2. 새롭게 진입차수가 0이 된 노드를 큐에 넣음
  • 이때 모든 원소를 방문하기 전에 큐가 빈다면, 사이클이 존재
    • 큐에서 원소가 V번 추출되기 전에 큐가 비어버리면 사이클이 발생한 것임
    • 사이클이 존재하는 경우 포함되어 있는 원소 중에서 어떠한 원소도 큐에 들어가지 못하기 때문
      • 진입차수가 0이 되지 못함

동작 예시

Untitled

초기 단계 : 진입 차수가 0인 노드큐에 삽입. 현재 노드 1의 진입차수만 0이므로 큐에 1만 삽입

Untitled

큐에 들어 있는 노드 1을 꺼내고, 노드 1과 연결된 간선들을 제거 → 그러면 새롭게 노드 2와 노드 5의 진입차수가 0이됨 → 따라서 2와 5 큐에 삽입

Untitled

큐에 들어 있는 노드 2을 꺼내고, 노드 2과 연결된 간선들을 제거 → 그러면 새롭게 노드 3이 진입차수가 0이됨 → 따라서 3을 큐에 삽입

Untitled

큐에 들어 있는 노드 5을 꺼내고, 노드 5과 연결된 간선들을 제거 → 그러면 새롭게 노드 6이 진입차수가 0이됨 → 따라서 6을 큐에 삽입

Untitled

큐에 들어 있는 노드 3을 꺼내고, 노드 3과 연결된 간선들을 제거 → 진입차수가 0이되는 노드가 없으므로 그냥 넘어감

Untitled

큐에 들어 있는 노드 6을 꺼내고, 노드 6과 연결된 간선들을 제거 → 그러면 새롭게 노드 4이 진입차수가 0이됨 → 따라서 4을 큐에 삽입

Untitled

큐에 들어 있는 노드 4을 꺼내고, 노드 4과 연결된 간선들을 제거 → 그러면 새롭게 노드 7이 진입차수가 0이됨 → 따라서 7을 큐에 삽입

Untitled

큐에 들어 있는 노드 7을 꺼내고, 노드 7과 연결된 간선들을 제거 → 진입차수가 0이되는 노드가 없으므로 그냥 넘어감

Untitled

큐가 비었으므로 종료됨

큐에서 나간 순서대로 1 - 2 - 5 - 3 - 6 - 4 - 7 또는 1 - 5 - 2 - 3 - 6 - 4 - 7 이 해가 됨

  • 위 과정에서 큐에서 빠져나간 노드를 순서대로 출력하면 그것이 위상 정렬을 수행한 결과가 됨
  • 위상 정렬을 수행한 결과의 해는 여러 개가 될 수 있음
    • 한 단계에서 큐에 들어가는 원소가 2개 이상인 경우

구현

from collections import deque

# 노드의 개수와 간선의 개수 입력받기
v, e = map(int, input().split())
# 모든 노드에 대한 진입차수는 0으로 초기화
indegree = [0] * (v + 1)
# 각 노드에 연결된 간선 정보를 담기 위한 연결 리스트(그래프) 초기화
graph = [[] for _ in range(v + 1)]

# 방향 그래프의 모든 간선 정보를 입력받기
for _ in range(e):
	a, b = map(int, input().split())
	graph[a].append(b) # 정점 A에서 B로 이동 가능
	# 진입차수를 1 증가
	indegree[b] += 1

# 위상 정렬 함수
def topology_sort():
	result = [] # 알고리즘 수행 결과를 담을 리스트
	q = deque() # 큐 기능을 위한 deque 라이브러리 사용

	# 처음 시작할 때는 진입차수가 0인 노드를 큐에 삽입
	for i in range(1, v + 1):
		if indegree[i] == 0:
			q.append(i)
	
	# 큐가 빌 때까지 반복
	while q:
		# 큐에서 원소 꺼내기
		now = q.popleft()
		result.append(now)
		# 해당 원소와 연결된 노드들의 진입차수에서 1 빼기
		for i in graph[now]:
			indegree[i] -= 1
			# 새롭게 진입차수가 0이 되는 노드를 큐에 삽입
			if indegree[i] == 0:
				q.append(i)

	#위상 정렬을 수행한 결과 출력
	for i in result:
		print(i, end=' ')

topology_sort()

입력

7 8
1 2
1 5
2 3
2 6
3 4
4 7
5 6
6 4

문제 풀이

팀 결성

Untitled

풀이

import sys
input = sys.stdin.readline

def find(p, x):
	if p[x] != x:
		p[x] = find(p, p[x])
	return p[x]

def union(p, a, b):
	a = find(p, a)
	b = find(p, b)
	if a < b:
		p[b] = a
	else:
		p[a] = b

n, m = map(int, input().split())
p = [0] * (n + 1)
for i in range(1, n + 1):
	p[i] = i

for i in range(m):
	op, a, b = map(int, input().split())
	if op == 0:
		union(p, a, b)
	else:
		print('YES') if find(p, a) == find(p, b) else print('NO')

입력

7 8
0 1 3
1 1 7
0 7 6
1 7 1
0 3 7
0 4 2
0 1 1
1 1 1

도시 분할 계획

Untitled

Untitled

풀이

import sys
input = sys.stdin.readline

def find(p, x):
    if p[x] != x:
        p[x] = find(p, p[x])
    return p[x]

def union(p, a, b):
    a = find(p, a)
    b = find(p, b)
    if a < b:
        p[b] = a
    else:
        p[a] = b

n, m = map(int, input().split())

p = [i for i in range(n + 1)]

edges = []
for i in range(m):
    a, b, c = map(int, input().split())
    edges.append((c, a, b))
edges.sort()

result = 0
last_road = 0                               # 가장 큰 비용의 신장트리 길을 저장하기 위함
for c, a, b in edges:
    if find(p, a) != find(p, b):
        union(p, a, b)
        result += c
        last_road = c

print(result - last_road)                   # 가장 큰 비용의 신장트리 길을 없애 2개의 마을을 만듬

입력

7 12
1 2 3
1 3 2
3 2 1
2 5 2
3 4 4
7 3 6
5 1 5
1 6 2
6 4 1
6 5 3
4 5 3
6 7 4

커리큘럼

Untitled

Untitled

풀이


입력

5
10 -1
10 1 -1
4 1 -1
4 3 1 -1
3 3 -1

1922번: 네트워크 연결

2252번: 줄 세우기