파이썬 코딩테스트 강좌, 유니온 파인드

안녕하세요! 이번 포스트에서는 유니온 파인드(Union-Find) 알고리즘에 대해 알아보고, 이를 활용한 알고리즘 문제를 풀이해 보도록 하겠습니다. 유니온 파인드는 서로소 집합을 관리하는데 매우 유용한 자료구조로, 여러 항목들의 연결 여부를 빠르게 판단하고 집합을 합치는 기능을 제공합니다.

유니온 파인드란?

유니온 파인드는 크게 두 가지 기능으로 구성됩니다:

  • Union: 두 원소가 속한 집합을 하나로 합치는 연산
  • Find: 특정 원소가 어느 집합에 속하는지 찾는 연산

이 두 운영을 통하여, 우리는 여러 요소 간의 관계를 쉽게 관리할 수 있으며, 복잡한 연결 문제를 효율적으로 해결할 수 있습니다. 특히, 그래프 문제나 네트워크 연결 문제에서 유용하게 사용됩니다.

유니온 파인드의 작동 원리

유니온 파인드는 주로 두 가지 기법을 사용하여 효율성을 극대화합니다:

1. 경로 압축(Path Compression)

Find 연산을 수행할 때, 경로를 압축하여 트리의 높이를 줄입니다. 이를 통해, 여러 번의 Find를 수행할 때 시간 복잡도를 감소시킬 수 있습니다.

2. Union by Rank

Union 연산 시 집합의 ‘랭크(높이)’를 고려하여, 더 낮은 랭크의 집합을 높은 랭크의 집합에 붙입니다. 이 방법 또한 트리의 깊이를 줄여 연산의 효율성을 높입니다.

문제: 연결 요소의 개수 구하기

다음과 같은 문제를 고려해 보세요:

주어진 n개의 노드와 m개의 간선이 있을 때, 서로 연결된 노드 집합의 개수를 구하시오. 노드는 0부터 n-1까지의 정수를 가지며, 간선 정보는 (a, b)의 형태로 주어진다.

입력 예시

    5 3
    0 1
    1 2
    3 4
    

출력 예시

    2
    

해결 과정

이 문제를 해결하기 위해 유니온 파인드 자료구조를 활용하여 각 노드의 관계를 정리할 것입니다. 아래 단계를 따라 진행하겠습니다:

1단계: 부모 배열 초기화

각 노드는 자기 자신을 부모로 가지는 상태로 초기화합니다. 즉, 부모 배열을 생성하고 각 노드가 자기 자신을 가리키도록 설정합니다.

    parent = [0, 1, 2, 3, 4]  # 노드 수에 맞춰 초기화
    

2단계: Union 연산 수행

주어진 간선 정보를 기반으로 Union 연산을 통해 노드들을 연결합니다. 이를 통해 서로 연결된 집합을 하나로 묶습니다.

3단계: Find 연산을 통해 연결된 집합 카운팅

각 노드에 대해 Find 연산을 수행하여 어떤 집합에 속하는지를 확인하고, 유일한 루트 노드의 개수를 센 후 결과를 출력합니다.

구현

이제 위의 과정들을 파이썬 코드로 구현해 보겠습니다.

    class UnionFind:
        def __init__(self, size):
            self.parent = list(range(size))
            self.rank = [1] * size
        
        def find(self, p):
            if self.parent[p] != p:
                # 경로 압축
                self.parent[p] = self.find(self.parent[p])
            return self.parent[p]
        
        def union(self, p, q):
            rootP = self.find(p)
            rootQ = self.find(q)
            
            if rootP != rootQ:
                # Union by rank
                if self.rank[rootP] > self.rank[rootQ]:
                    self.parent[rootQ] = rootP
                elif self.rank[rootP] < self.rank[rootQ]:
                    self.parent[rootP] = rootQ
                else:
                    self.parent[rootQ] = rootP
                    self.rank[rootP] += 1

    # 사용 예제
    def count_connected_components(n, edges):
        uf = UnionFind(n)
        
        # 간선 정보로 Union 수행
        for u, v in edges:
            uf.union(u, v)
        
        # 유일한 루트의 수를 세어 연결 구성 요소의 개수를 반환
        root_set = set(uf.find(i) for i in range(n))
        return len(root_set)

    # 입력 처리
    n = 5
    edges = [(0, 1), (1, 2), (3, 4)]
    print(count_connected_components(n, edges))  # 출력: 2
    

