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

안녕하세요, 여러분! 이번 포스트에서는 코딩테스트에서 자주 등장하는 친구 관계 파악하기 문제를 다뤄보겠습니다. 소개할 문제는 그래프 탐색 문제이며, 친구 관계를 분석하여 특정 조건을 만족하는 친구의 수를 계산하는 과제를 제시합니다.

문제 설명

여러분은 N명의 친구가 있는 파티를 주최했습니다. 각 친구는 서로에게 친구 관계로 연결되어 있습니다. 친구 관계는 양방향이며, 각 친구A와 친구B는 친구들에서만 서로의 친구를 확인할 수 있습니다. 즉, 친구 A의 친구는 A와 직접적으로 연결된 친구뿐입니다. 당신의 임무는 특정 친구의 친구들 중에서 ‘2단계 친구’를 얼마나 찾을 수 있는지 세는 것입니다.

문제 정의

    입력:
    - N (1 ≤ N ≤ 100) : 친구의 수
    - M (1 ≤ M ≤ 10^4) : 친구 관계의 쌍 수
    - M개의 쌍 (A, B): 두 친구 A와 B.

    출력:
    - X: 특정 친구의 2단계 친구의 수.

    특정 친구를 K라고 할 때, K의 2단계 친구를 세는 것입니다.
    

입력 예시

    6 5
    1 2
    2 3
    1 4
    4 5
    5 6
    1
    

출력 예시

    2
    

문제 접근법

이 문제를 해결하기 위해서는 그래프 자료구조를 사용하여 친구 관계를 표현해야 합니다. 이를 위해 인접 리스트(Adjacency List)를 생성하겠습니다. 이후 너비 우선 탐색(BFS) 또는 깊이 우선 탐색(DFS)을 통해 2단계 친구를 찾아야 합니다. 그래프의 각 친구는 정점, 친구 관계는 간선으로 볼 수 있습니다.

단계별 접근법

  1. 입력값을 읽어들이고 친구 관계의 연결 정보를 바탕으로 인접 리스트를 생성합니다.
  2. 특정 친구 K를 기준으로 BFS 또는 DFS를 사용하여 친구의 친구 관계를 탐색합니다.
  3. 탐색 과정 중 K와 직접 연결된 친구는 제외하고, 친구의 친구는 집합(Set)을 사용하여 중복을 방지합니다.
  4. 최종적으로 집합의 크기를 출력합니다.

구현 코드 (Java)

    import java.util.*;

    public class FriendRelations {
        static List[] graph;
        static Set friendsOfFriends;

        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);
            int N = scanner.nextInt();
            int M = scanner.nextInt();
            graph = new ArrayList[N + 1];
            for (int i = 1; i <= N; i++) {
                graph[i] = new ArrayList<>();
            }

            for (int i = 0; i < M; i++) {
                int A = scanner.nextInt();
                int B = scanner.nextInt();
                graph[A].add(B);
                graph[B].add(A);
            }

            int K = scanner.nextInt();
            friendsOfFriends = new HashSet<>();

            for (int friend : graph[K]) {
                for (int f2 : graph[friend]) {
                    if (f2 != K && !graph[K].contains(f2)) {
                        friendsOfFriends.add(f2);
                    }
                }
            }
            System.out.println(friendsOfFriends.size());
            scanner.close();
        }
    }
    

코드 설명

코드의 첫 부분에서 친구 관계를 저장할 그래프를 정의하고 입력값을 통해 친구들의 관계를 인접 리스트 형식으로 초기화합니다. 이후 지정한 친구 K를 기준으로 그 친구의 친구들을 탐색하고, 이를 집합에 추가하여 중복을 제거합니다. 마지막으로 집합의 크기를 통해 K의 2단계 친구 수를 구합니다.

코드 실행

이제 코드를 실행하여 결과를 확인해 봅시다. 위의 입력 예시를 사용할 경우 코드의 결과는 ‘2’가 될 것입니다. 이는 특정 친구 K가 총 2명의 2단계 친구를 가진다는 것을 의미합니다.

마무리

이 문제는 그래프 이론의 기초적인 개념을 활용하는 좋은 연습이 됩니다. 친구 관계와 같은 이론은 많은 코딩 인터뷰에서 자주 등장하며, 따라서 이러한 문제를 풀면서 그래프 탐색과 관련된 지식을 더욱 강화할 수 있습니다.

