C# 코딩테스트 강좌, 유니온 파인드

안녕하세요, 이번 포스팅에서는 C#을 이용한 코딩 테스트에서 자주 등장하는 유니온 파인드(Union-Find) 자료구조에 대해 다루어 보겠습니다.

유니온 파인드란?

유니온 파인드(또는 Disjoint Set Union, DSU)는 주로 그래프의 연결 요소를 관리하는 데 사용되는 자료구조입니다. 이 자료구조는 두 가지 주요 작업을 효율적으로 수행할 수 있도록 설계되었습니다.

  • Find: 특정 원소가 속한 집합을 찾는 연산
  • Union: 두 개의 집합을 합치는 연산

유니온 파인드는 주로 다음과 같은 상황에서 유용합니다:

  • 그래프의 연결 성분을 찾는 문제
  • 동일한 집합에 속하는 원소들을 그룹화하는 문제

문제 설명

어떤 무향 그래프가 있습니다. 그래프의 정점을 1부터 N까지 자연수로 표현하며, M개의 간선이 주어질 때, 주어진 간선으로 연결된 모든 정점의 집합을 구하세요.

입력

  • 첫 번째 줄에 정점의 개수 N (1 ≤ N ≤ 105)
  • 두 번째 줄에 간선의 개수 M (1 ≤ M ≤ 2 × 105)
  • 다음 M줄에 에지의 양끝점을 나타내는 두 정수 a, b (1 ≤ a, b ≤ N)

출력

각 집합의 원소를 정렬하여 공백으로 구분하여 출력합니다. 각 집합의 결과는 한 줄에 출력해야 하며 집합은 오름차순으로 정렬에 출력해야 합니다.

예제 입력

5
3
1 2
2 3
4 5

예제 출력

1 2 3
4 5

유니온 파인드 알고리즘 구현

이 문제를 해결하기 위해 유니온 파인드를 구현해야 합니다. 유니온 파인드 자료구조의 기본적인 구현은 다음과 같은 방식으로 진행됩니다:

1. 자료구조 초기화

각 원소는 자기 자신을 부모로 가지도록 초기화합니다.

C# 코딩테스트 강좌, 기수 정렬

안녕하세요! 오늘은 기수 정렬(Radix Sort)의 개념과 함께 C#을 이용한 구현 방법에 대해 알아보겠습니다. 기수 정렬은 비정렬 데이터를 정렬하는 효율적인 방법 중 하나로, 특히 정수가 차지하는 범위가 제한적일 때 특히 유용한 알고리즘입니다.

1. 기수 정렬이란?

기수 정렬은 자릿수의 위치에 따라서 숫자를 정렬하는 방식으로, 일반적으로 LSD(Least Significant Digit) 방식과 MSD(Most Significant Digit) 방식으로 나뉩니다. L를 사용하면 가장 낮은 자릿수부터 정렬하게 되고, M로 시작하면 가장 높은 자릿수부터 정렬됩니다.

2. 기수 정렬의 동작 원리

기수 정렬의 작동 방식은 다음과 같습니다:

  1. 가장 낮은 자릿수부터 시작하여 각 자릿수를 기준으로 데이터를 정렬합니다.
  2. 정렬을 반복하여 가장 높은 자릿수까지 진행합니다.
  3. 이 과정에서 안정적인 정렬 알고리즘, 예를 들어 계수 정렬(Counting Sort)을 사용하여 자릿수별로 정렬합니다.

3. 기수 정렬의 시간 복잡도

기수 정렬의 시간 복잡도는 O(d * (n + k))입니다. 여기서 d는 숫자의 자릿수, n은 정렬할 숫자의 개수, k는 0부터 최대 자릿수까지의 숫자 범위입니다. 가장 큰 장점 중 하나는 기수 정렬이 안정적이라는 점입니다.

4. 기수 정렬 문제 풀이 예제

이번에는 기수 정렬을 사용하여 특정 숫자 목록을 정렬하는 문제를 풀어보겠습니다.

문제 설명

주어진 정수 목록을 기수 정렬을 사용하여 오름차순으로 정렬하시오.
입력: [170, 45, 75, 90, 802, 24, 2, 66]
출력: [2, 24, 45, 66, 75, 90, 170, 802]

문제 풀이 과정

  1. 가장 낮은 자릿수부터 시작합니다. 첫 번째 자릿수인 1의 자리 숫자로 정렬합니다.