결과 해석

위 코드를 실행하면 2라는 결과가 출력됩니다. 이는 주어진 그래프에서 서로 연결된 노드 집합이 두 개 존재함을 의미합니다.

시간 복잡도 분석

유니온 파인드를 사용했을 때, 각 운영의 시간 복잡도는 거의 O(α(n))에 가까워지며, 여기서 α는 아커만 함수의 역함수입니다. 이는 매우 느리게 증가하는 함수이므로, 실제로는 상수에 가까운 시간을 소요한다고 볼 수 있습니다.

마무리

이번 포스트에서는 유니온 파인드 자료구조와 이를 활용한 문제 해결 방법을 살펴보았습니다. 유니온 파인드를 통해 복잡한 연결 문제를 효율적으로 해결할 수 있음을 확인했는데요, 다양한 응용을 해보며 연습해 보시기 바랍니다.

감사합니다!

파이썬 코딩테스트 강좌, 원하는 정수 찾기

안녕하세요! 이번 강좌에서는 ‘원하는 정수 찾기’라는 알고리즘 문제를 함께 풀어보겠습니다. 이 문제는 n개의 정수로 이루어진 리스트에서 특정 정수를 찾는 문제입니다. 이 과정에서 기본적인 알고리즘 및 데이터 구조에 대해 알아보는 시간을 가질 것입니다. 문제 설정과 함께 해결 방안을 단계별로 설명하겠습니다.

문제 설명

주어진 정수 리스트 nums와 정수 target이 있을 때, target이 리스트 nums에 존재하는 인덱스를 반환하는 함수를 작성하세요. 만약 target이 리스트에 존재하지 않으면 -1을 반환합니다.

입력 예시

  • nums = [2, 5, 1, 8, 3], target = 8 => 반환값: 3
  • nums = [2, 5, 1, 8, 3], target = 4 => 반환값: -1

문제 접근 방법

이 문제를 해결하기 위해 우리는 두 가지 접근 방식을 고려할 수 있습니다: 선형 검색과 이진 검색입니다. 두 방법은 각각의 장단점이 있으며, 성능을 비교해보는 것도 좋은 학습이 될 것입니다.

1. 선형 검색

선형 검색은 리스트를 처음부터 끝까지 순차적으로 탐색하면서 target을 찾는 방법입니다. 시간 복잡도는 O(n)입니다.

선형 검색 구현

def linear_search(nums, target):
    for index in range(len(nums)):
        if nums[index] == target:
            return index
    return -1

위 코드에서 for 루프는 리스트의 각 요소를 하나씩 확인합니다. target과 같은 요소를 찾으면 해당 인덱스를 반환하며, 리스트의 끝까지 탐색해도 찾지 못하면 -1을 반환합니다.

2. 이진 검색

이진 검색은 정렬된 리스트에서 사용할 수 있는 효율적인 검색 방법입니다. 리스트를 중간값과 비교하며 검색 범위를 절반으로 줄여나갑니다. 시간 복잡도는 O(log n)입니다. 따라서 이 방법은 검색 효율성이 중요한 경우 유용합니다.

이진 검색 구현

def binary_search(nums, target):
    left, right = 0, len(nums) - 1
    while left <= right:
        mid = (left + right) // 2
        if nums[mid] == target:
            return mid
        elif nums[mid] < target:
            left = mid + 1
        else:
            right = mid - 1
    return -1

위의 이진 검색 구현에서는 leftright 변수를 사용하여 리스트의 검색 범위를 조정합니다. 매 루프마다 중간값을 계산하고, 이 값과 target을 비교하여 검색 범위를 조정합니다.

문제 해결 과정

이제 주어진 문제를 해결하기 위해 두 개의 접근 방식을 모두 구현하고, 성능을 비교해보겠습니다.

성능 비교

선형 검색과 이진 검색의 성능을 비교하기 위해, 각 검색 알고리즘을 일정한 크기의 리스트 데이터에 대해 수행해보겠습니다. 다음은 성능 비교를 위한 테스트 코드입니다.

