C# 코딩테스트 강좌, 세그먼트 트리

알고리즘 대회나 코딩 인터뷰에서 효율적으로 문제를 해결하기 위한 기술 중 하나가 바로 세그먼트 트리(Segment Tree)입니다.
이 글에서는 세그먼트 트리의 기본 개념과 함께 실제 문제를 통해 그 써보는 방법을 알아보겠습니다. 세그먼트 트리는 주로 구간 쿼리 문제에서 매우 유용하게 사용될 수 있습니다.
특히, 구간 합, 구간 최소/최대값, 구간 업데이트 등의 작업을 효율적으로 처리할 수 있습니다.

세그먼트 트리란?

세그먼트 트리는 배열의 구간에 대한 정보를 효율적으로 관리하기 위한 자료구조입니다.
이를 통해 특정 구간에 대한 쿼리 연산 및 업데이트를 로그 시간 복잡도(logarithmic time complexity)로 처리할 수 있습니다.
세그먼트 트리는 일반적으로 이진 트리 형태로 구성되며, 각 노드는 특정 구간에 대한 정보를 저장합니다.

세그먼트 트리의 구조

세그먼트 트리는 아래와 같은 구조로 구성됩니다:

  • 리프 노드: 배열의 각 요소를 나타냅니다.
  • 내부 노드: 자신의 자식 노드(리프 노드)의 정보를 바탕으로 구간의 정보를 나타냅니다. 예를 들어, 리프 노드들에 대한 합, 최소 혹은 최대값 등을 저장합니다.

문제 소개

자 이제, 세그먼트 트리를 사용하여 해결할 수 있는 알고리즘 문제를 소개합니다.

문제 설명

주어진 정수 배열에서 다음의 두 가지 작업을 효율적으로 처리하는 프로그램을 작성하세요.

  1. 구간 합 쿼리: 배열의 특정 구간 [l, r]에 대한 합을 계산합니다.
  2. 구간 업데이트: 배열의 특정 인덱스에 있는 값을 업데이트하고, 이에 따른 변경사항을 반영합니다.

입력 형식

첫 줄에 배열의 크기 N과 쿼리의 수 M이 주어집니다. (1 ≤ N ≤ 100,000, 1 ≤ M ≤ 100,000)
이후 N개의 정수가 배열에 주어지며, 이후 M개의 쿼리가 주어집니다.

쿼리의 형식은 다음과 같습니다:

  • 1 l r: 배열의 인덱스 l에서 r까지의 합을 출력합니다 (1 ≤ l ≤ r ≤ N)
  • 2 x y: 배열의 x번째 요소를 y로 업데이트합니다 (1 ≤ x ≤ N, -1,000,000,000 ≤ y ≤ 1,000,000,000)

세그먼트 트리의 구현

이제 C#을 사용하여 위의 문제를 해결하기 위한 세그먼트 트리를 구현해보겠습니다.


using System;

class SegmentTree
{
    private int[] tree;
    private int size;

    public SegmentTree(int n)
    {
        size = n;
        tree = new int[n * 4]; // 세그먼트 트리의 크기
    }

    // 트리 구축
    public void Build(int[] arr, int node, int start, int end)
    {
        if (start == end)
        {
            tree[node] = arr[start]; // 리프 노드
        }
        else
        {
            int mid = (start + end) / 2;
            Build(arr, node * 2, start, mid); // 왼쪽 자식
            Build(arr, node * 2 + 1, mid + 1, end); // 오른쪽 자식
            tree[node] = tree[node * 2] + tree[node * 2 + 1]; // 부모 노드 업데이트
        }
    }

    // 구간 합 쿼리
    public int Query(int node, int start, int end, int l, int r)
    {
        if (r < start || end < l)
        {
            return 0; // 합을 구할 수 없는 구간
        }

        if (l <= start && end <= r)
        {
            return tree[node]; // 완전 포함
        }

        int mid = (start + end) / 2;
        int leftSum = Query(node * 2, start, mid, l, r);
        int rightSum = Query(node * 2 + 1, mid + 1, end, l, r);
        return leftSum + rightSum; // 두 구간의 합 반환
    }