C#
public static void LSDRadixSort(int[] array)
{
    // 가장 큰 숫자를 찾습니다.
    int max = array.Max();
    // 자릿수를 찾습니다.
    for (int exp = 1; max / exp > 0; exp *= 10)
    {
        CountingSort(array, exp);
    }
}

private static void CountingSort(int[] array, int exp)
{
    int n = array.Length;
    int[] output = new int[n]; // 정렬된 배열
    int[] count = new int[10]; // 0-9까지의 숫자 카운트

    // 자릿수에 해당하는 원소 개수를 계산합니다.
    for (int i = 0; i < n; i++)
    {
        count[(array[i] / exp) % 10]++;
    }

    // 누적 카운트를 계산합니다.
    for (int i = 1; i < 10; i++)
    {
        count[i] += count[i - 1];
    }

    // 정렬된 배열을 만듭니다.
    for (int i = n - 1; i >= 0; i--)
    {
        output[count[(array[i] / exp) % 10] - 1] = array[i];
        count[(array[i] / exp) % 10]--;
    }

    // 원본 배열을 업데이트합니다.
    for (int i = 0; i < n; i++)
    {
        array[i] = output[i];
    }
}

위의 코드에서 우리는 LSDRadixSort 메서드를 정의하였습니다. 이 메서드는 각 자릿수에 대해 CountingSort 도움을 받아 정렬을 진행합니다.

5. 구현 및 실행

이제 위의 함수를 사용하여 전체 프로그램을 실행해보겠습니다. 아래는 프로그램의 전체 코드입니다:

C#
using System;
using System.Linq;

class Program
{
    public static void Main(string[] args)
    {
        int[] array = {170, 45, 75, 90, 802, 24, 2, 66};
        LSDRadixSort(array);

        Console.WriteLine("정렬된 배열:");
        Console.WriteLine(string.Join(", ", array));
    }
  
    public static void LSDRadixSort(int[] array)
    {
        int max = array.Max();
        for (int exp = 1; max / exp > 0; exp *= 10)
        {
            CountingSort(array, exp);
        }
    }

    private static void CountingSort(int[] array, int exp)
    {
        int n = array.Length;
        int[] output = new int[n];
        int[] count = new int[10];

        for (int i = 0; i < n; i++)
        {
            count[(array[i] / exp) % 10]++;
        }

        for (int i = 1; i < 10; i++)
        {
            count[i] += count[i - 1];
        }

        for (int i = n - 1; i >= 0; i--)
        {
            output[count[(array[i] / exp) % 10] - 1] = array[i];
            count[(array[i] / exp) % 10]--;
        }

        for (int i = 0; i < n; i++)
        {
            array[i] = output[i];
        }
    }
}

위의 코드를 실행하면 다음과 같은 결과를 얻을 수 있습니다:


정렬된 배열:
2, 24, 45, 66, 75, 90, 170, 802

6. 기수 정렬의 장단점

기수 정렬의 장점은 다음과 같습니다:

  • 빠른 속도: 특정 조건에서 우수한 성능을 보입니다. (즉, 범위가 제한적일 때)
  • 안정적 정렬: 동일한 값에 대한 상대적인 순서 유지.

그러나 단점도 존재합니다:

  • 메모리 사용: 추가적인 메모리가 필요합니다.
  • 제한된 데이터: 정수가 아닌 다른 자료형에 대해서는 사용이 어렵습니다.

7. 마무리

오늘은 기수 정렬의 기본 개념, 동작 원리, 시간 복잡도, 문제 예제에 대한 풀이 등을 살펴보았습니다. 이 알고리즘은 정수가 포함된 컬렉션을 효율적으로 처리하는 데 매우 유용합니다. 실제 코딩 테스트에서도 기수 정렬을 사용해보면 좋습니다.

그럼 다음 강좌에서 또 만나요! 각자 코딩 테스트 준비 잘 하세요!

C# 코딩테스트 강좌, 가장 길게 증가하는 부분 수열 찾기

문제 설명

주어진 배열에서 가장 긴 증가하는 부분 수열의 길이를 찾는 문제를 해결해 보겠습니다. 증가하는 부분 수열은 배열에서 서로 다른 인덱스를 가지는 요소들 중에서, 해당 요소들이 오름차순으로 정렬된 부분 배열입니다. 예를 들어, 배열 [10, 22, 9, 33, 21, 50, 41, 60, 80]에 대해 가장 긴 증가하는 부분 수열은 [10, 22, 33, 50, 60, 80]로 길이는 6입니다.

문제 정의

주어진 정수 배열 arr이 있을 때, 가장 긴 증가하는 부분 수열의 길이를 반환하는 메서드를 작성하시오.

