파이썬 코딩테스트 강좌, 친구 관계 파악하기

문제 설명

당신은 소셜 네트워크의 사용자 관계를 파악하는 프로그램을 개발해야 합니다.
주어진 사용자들의 친구 관계를 토대로 다음 두 가지 요청에 대한 답을 제공하는 프로그램을 작성해야 합니다.

  • 주어진 사용자 A의 친구 목록을 출력하라.
  • 주어진 사용자 A와 B가 친구인지 여부를 확인하라.

입력 형식

첫 번째 줄에 사용자 수 N과 친구 관계의 수 M이 주어집니다. (1 ≤ N ≤ 100,000, 0 ≤ M ≤ 1,000,000)

다음 M줄에 걸쳐 각 줄마다 두 개의 정수 A와 B가 주어지며 이는 A가 B의 친구라는 것을 의미합니다.

출력 형식

첫 번째 줄에 사용자 A의 친구 목록을 오름차순으로 출력하라.
두 번째 줄에 ‘YES’ 또는 ‘NO’로 사용자 A와 B의 친구 여부를 출력하라.

예제

        입력 예시:
        5 4
        1 2
        1 3
        2 4
        3 5
        
        1
        2

        출력 예시:
        2 3
        NO
    

접근 방식

이 문제를 풀기 위해서는 친구 관계를 효율적으로 저장하고 조회해야 합니다.
그래프 이론을 활용하여 인접 리스트 또는 집합(set) 자료구조를 사용할 수 있습니다.

먼저 사용자들과 친구 관계 정보를 저장하기 위해 빈 리스트를 초기화합니다.
그 후, 각 친구 관계를 입력받아 해당 정보를 저장합니다.
친구 목록을 정렬하여 출력하고, 친구 관계 확인 요청에 대한 응답도 제공합니다.

소스 코드

        def manage_friendship(n, m, friendships, a, b):
            friends = {i: set() for i in range(1, n + 1)}
            
            for x, y in friendships:
                friends[x].add(y)
                friends[y].add(x)
            
            friend_list = sorted(friends[a])
            is_friend = "YES" if b in friends[a] else "NO"
            
            return friend_list, is_friend
        
        # 입력 처리
        n, m = map(int, input().split())
        friendships = [tuple(map(int, input().split())) for _ in range(m)]
        a = int(input())
        b = int(input())

        # 친구 관계 관리
        friend_list, is_friend = manage_friendship(n, m, friendships, a, b)

        # 결과 출력
        print(" ".join(map(str, friend_list)))
        print(is_friend)
    

결론

이 알고리즘 문제는 데이터 구조와 알고리즘의 기본적인 이해를 바탕으로 친구 관계를 효율적으로 관리하고
조회하는 방법을 요구합니다.
실제 소프트웨어 개발에서도 이러한 형태의 데이터 관계를 처리하는 것이 주문제작 또는 소셜 네트워크와 같은
플랫폼 개발에 자주 등장하므로, 이러한 문제를 해결하는 방법을 익히는 것은 매우 유용합니다.

부록

친구 관계와 같은 많은 그래프 문제는 DFS(깊이 우선 탐색) 또는 BFS(너비 우선 탐색)와 같은 탐색 알고리즘을 통해
더 확장된 문제로 발전할 수 있습니다.
이 문제를 바탕으로 그러한 심화 문제에도 도전해 보길 바랍니다.

파이썬 코딩테스트 강좌, 최장 공통 부분 수열 찾기

문제 설명

주어진 두 개의 문자열이 있을 때, 이 두 문자열의 최장 공통 부분 수열(Longest Common Subsequence, LCS)을 구하는 문제입니다. 공통 부분 수열이란 두 문자열에서 순서를 유지하면서 나타나는 공통 문자들을 나타냅니다. 길이가 최대인 경우를 찾는 것이 목표입니다.

예제

입력

문자열 A: ABCBDAB
문자열 B: BDCAB

출력

최장 공통 부분 수열: BDAB

문제 접근 방법