다음 포스트에서도 유익한 알고리즘 문제와 그 풀이 과정을 다루도록 하겠습니다. 궁금한 사항이 있다면 댓글로 남겨주세요!

자바 코딩테스트 강좌, 최장 공통 부분 수열 찾기

본 강좌에서는 코딩 테스트에서 자주 출제되는 문제인 “최장 공통 부분 수열(Longest Common Subsequence, LCS)”에 대해 알아보겠습니다. LCS 문제는 두 개의 시퀀스가 주어졌을 때, 두 시퀀스에서 각각의 요소의 상대적 순서를 유지하면서 만들 수 있는 가장 긴 공통 부분 수열을 찾는 문제입니다.

문제 설명

두 개의 문자열 str1str2가 주어질 때, 이 두 문자열의 최장 공통 부분 수열의 길이를 구하세요.

입력

  • 문자열 str1 : “AGGTAB”
  • 문자열 str2 : “GXTXAYB”

출력

최장 공통 부분 수열의 길이 : 4

문제 풀이 과정

이 문제를 해결하기 위해 동적 프로그래밍(Dynamic Programming) 접근 방식을 사용할 것입니다. 동적 프로그래밍은 문제를 작은 부분 문제로 나누어 해결하고, 그 결과를 저장하여 효율적으로 문제를 해결하는 방법입니다.

1단계: 이차원 배열 초기화

문자열 str1str2의 길이를 기반으로 이차원 배열 dp를 생성합니다. 이 배열의 크기는 (m+1) x (n+1)이며,

  • m : str1의 길이
  • n : str2의 길이

배열의 각 요소는 초기값으로 0으로 설정합니다.

2단계: 이차원 배열 채우기

이제 이차원 배열 dp를 채워 나갑니다. 문자열의 각 글자를 비교하면서 진행합니다.

  1. 반복문을 통해 각 문자를 비교합니다.
  2. 문자가 같으면 dp[i][j] = dp[i-1][j-1] + 1로 설정합니다.
  3. 문자가 다르면 dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1])로 설정합니다.

최종적으로 dp[m][n]의 값이 최장 공통 부분 수열의 길이가 됩니다.

3단계: 코드 구현

이제 이 과정을 자바 코드로 구현해 보겠습니다.


public class LongestCommonSubsequence {
    public static int lcs(String str1, String str2) {
        int m = str1.length();
        int n = str2.length();
        int[][] dp = new int[m + 1][n + 1];

        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        return dp[m][n];
    }

    public static void main(String[] args) {
        String str1 = "AGGTAB";
        String str2 = "GXTXAYB";
        System.out.println("최장 공통 부분 수열의 길이: " + lcs(str1, str2));
    }
}
    

코드 분석

위 코드는 다음과 같은 방식으로 작동합니다.

  • 두 개의 문자열을 입력받고, 그 길이에 따라 dp 배열을 생성합니다.
  • 이중 반복문을 사용하여 문자열을 비교하고, 동적 프로그래밍 방식으로 값을 업데이트합니다.
  • 출력으로는 최장 공통 부분 수열의 길이를 제공하고 있습니다.

실행 결과

입력: str1 = "AGGTAB", str2 = "GXTXAYB"

출력: 최장 공통 부분 수열의 길이: 4

결론

이번 강좌에서는 최장 공통 부분 수열을 찾는 방법에 대해 알아보았습니다. 알고리즘 문제를 해결하기 위해 동적 프로그래밍 기법을 활용하는 것이 얼마나 효과적일 수 있는지를 보여주었습니다. 이 문제는 다양한 분야에서 활용될 수 있으며, 알고리즘 문제를 풀 때 기본적으로 알아두어야 할 내용입니다.

더 나아가 본 강좌에서 공유된 코드를 바탕으로 다른 문자열 조합에 대해서도 실험해보며, 이론을 더욱 심화시킬 수 있습니다. 알고리즘 문제 풀이 능력을 키우는 데 큰 도움이 되기를 바랍니다.

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

문제 정의

주어진 숫자들과 괄호를 사용하여 최솟값을 만드는 방법을 찾아야 합니다. 예를 들어, 숫자와 연산자들이 포함된 문자열이 주어질 때 괄호를 적절히 배치하여 가능한 최솟값을 계산하는 문제입니다. 이 문제는 괄호의 위치에 따라 다르게 계산될 수 있는 수식을 최적화하는 것을 목표로 합니다.