입력

  • 첫 번째 줄에 배열의 길이 n이 주어집니다. (1 ≤ n ≤ 1000)
  • 두 번째 줄에 n개의 정수가 주어집니다. 각 정수는 -10^6 ≤ arr[i] ≤ 10^6입니다.

출력

가장 긴 증가하는 부분 수열의 길이를 출력합니다.

예제

입력

9
10 22 9 33 21 50 41 60 80

출력

6

문제 풀이 방법

가장 긴 증가하는 부분 수열을 찾는 여러 알고리즘 중 가장 많이 사용되는 방법은 동적 프로그래밍(Dynamic Programming)입니다. 이 방법은 문제를 작은 부분 문제로 나누어 해결하고, 이 결과를 바탕으로 전체 문제를 해결하는 접근법입니다.

단계별 설명

  1. 배열 초기화: 주어진 배열 arr의 길이를 n이라 가정합니다. 동적 프로그래밍 배열 dp를 초기화하여 각 인덱스 i에 대해, 그 인덱스까지의 가장 긴 증가하는 부분 수열의 길이를 저장할 것입니다. 우선 각 요소가 자기 자신만으로도 수열이 될 수 있으므로 초기값을 1로 설정합니다.
  2. 이중 반복문: 중첩 반복문을 사용하여 모든 쌍의 인덱스 ij를 비교합니다. 이때 ij 보다 크고, arr[i]arr[j]보다 클 경우 (즉, 증가하는 조건을 만족하는 경우), dp[i] 값을 업데이트합니다. 구체적으로, dp[i] = max(dp[i], dp[j] + 1)로 설정합니다. 이것은 j를 포함한 수열의 길이에 1을 추가하는 것입니다.
  3. 최대 길이 찾기: 모든 수열 길이를 계산한 후 dp 배열에서 최대값을 찾아 반환합니다.

C# 구현

using System;

class Program
{
    static void Main()
    {
        int n = int.Parse(Console.ReadLine());
        int[] arr = Array.ConvertAll(Console.ReadLine().Split(' '), int.Parse);

        int[] dp = new int[n];
        for (int i = 0; i < n; i++)
        {
            dp[i] = 1; // 각 요소는 최소 1개의 수열을 형성할 수 있음
        }

        for (int i = 1; i < n; i++)
        {
            for (int j = 0; j < i; j++)
            {
                if (arr[i] > arr[j] && dp[i] < dp[j] + 1)
                {
                    dp[i] = dp[j] + 1;
                }
            }
        }

        int maxLength = dp[0];
        for (int i = 1; i < n; i++)
        {
            if (dp[i] > maxLength)
            {
                maxLength = dp[i];
            }
        }

        Console.WriteLine(maxLength);
    }
}

시간 복잡도

이 알고리즘의 시간 복잡도는 O(n^2)입니다. 이는 두 개의 중첩 반복문을 사용하기 때문입니다. 외부 반복문과 내부 반복문 모두 각각 배열의 길이만큼 실행되므로 최악의 경우 총 n * n의 연산이 필요합니다. 공간 복잡도는 O(n)이며, 이는 dp 배열을 저장하기 위해 필요한 공간입니다.

결론

이번 강좌에서는 가장 긴 증가하는 부분 수열을 찾는 알고리즘을 C#으로 구현해 보았습니다. 이 문제는 코딩 테스트에서 자주 출제되며, 동적 프로그래밍의 기초를 배우고 익히는 데 매우 유용합니다. 꾸준한 연습을 통해 다양한 문제를 해결하는 데 필요한 사고력과 접근 방식을 기를 수 있습니다.

C# 코딩테스트 강좌, 선물 전달하기

문제 설명

당신은 친구들에게 크리스마스 서프라이즈 선물을 전달하는 이벤트를 기획하고 있습니다.
N명의 친구가 있고 각 친구는 선물을 받고 싶은 목록을 가지고 있습니다.
당신은 가능한 한 많은 친구들이 원하는 선물을 받을 수 있도록 해야 합니다.
각 친구는 원하는 선물 한 가지를 지정하며, 선물은 하나 밖에 없습니다.
단, 선물은 하나의 친구에게만 전달될 수 있습니다.

입력 형식

  • 첫째 줄에 친구의 수 N이 주어집니다.
  • 둘째 줄부터 N번째 줄까지 각 친구가 원하는 선물이 나열됩니다.

출력 형식

최대 몇 명의 친구들이 원하는 선물을 받을 수 있는지 출력합니다.