LCS 문제를 해결하기 위해서는 동적 프로그래밍(Dynamic Programming)을 사용할 수 있습니다. 동적 프로그래밍은 이미 계산한 결과를 저장해 두고, 이를 재사용하는 방식으로 문제를 해결하는 기법입니다. LCS를 구하기 위해서는 두 문자열의 각 문자 비교 결과를 통해 부분 수열의 길이를 저장해 나갑니다.

동적 프로그래밍을 이용한 LCS 구하기

1단계: DP 테이블 초기화

두 문자열의 길이를 각각 m, n이라 할 때, 크기가 (m + 1) x (n + 1)인 2차원 DP 배열을 생성하고 초기화합니다. DP 배열의 각 원소는 두 문자열에서의 공통 부분 수열의 길이를 저장합니다.

2단계: DP 테이블 작성

두 문자열의 각 문자를 비교하면서 DP 테이블을 채워나갑니다. 만약 두 문자가 같다면, 그 문자를 포함한 LCS 길이는 DP[i-1][j-1] + 1입니다. 만약 다르다면, DP[i][j] = max(DP[i-1][j], DP[i][j-1])을 사용하여 현재까지 찾은 LCS 길이를 기록합니다.

3단계: LCS 길이와 문자열 추출

DP 테이블을 모두 채운 후, DP[m][n]을 확인하여 최장 공통 부분 수열의 길이를 얻고, 이를 통해 실제 공통 부분 수열을 추출합니다. 추출 과정은 테이블을 역으로 탐색하여 공통된 문자를 확인하는 방식으로 진행됩니다.

알고리즘 코드

    
def lcs(A, B):
    # 1단계: DP 배열 초기화
    m = len(A)
    n = len(B)
    dp = [[0] * (n + 1) for _ in range(m + 1)]

    # 2단계: DP 배열 작성
    for i in range(1, m + 1):
        for j in range(1, n + 1):
            if A[i - 1] == B[j - 1]:
                dp[i][j] = dp[i - 1][j - 1] + 1
            else:
                dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

    # 3단계: LCS 길이 얻기
    lcs_length = dp[m][n]

    # LCS 문자열 추출
    lcs_str = []
    i, j = m, n
    while i > 0 and j > 0:
        if A[i - 1] == B[j - 1]:
            lcs_str.append(A[i - 1])
            i -= 1
            j -= 1
        elif dp[i - 1][j] >= dp[i][j - 1]:
            i -= 1
        else:
            j -= 1

    lcs_str.reverse()  # 꺼낸 문자열을 뒤집어서 원래 순서로 복원
    return lcs_length, ''.join(lcs_str)

# 테스트 코드
A = "ABCBDAB"
B = "BDCAB"
length, lcs_string = lcs(A, B)
print("LCS 길이:", length)
print("LCS 문자열:", lcs_string)
    
    

복잡도 분석

이 알고리즘의 시간 복잡도는 O(m * n)입니다. 각 문자를 비교해야 하므로, 두 문자열의 길이에 따라 곱해진 결과가 나옵니다. 공간 복잡도 또한 O(m * n)이며, 생성된 DP 테이블의 크기에 의해 결정됩니다. 하지만 DP 테이블의 크기를 줄일 방법도 있습니다. 예를 들어, 이전 행의 데이터만 필요하기 때문에, 2개의 1차원 배열로 구현할 수도 있습니다.

최적화된 코드 예시

    
def optimized_lcs(A, B):
    m = len(A)
    n = len(B)
    dp = [0] * (n + 1)

    for i in range(1, m + 1):
        current = 0  # 현재 위치의 값
        for j in range(1, n + 1):
            temp = dp[j]
            if A[i - 1] == B[j - 1]:
                dp[j] = current + 1
            else:
                dp[j] = max(dp[j], dp[j - 1])
            current = temp  # 이전 행의 값을 저장

    return dp[n]

# 최적화된 테스트
length = optimized_lcs(A, B)
print("LCS 길이:", length)
    
    

결론