문제 예시

입력: "1+2*3-4/2"

출력: -1 (예시)

문제 접근 방식

최솟값을 만들기 위해서는 모든 가능한 괄호 배치를 시도해 보고 결과를 비교해야 합니다. 이때 사용할 수 있는 알고리즘은 “분할 정복”입니다. 수식을 부분적으로 나누고 각각의 결과를 비교하여 최종 결과를 도출합니다.

1단계: 수식 파싱

우선 입력된 문자열을 파싱하여 숫자와 연산자를 분리합니다. 이후 각 연산에 대해 우선순위를 정하고, 그에 따라 결과를 계산합니다.

2단계: 재귀적 계산

재귀적으로 수식을 나누어 각 부분의 결과를 계산합니다. 각 연산자는 자식 노드로 나뉘며, 이를 통해 모든 조합의 값을 계산할 수 있습니다.

3단계: 최솟값 비교

각 경우의 수를 모두 계산한 후, 이를 비교하여 최솟값을 찾아냅니다. 이 과정에서 결과를 저장하여 중복 계산을 방지할 수 있습니다.

파이썬 코딩 예시


def minValue(expression):
    import re

    # 수식을 숫자와 연산자로 분리
    tokens = re.findall(r'\d+|[+*/-]', expression)
    
    # 모든 조합 검색 함수
    def dfs(tokens):
        if len(tokens) == 1:
            return int(tokens[0])
        
        min_result = float('inf')
        
        for i in range(1, len(tokens), 2):
            operator = tokens[i]
            left = tokens[:i]
            right = tokens[i + 1:]

            for left_result in dfs(left):
                for right_result in dfs(right):
                    if operator == '+':
                        result = left_result + right_result
                    elif operator == '-':
                        result = left_result - right_result
                    elif operator == '*':
                        result = left_result * right_result
                    elif operator == '/':
                        result = left_result // right_result
                    
                    min_result = min(min_result, result)
        
        return min_result
    
    return dfs(tokens)

# 사용 예시
print(minValue("1+2*3-4/2"))

결론

이 문제는 괄호 배치에 따라 결과가 어떻게 달라지는지를 이해하고, 재귀적 사고를 통해 최솟값을 찾는 연습이 됩니다. 여러 조건을 고려하여 최적화를 시도하는 과정은 코딩테스트에서 자주 사용되는 접근 방식입니다. 실제로 코드를 구현하면서 다양한 케이스를 테스트해 보세요.

코딩테스트 준비를 위한 팁

  • 문제를 이해하고 문제의 요구사항을 정확히 파악합니다.
  • 예시를 통해 다양한 케이스를 만들어 봅니다.
  • 재귀적인 접근 방식을 익히고 연습합니다.
  • 여러 아이디어를 실험하고 최적의 해결책을 찾아냅니다.

추가 자료

다양한 알고리즘 문제와 해법을 설명하는 자료를 찾아 공부해 보세요. 알고리즘의 기초를 다짐으로써 코딩 테스트에서 좋은 성적을 거둘 수 있을 것입니다.

자바 코딩테스트 강좌, 최솟값 찾기 1

문제 정의

여러분은 주어진 정수 배열에서 최솟값을 찾는 문제를 풀어야 합니다. 이 문제는 코딩테스트와 알고리즘 문제 풀이에서 매우 기초적이고 기본적인 문제로, 배열을 다루는 기본적인 사고를 필요로 합니다. 여러분은 이 문제를 통해 배열과 루프를 사용하는 방법, 조건문을 활용하는 방법을 연습할 수 있을 것입니다.

문제 설명

정수 배열 nums가 주어질 때, 배열 내에서 최솟값을 찾아 반환하는 함수를 작성하세요.

예를 들어, 배열이 [3, 1, 4, 1, 5, 9, 2, 6, 5] 일 경우 최솟값인 1을 반환해야 합니다.

입력 및 출력 형식

  • 입력: 정수 배열 nums (1 ≤ |nums| ≤ 105, -109 ≤ nums[i] ≤ 109)
  • 출력: 배열 내 최솟값

접근 방법