제한

  • 1 ≤ N ≤ 100
  • 선물은 알파벳 대문자로 표기되며, 최대 26종류가 존재합니다.

문제 접근 방법

이 문제는 기본적으로 그래프 이론에서 최대 매칭 문제와 유사합니다.
각 친구를 정점으로, 친구가 원하는 선물을 간선으로 연결할 수 있습니다.
이 문제를 해결하기 위해 우리는 친구들의 선물 요청을 입력 받아
각 선물에 대해 친구가 요청한 목록을 생성한 뒤,
최대 매칭을 구하는 알고리즘을 사용할 것입니다.

알고리즘 개요

– 입력된 친구의 수와 요청된 선물을 기반으로 그래프를 구성합니다.
– 그래프를 탐색하여 각 친구에게 할당 가능한 선물을 찾습니다.
– DFS(Depth First Search) 알고리즘을 사용하여 가능한 매칭을 시도합니다.

코드 구현

아래는 C#으로 작성된 선물 전달하기 문제의 풀이 코드입니다.

                
using System;
using System.Collections.Generic;

class Program
{
    static int N;                         // 친구의 수
    static List friends;          // 친구가 원하는 선물 목록
    static Dictionary gifts; // 각 선물의 매칭 상태
    static bool[] visited;                // DFS 방문 여부 기록

    static void Main(string[] args)
    {
        N = int.Parse(Console.ReadLine());
        friends = new List();

        for (int i = 0; i < N; i++)
        {
            friends.Add(Console.ReadLine());
        }

        gifts = new Dictionary();
        int totalMatched = 0;

        foreach (var friend in friends)
        {
            visited = new bool[26];
            if (DFS(friend[0], ref totalMatched))
            {
                totalMatched++;
            }
        }

        Console.WriteLine(totalMatched);
    }

    static bool DFS(char gift, ref int totalMatched)
    {
        int index = gift - 'A';

        if (visited[index]) return false; // 이미 방문한 선물
        visited[index] = true;

        if (!gifts.ContainsKey(gift.ToString()) || gifts[gift.ToString()] == -1)
        {
            gifts[gift.ToString()] = totalMatched; // 선물 매칭
            return true;
        }

        return false;
    }
}
                
            

코드 설명

위 코드는 다음과 같은 절차로 선물 전달 문제를 해결합니다.

  • 입력 읽기: 친구의 수 N을 입력받고, 각 친구마다 원하는 선물을 입력받습니다.
  • DFS 함수 구현: 요청한 선물이 가능한지 확인하며, 매칭을 시도합니다.
  • 방문 여부 기록: 선물 당 한 번만 요청을 수용하기 위해 방문 기록을 관리합니다.
  • 결과 출력: 최대 매칭 결과를 출력합니다.

시간 복잡도

이 알고리즘은 O(N^2) 복잡도를 가집니다.
각 친구에 대해 DFS를 호출하고, 선물의 수는 최대 26개로 제한되어 있기 때문에
문제의 범위 안에서는 충분히 효율적입니다.

결론

“선물 전달하기” 문제는 친구들에게 선물을 배분하는 흥미로운 문제입니다.
DFS와 그래프의 최대 매칭 원리를 통해 구현할 수 있음을 보여주었습니다.
향후 비슷한 문제에서도 이 원리를 적용하여 풀이할 수 있을 것입니다.

이 글이 C# 코딩 테스트 대비에 도움이 되었으면 합니다.
다양한 문제를 통해 알고리즘을 연습해 보시기 바랍니다.

C# 코딩테스트 강좌, 구간 합 구하기 3

코딩 테스트를 준비하는 많은 분들께서는 알고리즘 문제 풀이에 대한 체계적인 학습이 필요합니다. 오늘은 구간 합을 구하는 문제에 대해 다뤄보겠습니다. 특히, ‘구간 합 구하기 3’ 문제는 다양한 전략을 활용할 수 있는 흥미로운 문제입니다. 이 글에서는 문제의 정의, 풀이 방법, C# 코드를 통한 구현 및 시간 복잡성 분석까지 자세히 설명하겠습니다.

문제 정의

주어진 정수 배열 arr와 여러 쿼리 query가 있을 때, 각 쿼리는 구간의 시작과 끝을 나타냅니다. 우리의 목표는 각 쿼리의 구간 합을 효율적으로 계산하여 반환하는 것입니다.

예시

배열: arr = [1, 2, 3, 4, 5]
쿼리: query = [[1, 3], [0, 2], [2, 4]]
출력: [9, 6, 12]