이번 강좌에서는 최장 공통 부분 수열(Longest Common Subsequence) 문제를 다루었습니다. 이 문제는 문자열 처리와 동적 프로그래밍 개념을 쉽게 활용할 수 있는 좋은 예제입니다. LCS 문제는 다양한 분야, 특히 유전자 분석과 동일한 서열 찾기 문제에서 많이 사용되므로, 알고리즘의 이해와 구현은 코딩 테스트나 실무에서 매우 유용합니다.

참고 자료

파이썬 코딩테스트 강좌, 최솟값을 만드는 괄호 배치 찾기

이번 강좌에서는 “최솟값을 만드는 괄호 배치 찾기”라는 주제로 알고리즘 문제를 풀어보겠습니다.
이 문제는 주어진 숫자와 연산자에 대해 괄호를 적절히 배치하여 계산 결과를 최소화하는 방법을 찾는 것입니다.

문제 정의

주어진 숫자와 연산자 문자열에서, 괄호를 사용하여 가능한 모든 경우의 수를 고려하여
최솟값을 구하시오. 예를 들어, “3+5-2+6*3″와 같은 입력이 주어진다면,
괄호를 적절히 사용하여 계산 결과를 최소화하는 괄호 위치를 찾아야 합니다.

문제 예시

        입력: "3+5-2+6*3"
        출력: 9 (예: (3+5-2)+(6*3) => 9이 최소값)
    

문제 분석

이 문제는 괄호 배치에 따라 연산의 순서가 달라지는 성질을 가지고 있습니다.
따라서 동적 프로그래밍(dynamic programming) 기법을 활용하여 해결할 수 있습니다.
문제를 해결하기 위해 몇 가지 단계를 고려할 수 있습니다.

1단계: 입력 형식 이해

문제에서 볼 수 있듯이, 괄호를 넣어야 할 위치는 연산자 뒤에만 가능합니다.
따라서 입력을 숫자와 연산자로 분리하는 것이 필요합니다.

2단계: 괄호 배치의 가능한 조합 찾기

주어진 숫자와 연산자에서 가능한 모든 조합을 찾아야 합니다.
이는 재귀적 방법을 통해 해결할 수 있으며, 각 조합에 대해
구해진 결과값을 비교하여 최솟값을 저장합니다.

3단계: 계산 함수 구현

각 조합에 대해 실제로 계산을 수행하는 함수를 구현해야 합니다.
이때 연산자에 따라 다른 연산을 수행해야 하기 때문에 주의해야 합니다.

코드 작성

이하의 코드는 최솟값을 만드는 괄호 배치 찾기를 위한 최종 구현 예시입니다.

        
def calculate(expression):
    return eval(expression)

def min_parentheses(arr, ops):
    min_value = float('inf')
    
    def find_min(l, r):
        nonlocal min_value
        if l == r:
            return arr[l]
        
        for i in range(l, r):
            left = find_min(l, i)
            right = find_min(i + 1, r)
            expr = f"({left}{ops[i]}{right})"
            min_value = min(min_value, calculate(expr))
        
        return min_value
    
    find_min(0, len(arr) - 1)
    return min_value

def min_parentheses_solution(s):
    arr = list(map(int, s.replace('+', ' ').replace('-', ' ').replace('*', ' ').split()))
    ops = [char for char in s if not char.isdigit()]
    return min_parentheses(arr, ops)

# 예시 실행
print(min_parentheses_solution("3+5-2+6*3"))
        
    

코드 설명

위 코드에서 사용된 함수들을 하나씩 살펴보겠습니다.

1. calculate 함수

주어진 수식 문자열을 평가하여 그 결과를 반환합니다.
eval 함수를 사용하여 문자열을 수식으로 계산합니다.
하지만, 실제로는 eval 사용을 피하는 것이 좋으므로,
나중에 안전한 방식으로 수학 연산을 구현할 수 있도록 변경할 수 있습니다.

2. min_parentheses 함수

동적 프로그래밍 부분을 구현한 함수로, 인자로 받은 배열을
재귀적으로 나누어 최솟값을 계산합니다.
각 구간에 대해 가능한 모든 연산을 수행하여
최소 결과값을 업데이트합니다.

3. min_parentheses_solution 함수

입력 문자열을 숫자와 연산자로 분리한 뒤,
min_parentheses 함수를 호출하여 최솟값을 찾습니다.