import random
import time

# 데이터 생성
size = 1000000
nums = sorted(random.sample(range(1, 10000000), size))
target = random.choice(nums)

# 선형 검색 성능 테스트
start_time = time.time()
print("Linear Search Result: ", linear_search(nums, target))
print("Linear Search Time: ", time.time() - start_time)

# 이진 검색 성능 테스트
start_time = time.time()
print("Binary Search Result: ", binary_search(nums, target))
print("Binary Search Time: ", time.time() - start_time)

위 코드를 실행하면 두 검색 알고리즘의 수행 시간과 결과를 확인할 수 있습니다. 대체로 이진 검색이 더 빠른 성능을 보일 것입니다.

결론

이번 강좌에서는 ‘원하는 정수 찾기’ 문제를 선형 검색과 이진 검색 두 가지 방법으로 해결해보았습니다. 각각의 알고리즘이 어떻게 작동하는지, 그리고 어떤 상황에서 사용해야 하는지에 대해 알아보았으니, 실제 코딩 테스트나 알고리즘 문제 풀이에 있어 큰 도움이 될 것입니다.

앞으로도 다양한 알고리즘 문제를 통해 코딩 능력을 향상시켜 나가시길 바랍니다. 감사합니다!

파이썬 코딩테스트 강좌, 위상 정렬

위상 정렬(Topological Sorting)은 Directed Acyclic Graph (DAG, 방향성이 있는 비순환 그래프)에서 노드들을 정렬하는 방법입니다. 모든 간선이 의 노드에서 아래의 노드로 향하는 순서로 배열 하는 것입니다. 이 정렬은 주로 작업의 수행 순서나, dependency(의존성) 및 다양한 프로그래밍 문제에서 사용됩니다.

문제 설명

주어진 수업 개수 N과 각 수업 간의 선후 관계를 나타내는 edges 리스트가 있습니다. 수업을 들을 수 있는 순서를 위상 정렬 알고리즘을 사용하여 구하는 문제입니다.

입력 형식

N = 6
edges = [(2, 1), (3, 1), (4, 1), (6, 4), (5, 2), (5, 3)]

출력 형식

1 2 3 4 5 6 (수업을 들을 수 있는 한 가지 순서)

문제 풀이 과정

1. 문제 이해하기

위상 정렬 문제는 주어진 노드들(N)과 간선 정보(edges)를 통해 선후 관계를 정립한 후, 모든 노드를 정렬하는 것입니다. 여기서 edges는 (A, B) 형태로 표현되며, A를 먼저 들어야 B를 들을 수 있음을 의미합니다.

2. 입력 파라미터 처리

위상 정렬을 구현하기 위해 우선 그래프의 인접리스트 및 진입 차수 배열을 구성해야 합니다. 진입 차수 배열은 각 노드가 들어야 할 수업의 수를 카운트합니다.

from collections import deque

def topological_sort(N, edges):
    # 그래프와 진입차수 초기화
    graph = [[] for _ in range(N + 1)]
    in_degree = [0] * (N + 1)
    
    # 간선 정보를 그래프와 진입차수에 등록
    for u, v in edges:
        graph[u].append(v)
        in_degree[v] += 1

3. 위상 정렬 알고리즘 설계

이제, 위상 정렬을 수행하기 위한 알고리즘을 설계하겠습니다. 진입 차수가 0인 노드를 큐(Queue)에 추가하고, 큐에서 노드를 하나씩 꺼내면서 그 노드와 연결된 다른 노드들의 진입 차수를 감소시킵니다. 감소 후 진입 차수가 0이 되는 노드는 다시 큐에 추가됩니다. 이 과정을 큐가 빌 때까지 반복합니다. 최종적으로는 정렬된 노드의 순서를 반환합니다.

    # 진입차수가 0인 노드를 큐에 추가
    queue = deque()
    for i in range(1, N + 1):
        if in_degree[i] == 0:
            queue.append(i)

    result = []
    
    while queue:
        current = queue.popleft()
        result.append(current)
        
        for neighbor in graph[current]:
            in_degree[neighbor] -= 1
            if in_degree[neighbor] == 0:
                queue.append(neighbor)

    return result

4. 전체 코드 작성 및 실행