    // 업데이트
    public void Update(int node, int start, int end, int idx, int val)
    {
        if (start == end)
        {
            tree[node] = val; // 리프 노드 업데이트
        }
        else
        {
            int mid = (start + end) / 2;
            if (start <= idx && idx <= mid)
            {
                Update(node * 2, start, mid, idx, val); // 왼쪽 자식 업데이트
            }
            else
            {
                Update(node * 2 + 1, mid + 1, end, idx, val); // 오른쪽 자식 업데이트
            }
            tree[node] = tree[node * 2] + tree[node * 2 + 1]; // 부모 노드 업데이트
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        int N, M;
        N = int.Parse(Console.ReadLine());
        M = int.Parse(Console.ReadLine());

        int[] arr = Array.ConvertAll(Console.ReadLine().Split(' '), int.Parse);
        SegmentTree segTree = new SegmentTree(N);
        segTree.Build(arr, 1, 0, N - 1); // 세그먼트 트리 구축

        for (int i = 0; i < M; i++)
        {
            string[] query = Console.ReadLine().Split(' ');
            int type = int.Parse(query[0]);

            if (type == 1)
            {
                // 구간 합 쿼리
                int l = int.Parse(query[1]) - 1;
                int r = int.Parse(query[2]) - 1;
                Console.WriteLine(segTree.Query(1, 0, N - 1, l, r));
            }
            else if (type == 2)
            {
                // 업데이트
                int x = int.Parse(query[1]) - 1;
                int y = int.Parse(query[2]);
                segTree.Update(1, 0, N - 1, x, y);
            }
        }
    }
}

코드 설명

위의 C# 코드는 주어진 문제를 해결하기 위한 세그먼트 트리의 구현입니다.
각 메서드와 클래스에 대한 설명은 다음과 같습니다:

클래스 및 변수를 설명

  • SegmentTree: 세그먼트 트리를 나타내는 클래스입니다. 배열을 받아 트리를 구축하고, 구간 합 쿼리와 업데이트를 처리합니다.
  • tree: 세그먼트 트리의 배열입니다. 배열의 최대 크기는 N * 4입니다.
  • Build: 주어진 배열로부터 세그먼트 트리를 구축하는 메서드입니다.
  • Query: 주어진 구간에 대한 합을 반환하는 메서드입니다.
  • Update: 주어진 인덱스의 값을 업데이트하고, 이에 따른 변경사항을 트리에 반영하는 메서드입니다.

메인 함수 설명

메인 함수에서는 배열의 크기와 쿼리의 수를 입력받고, 주어진 배열로 세그먼트 트리를 구축한 다음, 쿼리에 따라
구간 합을 계산하거나 업데이트를 수행합니다.

결론

이번 글에서는 C#을 이용한 세그먼트 트리의 구현과 구간 합 쿼리 및 업데이트 문제를 해결하는 방법에 대해 알아보았습니다.
세그먼트 트리는 구간 쿼리를 효율적으로 처리할 수 있는 강력한 도구이며, 여러 문제에 적용 가능합니다.
복잡한 쿼리나 업데이트가 요구되는 문제에 직면했을 때, 세그먼트 트리를 고려해보는 것이 좋습니다.
다양한 연습 문제를 풀어보며 이 자료구조의 이해를 깊이 있게 다지길 바랍니다.

C# 코딩테스트 강좌, 슬라이딩 윈도우

이 글에서는 슬라이딩 윈도우 기법을 사용하여 알고리즘 문제를 해결하는 방법을 소개합니다. 슬라이딩 윈도우는 다양한 배열 및 문자열 관련 문제에 효과적인 기법으로, 시간 복잡도를 줄이고 효율적인 해결책을 제공합니다.

문제 설명

다음과 같은 배열이 주어졌을 때, 가장 긴 연속적인 합이 K를 초과하지 않는 부분 배열의 길이를 구하는 문제를 해결해 보겠습니다.