결과 확인

위 코드를 통해 “3+5-2+6*3″의 경우 최솟값이 9임을 확인할 수 있습니다.
이 예제는 기본적인 알고리즘의 구조를 보여주며,
사용자 정의 데이터 구조 또는 문법 등을 연습하기에 좋은 문제입니다.

결론

이번 강좌를 통해 다양한 괄호 배치의 경우를 다루며 문제를 해결하는 방법을 배웠습니다.
이러한 문제는 코딩 테스트에서 자주 등장하므로,
자신만의 알고리즘을 확립하고 이에 대한 이해를 깊이는 것이 중요합니다.
더 나아가, 문제를 해결하기 위한 다양한 접근 방식을 실험해 보길 권장합니다.

알고리즘 고민 정리

마지막으로, 최솟값을 만들기 위해 괄호를 배치하는 문제는 경우의 수를 탐색해야 하므로,
브루트포스(brute force) 알고리즘과 동적 프로그래밍을 조합하여 접근하는 것이 효과적입니다.
문제를 해결하는 과정에서 생기는 난관을 헤쳐 나가며,
알고리즘적 사고력을 기르는 데에 많은 도움이 됩니다.

추가 연습 문제

이와 유사한 문제를 풀어보며 알고리즘을 응용해보세요.
예시: “1+2*3-4/2+5*6″와 같은 입력의 최솟값을 구해보세요.

파이썬 코딩테스트 강좌, 최솟값 찾기 2

안녕하세요, 코딩 테스트를 준비하는 여러분! 오늘은 ‘최솟값 찾기’에 대한 두 번째 강좌를 진행하겠습니다. 이 강좌에서는 주어진 배열에서 특정 조건을 만족하는 최솟값을 찾는 알고리즘 문제에 대해 다뤄보겠습니다. 알고리즘 문제를 푸는 과정을 단계별로 설명할 것이니, 주의 깊게 읽어주세요.

문제 설명

주어진 정수 배열 nums와 하나의 정수 target가 있습니다. 배열에서 target보다 작은 수 중 최솟값을 찾아 반환하는 함수를 작성하세요. 만약 그런 수가 없다면, -1을 반환해야 합니다.

예제 입력 및 출력

  • 입력: nums = [1, 3, 5, 7, 9], target = 6
  • 출력: 5
  • 입력: nums = [1, 2, 3], target = 1
  • 출력: -1

문제 접근 방식

이 문제를 해결하기 위해서는 다음과 같은 단계를 거쳐야 합니다:

  1. 배열을 순회하며 target보다 작은 숫자를 찾는다.
  2. 찾은 숫자 중 최솟값을 저장한다.
  3. 조건을 만족하는 숫자가 없으면 -1을 반환하고, 있으면 최솟값을 반환한다.

알고리즘 분석

위 문제를 해결하기 위한 시간 복잡도는 O(n)입니다. 이는 배열을 한 번만 순회하기 때문입니다. 공간 복잡도는 O(1)로 추가적인 공간이 필요하지 않습니다.

코드 구현

이제 위의 알고리즘을 바탕으로 파이썬 코드를 작성해 보겠습니다.

def find_min_less_than_target(nums, target):
    min_value = float('inf')  # 무한대로 초기화
    found = False  # 조건을 만족하는 숫자가 찾았는지 확인하는 변수

    for num in nums:
        if num < target:
            found = True  # target보다 작은 수를 찾았음
            min_value = min(min_value, num)  # 최솟값 업데이트

    return min_value if found else -1  # 조건 만족 여부에 따라 값 반환

테스트 케이스

코드를 작성한 후에는 몇 가지 테스트 케이스를 통해 올바르게 작동하는지 확인해야 합니다.

assert find_min_less_than_target([1, 3, 5, 7, 9], 6) == 5
assert find_min_less_than_target([1, 2, 3], 1) == -1
assert find_min_less_than_target([10, 15, 20, 25, 30], 20) == 15
assert find_min_less_than_target([1, 2, 3, 0, -1], 2) == 1
assert find_min_less_than_target([], 5) == -1

결론