이 문제는 아래와 같은 접근 방식을 통해 해결할 수 있습니다.

  1. 배열이 비어있는지 확인합니다. 비어있다면 예외 처리를 합니다.
  2. 배열의 첫 번째 요소를 최솟값으로 초기화합니다.
  3. 배열을 순회하며 각 요소와 현재 최솟값을 비교합니다.
  4. 현재 요소가 최솟값보다 작으면 최솟값을 갱신합니다.
  5. 모든 요소를 확인한 후 최솟값을 반환합니다.

상세 풀이 과정

1. 배열 비어 있는지 체크

먼저, 입력으로 주어진 배열이 비어 있는지 확인해야 합니다. 배열이 비어 있다면 최솟값을 찾는 것이 불가능하므로 이 경우 적절한 예외 처리를 해줘야 합니다. 예를 들어, 배열이 비어 있을 때는 IllegalArgumentException을 던질 수 있습니다.

2. 최솟값 초기화

배열의 첫 번째 요소를 최솟값으로 설정합니다. 이렇게 하면 각 요소를 순회하며 비교할 수 있는 기준값이 생성됩니다.

3. 배열 탐색

배열을 순회하면서 각 요소를 최솟값과 비교합니다. 안정적인 방법으로 최솟값을 찾기 위해 일반적으로 for 반복문을 사용합니다. 모든 요소를 체크할 필요가 있습니다.

4. 최솟값 비교 및 갱신

현재 확인 중인 요소가 최솟값보다 작으면 최솟값을 갱신합니다. 이를 통해 최종적으로 배열 내에서 가장 작은 값을 갖게 될 것입니다.

5. 최솟값 반환

모든 요소를 확인한 후, 최솟값을 반환합니다.

자바 구현

public class MinFinder {
    public static int findMin(int[] nums) {
        // 비어 있는 배열 체크
        if (nums.length == 0) {
            throw new IllegalArgumentException("배열이 비어 있습니다.");
        }

        // 배열의 첫 번째 요소로 초기화
        int min = nums[0];

        // 배열을 순회하며 최솟값 찾기
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] < min) {
                min = nums[i]; // 최솟값 갱신
            }
        }

        // 최솟값 반환
        return min;
    }

    public static void main(String[] args) {
        int[] nums = {3, 1, 4, 1, 5, 9, 2, 6, 5};
        int minValue = findMin(nums);
        System.out.println("최솟값: " + minValue); // 출력: 최솟값: 1
    }
}

시간 복잡도 분석

이 알고리즘의 시간 복잡도는 O(n)입니다. 배열의 모든 요소를 한 번씩 살펴봐야 하므로 입력의 크기에 대한 선형적 성질을 유지합니다. 이는 최적의 시간 복잡도입니다.

공간 복잡도 분석

이 알고리즘의 공간 복잡도는 O(1)입니다. 추가적인 데이터 구조를 사용하지 않고, 단지 정수 변수 하나만 사용하는 방식이므로 공간을 거의 차지하지 않습니다.

결론

이번 강좌를 통해 여러분은 배열에서 최솟값을 찾는 알고리즘을 구현해보았습니다. 이 문제는 기초적인 알고리즘이지만, 이후 더 복잡한 문제로 확장될 수 있는 기반을 마련해줍니다. 배열과 루프를 사용한 기본적인 사고 방식을 익혔기 때문에 앞으로의 알고리즘 문제에서도 유용하게 사용할 수 있을 것입니다.

다음 강좌에서는 더욱 복잡한 알고리즘 문제를 다루게 될 것입니다. 많은 기대 부탁드립니다.

자바 코딩테스트 강좌, 최솟값 찾기 2

문제 설명

주어진 정수 배열이 있습니다. 이 배열에서 특정 범위 내의 최솟값을 찾아야 합니다.

뿐만 아니라, 이 범위는 동적으로 주어지며 여러 쿼리에 대해 요구될 수 있습니다.

즉, 배열의 특정 인덱스 i와 j가 주어졌을 때,

i에서 j 사이의 최솟값을 찾아야 합니다.

이러한 문제를 해결하기 위한 효율적인 알고리즘을 설계하고 구현하는 것이 이번 강좌의 목표입니다.

문제 형식

입력: 정수 배열 nums와 쿼리 리스트 queries가 주어집니다.
– nums: n개의 정수로 이루어진 배열 (0 ≤ n ≤ 10^5, -10^9 ≤ nums[i] ≤ 10^9)
– queries: 여러 (i, j) 쌍이 포함된 리스트 (0 ≤ queries.length ≤ 10^5, 0 ≤ i ≤ j < n)
출력: 각 쿼리에 대한 최솟값을 리스트로 반환합니다.