            입력: nums = [1,2,3,4,5,6], K = 10
            출력: 4
            설명: [1,2,3,4]는 합이 10을 넘지 않으면서 가장 긴 부분 배열입니다.
            

문제 접근 방식

슬라이딩 윈도우 기법을 활용하여 배열을 순회하면서 현재의 합을 유지하고, 이 합이 K를 초과하지 않을 때 최대 길이를 기록합니다. 현재의 합이 K를 초과하는 경우, 시작 인덱스를 증가시켜 합을 줄이고 조건을 만족하는 지점을 찾아갑니다.

슬라이딩 윈도우 알고리즘 구현

            
            public class Solution {
                public int MaxLengthSubArray(int[] nums, int K) {
                    int left = 0, maxLength = 0, currentSum = 0;

                    for (int right = 0; right < nums.Length; right++) {
                        currentSum += nums[right];

                        while (currentSum > K) {
                            currentSum -= nums[left];
                            left++;
                        }

                        maxLength = Math.Max(maxLength, right - left + 1);
                    }

                    return maxLength;
                }
            }
            
            

코드 설명

이제 코드의 각 부분을 살펴보겠습니다.

  • 변수 초기화: left, maxLength, currentSum 변수를 선언합니다.
  • for 루프: 오른쪽 포인터 right를 사용하여 배열을 순회합니다. currentSum에 현재 숫자를 추가합니다.
  • while 루프: 현재 합이 K를 초과할 경우, 왼쪽 포인터 left를 증가시키며 currentSum에서 해당 숫자를 빼고, 이 과정을 반복하여 합이 K 이하가 될 때까지 진행합니다.
  • 최대 길이 업데이트: 현재 윈도우의 길이를 계산하고, maxLength를 갱신합니다.

복잡도 분석

이 알고리즘은 배열을 한 번 순회하기 때문에 시간 복잡도는 O(N)이며, 추가적인 공간을 사용하지 않기 때문에 공간 복잡도는 O(1)입니다. 이는 슬라이딩 윈도우 기법이 매우 효율적임을 보여줍니다.

결론

슬라이딩 윈도우 기법은 배열과 문자열 문제에서 매우 유용한 기술로, 효율적인 솔루션을 제공할 수 있습니다. 이 기법은 다양한 문제에 응용할 수 있으며, 여러분의 코딩 테스트 준비에 큰 도움이 될 것입니다. 문제를 풀어보면서 슬라이딩 윈도우 기법의 이해도를 높여보세요!

C# 코딩테스트 강좌, 디버깅 활용 사례 살펴보기

안녕하세요! 본 글에서는 C#을 활용한 코딩 테스트 문제를 함께 해결해보며, 디버깅의 중요성과 활용 방법에 대해 살펴보겠습니다. 실제 문제를 풀며 디버깅 기술이 어떻게 도움이 되는지 구체적인 사례를 통해 확인해보겠습니다.

문제 설명

문제: 가장 긴 펠린드롬 부분 문자열

주어진 문자열에서 가장 긴 펠린드롬 부분 문자열을 찾아 반환하는 함수를 만드세요. 문자열의 길이는 최대 1000입니다.

예시 입력:


"babad"

예시 출력:


"bab" 또는 "aba"

문제 해결 과정

이제, 이 문제를 해결하기 위한 접근 방식을 단계별로 살펴보겠습니다.

1단계: 문제 이해하기

문제의 핵심은 입력된 문자열에서 펠린드롬을 검사하는 것입니다. 펠린드롬은 앞뒤가 같은 문자열을 의미합니다. 예를 들어, “racecar”가 그 예시입니다. 우리는 주어진 문자열에서 가능한 모든 부분 문자열을 생성하고, 각 문자열이 펠린드롬인지 확인해야 합니다.

2단계: 알고리즘 설계

가장 긴 펠린드롬 부분 문자열을 찾기 위해서 다음과 같은 알고리즘을 설계했습니다:

  1. 주어진 문자열의 모든 부분 문자열을 생성합니다.
  2. 각 부분 문자열이 펠린드롬인지 확인합니다.
  3. 펠린드롬 일 경우 해당 문자열의 길이를 비교하여 가장 긴 것을 저장합니다.

3단계: C# 코드 구현

이제 각 단계에 맞게 C# 코드를 구현해보겠습니다.


using System;

public class LongestPalindromicSubstring
{
    public string GetLongestPalindrome(string s)
    {
        if (string.IsNullOrEmpty(s)) return "";

        string longestPalindrome = "";

        for (int i = 0; i < s.Length; i++)
        {
            // 홀수 길이의 펠린드롬 검사
            string oddPalindrome = ExpandFromCenter(s, i, i);
            if (oddPalindrome.Length > longestPalindrome.Length)
            {
                longestPalindrome = oddPalindrome;
            }

            // 짝수 길이의 펠린드롬 검사
            string evenPalindrome = ExpandFromCenter(s, i, i + 1);
            if (evenPalindrome.Length > longestPalindrome.Length)
            {
                longestPalindrome = evenPalindrome;
            }
        }

        return longestPalindrome;
    }

    private string ExpandFromCenter(string s, int left, int right)
    {
        while (left >= 0 && right < s.Length && s[left] == s[right])
        {
            left--;
            right++;
        }
        return s.Substring(left + 1, right - left - 1);
    }
}

4단계: 디버깅 및 테스트

이제 구현한 코드를 테스트하고, 오류가 있는지 디버깅할 차례입니다.

  1. 코드를 실행 후, 다양한 케이스를 입력하여 결과가 예상과 일치하는지 확인합니다.
  2. 특히, 공백 문자, 숫자, 특수문자 등이 포함된 복잡한 문자열을 테스트하여 펠린드롬 검사가 잘 이루어지는지 확인합니다.

예제 테스트 케이스


LongestPalindromicSubstring lps = new LongestPalindromicSubstring();
Console.WriteLine(lps.GetLongestPalindrome("babad")); // bab 또는 aba
Console.WriteLine(lps.GetLongestPalindrome("cbbd")); // bb
Console.WriteLine(lps.GetLongestPalindrome("a")); // a
Console.WriteLine(lps.GetLongestPalindrome("ac")); // a
Console.WriteLine(lps.GetLongestPalindrome("racecar")); // racecar

디버깅 팁

디버깅할 때 다음과 같은 점을 유념하세요:

  • 변수 값을 주의 깊게 확인하며 필요시 중단점을 두고 코드를 실행해보세요.
  • 각 단계별로 예상하는 값과 실제 값을 비교하여 어디에서 오류가 발생하는지 파악하세요.
  • 코드를 작은 조각으로 나누어 각각의 기능이 잘 작동하는지 독립적으로 테스트하세요.

마무리

이번 글에서는 C#을 활용한 펠린드롬 문제를 통해 알고리즘 설계 및 디버깅 과정에 대해 살펴보았습니다. 디버깅은 문제를 해결하는 데 있어 매우 중요한 단계이며, 프로그램이 예상대로 작동하게 만들기 위해 반복적으로 수행해야 하는 과정입니다. 각 단계를 천천히 검토하고, 필요한 도움을 찾는 것을 주저하지 마세요.

이제 여러분도 이 문제를 활용하여 C# 코딩 테스트에 대비할 수 있습니다. 지속적으로 연습하고, 다양한 문제를 풀어나가세요!

추가 학습 자료

마지막으로, 다음과 같은 자료를 참고하여 더 깊이 있는 학습을 할 수 있습니다:

C# 코딩테스트 강좌, 어떤 알고리즘으로 풀어야 할까

코딩테스트는 현대의 IT 기업에서 필수적인 절차입니다. 많은 지원자들이 알고리즘과 자료구조를 이해하고, 이를 바탕으로 문제를 해결하는 능력을 평가받기 위해 준비하고 있습니다. 이 글에서는 C#을 활용하여 알고리즘 문제를 해결하는 방법에 대해 자세히 살펴보겠습니다.

문제: 유효한 괄호 문자열

주어진 문자열이 유효한 괄호 문자열인지 판단하는 문제입니다. 문자열은 ‘(‘, ‘)’, ‘{‘, ‘}’, ‘[‘ 및 ‘]’ 문자로만 구성됩니다. 유효한 괄호 문자열은 다음 조건을 만족해야 합니다:

  • 모든 열린 괄호는 닫힌 괄호로 닫혀야 합니다.
  • 닫힌 괄호는 항상 그에 해당하는 최근의 열린 괄호와 짝을 이루어야 합니다.
  • 괄호의 닫힘이 열림 이전에 발생하지 않아야 합니다.

예를 들어, 문자열 “{[()]}”는 유효한 괄호 문자열이며, “{[(])}”는 유효하지 않습니다.

문제 해결 과정

1. 문제 분석

이 문제를 해결하기 위해서 스택 자료구조를 사용할 수 있습니다. 스택은 후입선출(LIFO) 방식으로 작동하므로, 열린 괄호를 스택에 추가하고, 닫힌 괄호를 만날 경우 스택에서 가장 최근의 열린 괄호를 제거하는 방식으로 진행하면 됩니다. 모든 괄호를 확인한 후 스택이 비어 있다면 올바른 괄호 문자열입니다.

2. 알고리즘 설계

이 문제를 해결하기 위한 알고리즘은 다음과 같습니다:

  1. 괄호의 짝을 정의한다: { '(': ')', '{': '}', '[': ']' }
  2. 문자열을 순회하면서 열린 괄호는 스택에 추가한다.
  3. 닫힌 괄호를 만났을 경우, 스택에서 최근 열린 괄호를 꺼내서 짝이 맞는지 확인한다.
  4. 스택이 비어 있지 않거나 짝이 맞지 않는 경우, 유효하지 않은 문자열로 판단한다.
  5. 문자열을 모두 탐색 이후, 스택이 비어 있으면 유효한 문자열이다.

3. C# 코드 구현


using System;
using System.Collections.Generic;

public class ValidParentheses
{
    public static bool IsValid(string s)
    {
        Stack stack = new Stack();
        Dictionary parentheses = new Dictionary()
        {
            {'(', ')'},
            {'{', '}'},
            {'[', ']'}
        };

        foreach (char c in s)
        {
            // 열린 괄호일 경우 스택에 추가
            if (parentheses.ContainsKey(c))
            {
                stack.Push(c);
            }
            else
            {
                // 닫힌 괄호일 경우 스택 비어있으면 검증 실패
                if (stack.Count == 0 || parentheses[stack.Pop()] != c)
                {
                    return false;
                }
            }
        }

        // 스택이 비어있다면 유효한 괄호 문자열
        return stack.Count == 0;
    }
}

class Program
{
    static void Main()
    {
        string input = "{[()]}";
        bool result = ValidParentheses.IsValid(input);
        Console.WriteLine($"\"{input}\"는 유효한 괄호 문자열인가? {result}");
    }
}
    

4. 알고리즘 분석

위 코드는 스택을 사용하여 문자열을 한 번 순회하는 방식으로, 시간 복잡도는 O(n)입니다. n은 문자열의 길이입니다. 공간 복잡도 역시 O(n)입니다. 스택에 최대 n개의 열린 괄호가 쌓일 수 있기 때문입니다. 이 알고리즘은 입력 문자열이 길어져도 효율적으로 동작할 수 있습니다.

마무리

이번 글에서는 C#을 사용하여 유효한 괄호 문자열 문제를 해결하는 방법에 대해 알아보았습니다. 알고리즘 문제를 풀 때는 문제를 잘 분석하고, 적절한 자료구조를 선택하는 것이 중요합니다. 스택과 같은 간단한 자료구조를 활용하면 복잡한 문제도 효율적으로 해결할 수 있습니다. 더욱 다양한 문제를 통해 알고리즘적 사고를 키우는 데 도움이 되길 바랍니다.

C# 코딩테스트 강좌, 물의 양 구하기

문제 설명

특정 지역에 물을 담을 수 있는 수조가 있습니다. 수조의 각 벽은 높이가 다릅니다. 비가 내리면 물이 담기게 될텐데,
각 벽 사이에 얼마나 많은 물이 담길 수 있는지를 구하는 문제입니다.

입력

첫 번째 줄에는 정수 N(1 ≤ N ≤ 100,000)이 주어집니다. N은 벽의 개수를 나타냅니다.
두 번째 줄에는 N개의 정수 h_i (1 ≤ h_i ≤ 1,000,000)가 주어지며,
이는 각 벽의 높이를 나타냅니다.

출력

한 줄에 물이 담길 수 있는 양을 출력합니다.

문제 예제

예제 입력:
5
0 1 0 2 1 0 1 3 2 1 2 1
예제 출력:
6

문제 풀이 과정

이 문제를 풀기 위해서 우리는 두 개의 배열을 사용할 것입니다.
하나는 현재 위치에서 왼쪽에 있는 벽의 최고 높이를 저장하고, 또 하나는 오른쪽에 있는 벽의 최고 높이를 저장합니다.

1. 문제 분석

각각의 위치에서 담길 수 있는 물의 양은, 그 위치의 벽 높이보다 왼쪽과 오른쪽의 벽 높이 중 최소값에서 현재 벽 높이를 뺀 값입니다.
예를 들어, 위치 i에서 물의 양은 다음과 같이 계산할 수 있습니다:
water[i] = max(0, min(left_max[i], right_max[i]) - height[i])
여기서 left_max는 i의 왼쪽 벽 중 최대 높이를, right_max는 오른쪽 벽 중 최대 높이를 의미합니다.

2. 알고리즘 설계

우리는 다음과 같은 단계로 알고리즘을 설계할 수 있습니다.