위의 예시에서 첫 번째 쿼리는 인덱스 1부터 3까지의 구간을 의미하므로, 2 + 3 + 4 = 9가 됩니다. 나머지 쿼리들도 동일한 방식으로 처리할 수 있습니다.

문제 접근법

문제를 해결하는 여러 방법이 있지만, 배열의 원소 개수가 많을 경우 성능이 중요한 요소로 작용할 수 있습니다. 따라서 효율적인 알고리즘은 필수적입니다.

1. 전통적인 방법

가장 기본적인 접근법은 각 쿼리에 대해 배열의 값을 직접 합산하는 것입니다. 그러나 이러한 방법은 다음과 같은 문제점을 가집니다:

  • 시간 복잡도가 O(n)으로, 쿼리 수가 많아지면 효율적이지 못합니다.
  • 배열의 원소 개수가 많을 경우 매우 비효율적입니다.

2. 누적 합을 이용한 방법

보다 효율적으로 문제를 해결하기 위해 노출된 방법은 누적 합을 사용하는 것입니다. 누적 합 배열은 다음과 같이 정의됩니다:

sum[i] = arr[0] + arr[1] + ... + arr[i]

이렇게 구해진 누적 합 배열을 통해 주어진 구간 합을 빠르게 계산할 수 있습니다. 구간 [left, right]의 합은 sum[right] - sum[left - 1]로 구해집니다. 이러한 방식으로 시간 복잡도를 O(1)로 줄일 수 있습니다.

3. C# 코드 구현

이제 주어진 문제를 바탕으로 C# 언어를 사용하여 누적 합을 계산하는 코드를 구현해 보겠습니다.


using System;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        // 배열 및 쿼리 정의
        int[] arr = { 1, 2, 3, 4, 5 };
        int[][] queries = { new int[] { 1, 3 }, new int[] { 0, 2 }, new int[] { 2, 4 } };

        // 구간합을 저장할 리스트
        List results = new List();

        // 누적 합 배열 생성
        int[] prefixSum = new int[arr.Length];
        prefixSum[0] = arr[0];

        for (int i = 1; i < arr.Length; i++)
        {
            prefixSum[i] = prefixSum[i - 1] + arr[i];
        }

        // 각 쿼리에 대해 구간 합 계산
        foreach (var query in queries)
        {
            int left = query[0];
            int right = query[1];

            if (left == 0)
            {
                results.Add(prefixSum[right]);
            }
            else
            {
                results.Add(prefixSum[right] - prefixSum[left - 1]);
            }
        }

        // 결과 출력
        Console.WriteLine(string.Join(", ", results));
    }
}

코드 분석 및 설명

위 코드에서는 배열을 미리 누적 합으로 변환하여 각 쿼리의 구간 합을 O(1) 시간 내에 계산할 수 있도록 설계했습니다. 다음과 같은 단계로 진행됩니다:

  1. 누적 합 배열 생성: 배열의 첫 번째 원소를 초기화한 후, 반복문을 통해 각 원소의 누적 합을 계산합니다.
  2. 쿼리 수행: 각 쿼리를 순회하며 구간 합을 계산합니다. 만약 왼쪽 경계가 0이면, 바로 누적 합을 가져오고, 그렇지 않으면 두 원소의 차를 통해 구간 합을 구합니다.
  3. 결과 출력: 각각의 결과를 출력합니다.

시간 복잡성 분석

이 알고리즘의 시간 복잡성은 다음과 같습니다:

  • 누적 합 배열을 만들기 위한 시간: O(n)
  • 각 쿼리에 대한 구간 합 계산 시간: O(1) (쿼리 수에 비례)

따라서 전체 시간 복잡성은 O(n + m)으로 나타낼 수 있으며, 여기서 m은 쿼리의 수입니다. 이는 많은 쿼리를 처리해야 할 때 효율적인 방법입니다.

마무리

오늘은 “구간 합 구하기 3” 문제를 통해 효율적인 알고리즘을 설계하고 구현하는 방법에 대해 살펴보았습니다. 알고리즘 문제는 단순한 개념일 수 있지만, 다양한 접근 방식을 통해 해결책을 찾는 것은 코딩 테스트에 있어 매우 중요한 기술입니다. 이 강좌를 통해 여러분이 직접 코드를 작성하고, 문제를 해결하는 과정에서 많은 도움을 얻기를 바랍니다.

코드의 활용, 알고리즘의 개선을 통해 여러분의 코딩 테스트 실력을 한 단계 끌어올리길 바랍니다. 다음 강좌에서 또 만나요!