문제 예시

예시 1

입력:

            nums = [2, 0, 3, 5, 1]
            queries = [[0, 2], [1, 4], [0, 4]]
            

출력:

            [0, 1, 0]
            

예시 2

입력:

            nums = [1, 2, 3, 4, 5]
            queries = [[0, 0], [0, 4], [2, 3]]
            

출력:

            [1, 1, 3]
            

해결 전략

이 문제를 해결하기 위해서는 여러 가지 접근 방식이 있습니다.

가장 단순한 방법은 각 쿼리에 대해 선형 탐색을 사용하는 것입니다.

그러나 이 방법은 최악의 경우 O(m * n)의 시간 복잡도를 가지므로,

더 효율적인 방법이 필요합니다.

우리는 세그먼트 트리(Segment Tree) 또는 희소 테이블(Sparse Table)과 같은 자료구조를 활용하여

각 쿼리에 대해 O(log n) 또는 O(1)의 시간 복잡도로 최솟값을 찾도록 하겠습니다.

세그먼트 트리 사용하기

세그먼트 트리는 주어진 배열에서 특정 구간에 대한 쿼리를 효율적으로 처리하는 자료구조입니다.

이를 통해 우리는 O(n)으로 세그먼트 트리를 구축하고, 각 쿼리를 O(log n)으로 처리할 수 있습니다.

다음은 세그먼트 트리를 구축하고 쿼리를 처리하는 방법입니다.

세그먼트 트리 구현

            // 세그먼트 트리 클래스
            class SegmentTree {
                private int[] tree;
                private int n;

                public SegmentTree(int[] nums) {
                    n = nums.length;
                    tree = new int[4 * n]; // 충분한 크기의 트리 배열
                    build(nums, 0, 0, n - 1);
                }

                private void build(int[] nums, int node, int start, int end) {
                    if (start == end) {
                        tree[node] = nums[start]; // 리프 노드에 값 저장
                    } else {
                        int mid = (start + end) / 2;
                        build(nums, 2 * node + 1, start, mid);
                        build(nums, 2 * node + 2, mid + 1, end);
                        tree[node] = Math.min(tree[2 * node + 1], tree[2 * node + 2]); 
                    }
                }

                public int query(int L, int R) {
                    return query(0, 0, n - 1, L, R);
                }

                private int query(int node, int start, int end, int L, int R) {
                    if (R < start || end < L) { 
                        return Integer.MAX_VALUE; // 해당 구간에 포함되지 않으면 무한대 반환
                    }
                    if (L <= start && end <= R) { 
                        return tree[node]; // 구간 포함 시 노드 값 반환
                    }
                    int mid = (start + end) / 2;
                    int leftMin = query(2 * node + 1, start, mid, L, R);
                    int rightMin = query(2 * node + 2, mid + 1, end, L, R);
                    return Math.min(leftMin, rightMin); // 두 자식의 최소값 반환
                }
            }
            

최종 구현 및 실험

이제 각 쿼리들을 처리하고 결과를 반환하는 메인 함수를 구현하겠습니다.

사용자가 쿼리를 요청할 수 있도록 하여, 이를 통해 효율적으로 최솟값을 가져올 수 있습니다.

최종 구현은 다음과 같습니다.

            public class MinValueFinder {
                public int[] minInRange(int[] nums, int[][] queries) {
                    SegmentTree segmentTree = new SegmentTree(nums); 
                    int[] results = new int[queries.length];

                    for (int i = 0; i < queries.length; i++) {
                        results[i] = segmentTree.query(queries[i][0], queries[i][1]); 
                    }
                    return results; 
                }
            }
            

시간 복잡도 분석

– 세그먼트 트리 구축: O(n)
– 각 쿼리 처리: O(log n)
전체 시간 복잡도는 O(n + m * log n)입니다.
이는 매우 효율적이며, 제한된 입력 조건에서도 충분히 수행할 수 있습니다.

결론

이번 강좌에서는 주어진 배열에서 특정 범위 내의 최솟값을 찾는 문제를 세그먼트 트리를 활용하여 해결했습니다.

세그먼트 트리는 여러 쿼리를 효율적으로 처리할 수 있어, 대규모 데이터에서 자주 사용되는 자료구조입니다.

이를 통해 문제 해결 능력을 향상시키기를 바랍니다.