  1. 입력을 받아온다.
  2. 왼쪽 벽의 최대 높이를 저장하는 배열 left_max를 생성한다.
  3. 오른쪽 벽의 최대 높이를 저장하는 배열 right_max를 생성한다.
  4. 각 위치에서 담길 수 있는 물의 양을 계산한다.
  5. 최종적으로 물의 총량을 출력한다.

3. 코드 구현

아래는 위의 알고리즘을 C#으로 구현한 코드입니다.


using System;

class Program
{
    static void Main(string[] args)
    {
        int N = int.Parse(Console.ReadLine());
        int[] height = Array.ConvertAll(Console.ReadLine().Split(), int.Parse);

        // 왼쪽 최대 높이 배열
        int[] left_max = new int[N];
        left_max[0] = height[0];

        for (int i = 1; i < N; i++)
        {
            left_max[i] = Math.Max(left_max[i - 1], height[i]);
        }

        // 오른쪽 최대 높이 배열
        int[] right_max = new int[N];
        right_max[N - 1] = height[N - 1];

        for (int i = N - 2; i >= 0; i--)
        {
            right_max[i] = Math.Max(right_max[i + 1], height[i]);
        }

        // 물의 양 계산
        int water = 0;
        for (int i = 0; i < N; i++)
        {
            water += Math.Max(0, Math.Min(left_max[i], right_max[i]) - height[i]);
        }

        Console.WriteLine(water);
    }
}
    

4. 코드 설명

위의 C# 코드에서 사용된 알고리즘은 다음과 같습니다.

  • int N = int.Parse(Console.ReadLine());
    – N값을 입력받아 벽의 개수를 저장합니다.
  • int[] height = Array.ConvertAll(Console.ReadLine().Split(), int.Parse);
    – 높이 입력을 받아 배열로 변환합니다.
  • left_max 배열은 왼쪽에서부터의 최대 높이를 저장합니다. 이를 위해 첫 번째 벽의 높이를 세팅한 후, 반복문을 돌며
    각 벽의 높이를 비교하여 최대값을 저장합니다.
  • right_max 배열도 마찬가지로 오른쪽에서부터 최대 높이를 저장합니다.
  • 마지막으로, 각 벽 위치에서 물이 담길 수 있는 양을 계산하여 총 물의 양을 구합니다.

5. 복잡도 분석

이 알고리즘의 시간 복잡도는 O(N)입니다. 벽의 개수 N에 비례하여 연산을 수행하므로 제공된
입력 범위 내에서 효율적으로 동작합니다. 공간 복잡도 또한 O(N)으로 두 개의 배열을 저장하기 위한 공간이 필요합니다.

결론

이번 글에서는 물의 양을 계산하는 문제를 C#으로 해결해 보았습니다. 알고리즘을 단계적으로 설계하고
구현하는 과정에서 코딩 테스트에서 자주 접할 수 있는 문제 유형들을 확인할 수 있었습니다.
이 강좌가 당신의 코딩 테스트 준비에 도움이 되기를 바랍니다.