이번 강좌에서는 배열에서 특정 조건을 만족하는 최솟값을 찾는 문제를 해결했습니다. 알고리즘 구현 방식과 성능 분석을 통해 문제를 명확하게 이해하고 해결하는 방법을 배웠습니다. 앞으로도 다양한 알고리즘 문제에 도전하며 실력을 쌓아가길 바랍니다. 다음 시간에는 다른 주제로 돌아오겠습니다. 감사합니다!

파이썬 코딩테스트 강좌, 최솟값 찾기 1

코딩 테스트는 최근 많은 기업들의 채용 과정에서 중요한 단계로 여겨지고 있습니다. 오늘은 알고리즘 문제 중 하나인 “최솟값 찾기”에 대해 알아보겠습니다.
이 문제는 배열에서 최솟값을 찾는 간단한 문제처럼 보일 수 있지만, 다양한 변형과 최적화 기법을 사용하여 실제로 매우 유용하게 활용될 수 있습니다.
이 강좌를 통해 이론적 배경과 함께 코드 구현 방법을 자세히 살펴보겠습니다.

문제 설명

주어진 정수 배열 arr가 있을 때, 이 배열의 최솟값을 찾아 반환하는 함수를 작성하시오.
배열의 크기는 1 ≤ len(arr) ≤ 10^6, 배열의 각 원소는 -10^9 ≤ arr[i] ≤ 10^9 범위 내의 정수입니다.

입력

arr = [3, 1, 4, 1, 5, 9, 2, 6, 5]

출력

1

문제풀이 과정

1. 문제 이해하기

주어진 문제는 배열에서 최솟값을 찾아 반환하는 것입니다. 배열의 원소 개수가 1백만 개까지 가능하므로,
효율적인 알고리즘이 필요합니다. 최솟값을 찾기 위해 여러 접근 방법이 있을 수 있지만,
가장 기본적인 방법부터 시작해 보겠습니다.

2. 알고리즘 설계

최솟값을 찾는 가장 간단한 방법은 배열을 순회하며 각 원소와 현재 최솟값을 비교하는 것입니다.
이 방법은 시간 복잡도가 O(n)이며, 여기서 n은 배열의 요소 개수입니다.
이 방법의 장점은 구현이 매우 간단하고 직관적이라는 것입니다.
그러나 최솟값을 찾는 방법은 다양하므로, 다른 접근 방법도 고려할 수 있습니다.

3. 코드 구현

이제 알고리즘을 Python 코드로 구현해 보겠습니다.

def find_min(arr):
    # 배열이 비어있는 경우 예외 처리
    if not arr:
        return None

    # 첫 번째 요소를 최솟값으로 초기화
    min_value = arr[0]

    # 배열을 순회하며 최솟값 찾기
    for num in arr:
        if num < min_value:
            min_value = num

    return min_value

# 예제 사용
arr = [3, 1, 4, 1, 5, 9, 2, 6, 5]
result = find_min(arr)
print(f"최솟값은: {result}")

4. 코드 설명

위 코드에서 find_min 함수는 배열 arr를 입력으로 받아 최솟값을 찾습니다.
먼저 배열이 비어있는 경우를 대비하여 None을 반환하도록 예외 처리를 합니다.
그 다음, 배열의 첫 번째 요소를 최솟값으로 초기화하고, 배열의 모든 요소를 순회하며 현재 최솟값과 비교합니다.
만약 현재 요소가 최솟값보다 작다면, 최솟값을 업데이트합니다.
최종적으로 최솟값을 반환합니다.

5. 시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(n)입니다.
이는 배열의 모든 요소를 한 번씩 순회해야 하기 때문에 나타나는 복잡도입니다.
적어도 n개의 원소가 존재하는 배열에서는 최솟값을 찾기 위해 모든 원소를 확인해야 하므로,
이보다 더 좋은 시간 복잡도를 갖는 방법은 존재하지 않습니다.

리스트의 최솟값을 찾는 다른 방법들

1. 내장 함수 사용

Python에서는 내장 함수 min()를 사용하여 간단하게 최솟값을 찾을 수 있습니다.
이 경우에도 시간 복잡도는 여전히 O(n)입니다.