이제 전체 코드를 통합하여 최종 코드를 작성하겠습니다. 처리한 입력값을 함수에 전달하여 결과를 확인할 수 있습니다.

N = 6
edges = [(2, 1), (3, 1), (4, 1), (6, 4), (5, 2), (5, 3)]

sorted_order = topological_sort(N, edges)
print("수업을 들을 수 있는 순서:", sorted_order)

5. 결과 및 평가

위 코드를 실행하면 위상 정렬을 통해 수업을 들을 수 있는 순서를 구할 수 있습니다. 이 과정에서 그래프의 성질에 따라 여러 개의 올바른 정답이 나올 수 있습니다. 따라서 알고리즘이 올바르게 작동했다면 결과의 유효성을 폭넓게 검증할 필요가 있습니다.

6. 코드 및 알고리즘 최적화

위상 정렬 알고리즘의 시간 복잡도는 O(V + E)입니다. 여기서 V는 정점(vertex)의 수, E는 간선(edge)의 수입니다. 이 알고리즘은 대규모 데이터셋에서도 효율적으로 동작할 수 있기 때문에 취업 코딩테스트에 유용한 도구가 될 수 있습니다.

결론

위상 정렬은 그래프 이론에서 유용하게 사용되는 알고리즘으로, 다양한 문제에 응용될 수 있습니다. 이번 강좌에서는 파이썬을 이용해 위상 정렬을 구현해보았고, 실전 코딩테스트에 적합한 내용을 제공하였습니다. 앞으로도 이러한 알고리즘 문제를 깊이 있게 이해하고 활용하시길 바랍니다.

참고 자료

파이썬 코딩테스트 강좌, 오큰수 구하기

안녕하세요! 오늘은 취업 준비생들에게 매우 유용한 알고리즘 문제인 ‘오큰수 구하기’를 소개하고 자세히 설명하는 시간을 가지겠습니다. 이 문제는 스택(Stack)의 개념을 이해하고 활용하는 데 큰 도움이 될 것입니다. 그럼 시작해볼까요?

문제 설명

문제 설명: 주어진 배열에서 각 원소에 대해 “오큰수”를 구하는 문제입니다. “오큰수”는 해당 원소의 오른쪽에 있는 원소 중에서 가장 첫 번째로 크기가 큰 원소를 의미합니다. 만약 그러한 원소가 존재하지 않으면 -1로 표시합니다.

입력: 첫 번째 줄에 자연수 N(1 ≤ N ≤ 1,000,000)이 주어지고, 두 번째 줄에는 N개의 자연수 A1, A2, …, AN (1 ≤ Ai ≤ 1,000,000)이 주어집니다.

출력: 각 원소에 대한 오큰수를 한 줄에 하나씩 출력합니다.

예제

입력:

5
2 1 3 4 5

출력:

3
3
4
5
-1

문제 해결 전략

문제를 해결하기 위한 가장 효과적인 방법 중 하나는 스택을 이용하는 것입니다. 스택은 LIFO(Last In, First Out) 구조의 자료구조로, 문제를 해결하는 데 유용하게 사용될 수 있습니다.

스택을 이용한 접근 방법

  1. 비어 있는 스택을 하나 생성합니다.
  2. 주어진 배열을 오른쪽부터 왼쪽으로 탐색하면서 각 원소에 대해 다음을 수행합니다:
    1. 현재 원소와 스택의 최상단 원소를 비교합니다.
    2. 스택의 최상단 원소가 현재 원소보다 크면, 그 원소가 오큰수입니다. 현재 원소에 오큰수를 저장합니다.
    3. 스택의 최상단 원소가 현재 원소보다 작거나 스택이 비어있으면 스택에서 원소를 pop합니다.
    4. 현재 원소를 스택에 추가합니다.
  3. 모든 원소를 탐색한 후, 오큰수가 저장된 결과 배열을 출력합니다.

코드 구현

이제 위의 알고리즘을 파이썬 코드로 구현해보겠습니다.


def find_next_greater_element(arr):
    n = len(arr)
    result = [-1] * n  # 오큰수를 저장할 결과 배열
    stack = []  # 스택 생성

    # 배열을 오른쪽부터 왼쪽으로 탐색
    for i in range(n - 1, -1, -1):
        # 스택의 최상단 원소와 비교
        while stack and stack[-1] <= arr[i]:
            stack.pop()  # 스택에서 pop
        
        # 스택이 비어있지 않다면 오큰수 기록
        if stack:
            result[i] = stack[-1]
        
        # 현재 원소를 스택에 추가
        stack.append(arr[i])

    return result

# 입력 처리
n = int(input())
arr = list(map(int, input().split()))

# 오큰수 구하기
result = find_next_greater_element(arr)

# 결과 출력
for r in result:
    print(r)

코드 설명

위 코드는 주어진 배열에서 각 원소의 오큰수를 찾는 함수입니다. 함수는 다음과 같은 과정을 따릅니다:

  1. 입력으로 받은 배열의 길이를 구하고, 결과를 저장할 배열을 초기화합니다.
  2. 주어진 배열을 오른쪽부터 왼쪽으로 순회합니다.
  3. 현재 원소와 비교하기 위해 스택의 최상단 원소를 확인합니다.
  4. 스택의 최상단 원소가 현재 원소보다 작거나 같으면 스택에서 pop합니다. 이렇게 해서 스택에는 항상 현재 원소보다 큰 값만 남게 됩니다.
  5. 스택이 비어 있지 않다면, 스택의 최상단 원소가 현재 원소의 오큰수가 됩니다. 이것을 결과 배열에 기록합니다.
  6. 현재 원소를 스택에 추가합니다.

복잡도 분석

시간 복잡도는 O(N)입니다. 각 원소는 스택에 한 번 추가되고 한 번 제거되므로, 전체적으로 N번의 연산이 필요합니다. 공간 복잡도는 O(N)으로 결과 배열과 스택을 저장하는 데 필요한 공간을 고려해야 합니다.

결과 예시

이제 코드의 일부 테스트 케이스를 확인해보겠습니다. 위에서 제시한 예제를 사용하여 오큰수를 구해보겠습니다.


# 입력 예제
# 5
# 2 1 3 4 5

print(find_next_greater_element([2, 1, 3, 4, 5]))  # 출력: [3, 3, 4, 5, -1]

결론

이번 시간에는 ‘오큰수 구하기’ 문제를 해결하는 과정을 살펴보았습니다. 스택을 사용하여 효율적으로 문제를 해결하고, 실제 코드를 통해 그 원리를 적용해 보았습니다. 이러한 방식의 문제 해결은 실제 코딩 테스트와 알고리즘 면접에서 매우 유용하니 꼭 연습해 보시기 바랍니다.

다음 시간에는 또 다른 알고리즘 문제를 가지고 찾아오겠습니다. 감사합니다!

파이썬 코딩테스트 강좌, 외판원의 순회 경로 짜기

안녕하세요, 여러분! 이번 강좌에서는 외판원의 순회 문제(TSP, Traveling Salesman Problem)를 해결하는 알고리즘을 구현하는 방법에 대해 자세히 알아보겠습니다. TSP는 주어진 도시를 최소 비용으로 모두 순회한 후, 다시 시작했던 도시에 돌아오는 경로를 찾는 문제입니다. 이 문제는 고전적인 조합 최적화 문제 중 하나로, 여러 가지 접근 방법이 있습니다.

1. 문제 설명

주어진 N개의 도시가 있고, 각 도시 간의 이동 비용이 주어졌을 때, 모든 도시를 한 번씩 방문하고 시작한 도시로 돌아오는 최소 비용 경로를 구하는 것이 목표입니다. 이 문제를 해결하기 위해 다음과 같은 입출력 형식을 사용합니다.

입력

  • 첫째 줄에는 도시의 개수 N (1 ≤ N ≤ 10)이 주어집니다.
  • 둘째 줄부터 N개의 줄에 걸쳐 N x N 크기의 인접 행렬이 주어지며, 인접 행렬의 i행 j열의 값은 도시 i에서 도시 j로 가는 비용을 나타냅니다. 만약 두 도시 간의 이동이 불가능하다면, 그 비용은 0입니다.

출력

  • 모든 도시를 방문하고 다시 시작 도시로 돌아오는 최소 비용을 출력합니다.

2. 문제 해결 과정