result = min(arr)
print(f"최솟값은: {result}")

2. 재귀적 방법

재귀를 사용하여 최솟값을 찾는 방법도 있습니다. 이 방법은 코드가 더 복잡하지만,
동일한 시간 복잡도를 유지합니다. 아래는 간단한 재귀적 접근 방식입니다.

def find_min_recursive(arr, low, high):
    # 배열의 한 요소인 경우
    if low == high:
        return arr[low]

    # 배열의 중간 인덱스 계산
    mid = (low + high) // 2

    # 왼쪽 반과 오른쪽 반에서 각각 최솟값 찾기
    left_min = find_min_recursive(arr, low, mid)
    right_min = find_min_recursive(arr, mid + 1, high)

    return min(left_min, right_min)

# 재귀를 사용한 최솟값 찾기
result = find_min_recursive(arr, 0, len(arr) - 1)
print(f"최솟값은: {result}")

3. 정렬 후 첫 번째 요소 사용

배열을 정렬한 후 최솟값을 찾는 방법도 있습니다. 이 방법은 시간 복잡도가 O(n log n)이며,
따라서 일반적인 최솟값 찾기 방법보다 비효율적입니다. 그러나 정렬이 필요한 다른 작업과 연관되었다면 유용할 수 있습니다.

sorted_arr = sorted(arr)
min_value = sorted_arr[0]
print(f"최솟값은: {min_value}")

문제의 변형

최솟값 찾기 문제는 다양한 변형을 가질 수 있습니다. 예를 들어, 다음과 같은 변형 문제가 있을 수 있습니다.

1. 최솟값의 인덱스 찾기

최솟값뿐만 아니라 최솟값의 인덱스를 반환하도록 문제를 변형할 수 있습니다. 이 경우,
최솟값을 업데이트할 때 해당 인덱스를 기록하면 됩니다.

def find_min_index(arr):
    if not arr:
        return None, None

    min_value = arr[0]
    min_index = 0

    for i in range(len(arr)):
        if arr[i] < min_value:
            min_value = arr[i]
            min_index = i

    return min_value, min_index

# 예제 사용
min_value, min_index = find_min_index(arr)
print(f"최솟값은: {min_value}, 인덱스는: {min_index}")

2. 여러 개의 최솟값 반환

배열에 여러 개의 최솟값이 존재하는 경우, 이들을 모두 반환하는 방법을 고려할 수 있습니다.
이때는 최솟값이 결정된 후 해당 최솟값을 가진 모든 인덱스를 저장해 반환하면 됩니다.

def find_all_min(arr):
    if not arr:
        return [], None

    min_value = arr[0]
    min_indices = []

    for i in range(len(arr)):
        if arr[i] < min_value:
            min_value = arr[i]
            min_indices = [i]  # 최솟값이 바뀌면 새로운 인덱스 기록
        elif arr[i] == min_value:
            min_indices.append(i)  # 동일한 최솟값 추가

    return min_indices, min_value

# 예제 사용
min_indices, min_value = find_all_min(arr)
print(f"최솟값은: {min_value}, 인덱스는: {min_indices}")

결론

오늘은 “최솟값 찾기” 문제를 통해 배열 내 최솟값을 찾는 다양한 방법에 대해 알아보았습니다.
기본적인 순회 방법뿐만 아니라 내장 함수, 재귀적 접근, 정렬을 통한 방법과 같은 여러 접근 방식을 다뤘습니다.
또한 문제의 변형을 통해 더욱 복잡한 상황을 해결할 수 있는 방법도 제시하였습니다.
이러한 문제는 코딩 테스트에서 자주 출제되므로, 다양한 접근 방식을 이해하고 연습하는 것이 중요합니다.

연습문제

아래의 연습문제를 풀어보세요.

  • 주어진 배열에서 중복된 요소를 제거한 후 최솟값을 찾는 함수를 작성하시오.
  • 2차원 배열에서 최솟값을 찾는 함수를 작성하시오.
  • 미정렬된 배열에서 k번째 최솟값을 찾는 함수를 작성하시오.