문제를 해결하기 위해 다양한 알고리즘이 존재하지만, 여기서는 DFS(Depth-First Search)메모이제이션(Memoization) 기법을 사용한 접근 방식을 설명하겠습니다. 다음은 문제를 해결하는 단계입니다.

Step 1: 문제 이해하기

TSP 문제를 해결하기 위해서는 모든 가능한 경로를 찾아야 합니다. 완전 탐색을 통해 모든 경우의 수를 따져 최소 비용을 계산할 수 있지만, 도시의 수가 증가할수록 경우의 수는 기하급수적으로 증가하므로 효율적인 방법이 필요합니다.

Step 2: 비트 마스크로 방문 도시 기록하기

비트 마스크를 사용하여 각 도시의 방문 여부를 기록할 수 있습니다. 예를 들어, 4개의 도시가 있을 경우, 각 도시를 비트로 표현하여 0b0000에서 0b1111까지의 경우를 만들 수 있습니다. 이 방법을 통해 각 도시를 방문했는지 여부를 간편하게 확인할 수 있습니다.

Step 3: DFS 및 메모이제이션 구현하기

DFS를 이용하여 모든 경로를 탐색하면서 비용을 계산하되, 이미 계산한 경로는 저장하여 중복 계산을 피하는 메모이즈 기법을 활용합니다. 아래는 이를 구현한 파이썬 코드입니다:

from sys import maxsize

def tsp(curr_city, visited, n, cost_matrix, memo):
    if visited == (1 << n) - 1:  # All cities visited
        return cost_matrix[curr_city][0] or maxsize  # Return to starting city or maxsize if not possible

    if memo[curr_city][visited] != -1:
        return memo[curr_city][visited]  # Return already computed cost

    min_cost = maxsize
    for city in range(n):
        if visited & (1 << city) == 0:  # City not visited
            new_cost = cost_matrix[curr_city][city] + tsp(city, visited | (1 << city), n, cost_matrix, memo)
            min_cost = min(min_cost, new_cost)

    memo[curr_city][visited] = min_cost
    return min_cost

def solve_tsp(n, cost_matrix):
    memo = [[-1] * (1 << n) for _ in range(n)]
    return tsp(0, 1, n, cost_matrix, memo)  # Start from city 0 with only city 0 visited

# Example usage
if __name__ == "__main__":
    n = 4  # Number of cities
    cost_matrix = [
        [0, 10, 15, 20],
        [10, 0, 35, 25],
        [15, 35, 0, 30],
        [20, 25, 30, 0]
    ]
    result = solve_tsp(n, cost_matrix)
    print(f"Minimum cost: {result}")
    

Step 4: 코드 설명

위 코드는 다음과 같은 주요 함수로 구성됩니다:

  • tsp(curr_city, visited, n, cost_matrix, memo): 현재 도시, 방문한 도시의 비트마스크, 도시의 수, 비용 행렬, 그리고 메모이제이션을 위한 리스트를 매개변수로 받아, 최소 비용을 계산합니다.
  • solve_tsp(n, cost_matrix): 메모이제이션 리스트를 초기화하고 TSP 기능을 수행합니다.

Step 5: 시간 복잡도 분석

위 알고리즘의 시간 복잡도는 O(n^2 * 2^n)입니다. 여기서 n은 도시의 개수, 2^n는 모든 비트마스크 조합의 수입니다. 따라서 도시의 개수가 많아질수록 계산량이 급격히 증가할 수 있으므로, 실제 문제에서는 도시 개수를 10개 이하로 제한합니다.

3. 결론

이번 강좌에서는 외판원의 순회 문제(TSP)에 대한 개념 및 알고리즘 구현 방법에 대해 알아보았습니다. TSP 문제는 다양한 문제 해결 기법을 활용할 수 있는 좋은 예제이며, 깊은 이해를 통해 다른 알고리즘 문제에도 적용할 수 있는 사고를 기르는데 도움이 됩니다.

코딩 테스트에서 TSP와 관련된 문제를 만나게 될 경우, 위와 같은 방식으로 접근해 보시면 좋을 것입니다. 이제 여러분이 이 문제를 독립적으로 해결할 수 있도록 더욱 연습하시기 바랍니다. 감사합니다!