코딩테스트/백준

2696번 중앙값 구하기

Cadi 2025. 4. 24. 03:07

코딩테스트 :  2696번 중앙값 구하기 

시간 제한메모리 제한제출정답맞힌 사람정답 비율
1 초 128 MB 10306 5007 3922 50.334%

문제

어떤 수열을 읽고, 홀수번째 수를 읽을 때 마다, 지금까지 입력받은 값의 중앙값을 출력하는 프로그램을 작성하시오.

예를 들어, 수열이 1, 5, 4, 3, 2 이면, 홀수번째 수는 1번째 수, 3번째 수, 5번째 수이고, 1번째 수를 읽었을 때 중앙값은 1, 3번째 수를 읽었을 때는 4, 5번째 수를 읽었을 때는 3이다.

입력

첫째 줄에 테스트 케이스의 개수 T(1 ≤ T ≤ 1,000)가 주어진다. 각 테스트 케이스의 첫째 줄에는 수열의 크기 M(1 ≤ M ≤ 9999, M은 홀수)이 주어지고, 그 다음 줄부터 이 수열의 원소가 차례대로 주어진다. 원소는 한 줄에 10개씩 나누어져있고, 32비트 부호있는 정수이다.

출력

각 테스트 케이스에 대해 첫째 줄에 출력하는 중앙값의 개수를 출력하고, 둘째 줄에는 홀수 번째 수를 읽을 때 마다 구한 중앙값을 차례대로 공백으로 구분하여 출력한다. 이때, 한 줄에 10개씩 출력해야 한다.

using System.Collections.Generic;
using System.Runtime.Intrinsics.X86;
using System.Text;

public class BackJoon
{
    // 2696번 중앙값 구하기
    static List<int> maxHeap= new List<int>();
    static List<int> minHeap= new List<int>();
    private static int middle = 0;


    public static void Main()
    {
        // 아이디어 : 중앙값을 Pop해야 한다, 그러므로 왼쪽 오른쪽을 따로 유지하여 왼쪽에서는 최댓값 힙,
        // 오른쪽에서는 최솟값 힙을 구현하여 Pop 되는 수가 중앙값임을 보장한다.
        
        // 왼쪽과 오른쪽의 수가 같게 만들어야 한다, 어느 한쪽에 들어갔으면 다음번 수는 반대쪽에 넣어야 한다. ( 홀수 ) 
        // 혹은 어느 한 쪾에 두 번 들어간다면, 맨 위에 있는 것을 빼서 반대쪽에 추가해 주어야 한다. 
        
        // 테스트 케이스 개수
        int n = int.Parse(Console.ReadLine());

        StringBuilder sb = new StringBuilder();
        
        for (int i = 0; i < n; i++)
        {
            int tempCase = int.Parse(Console.ReadLine());
            List<int> tempArr = new List<int>();
            while (tempArr.Count < tempCase)
            {
                tempArr.AddRange(Console.ReadLine().Split(' ').Select(int.Parse));
            }            
            minHeap.Clear();
            maxHeap.Clear();

            middle = tempArr[0];
            sb.AppendLine((tempCase / 2 + 1).ToString());
            int outputCount = 0;
            sb.Append(middle + " ");
            outputCount++;
            for (int j = 1; j < tempCase; j++)
            {
                int temp = tempArr[j];

                // 왼쪽 오른쪽에 넣기
                if (temp < middle)
                {
                    maxHeap.Add(temp);
                    HeapifyUpLeft();
                }
                else
                {
                    minHeap.Add(temp);
                    HeapifyUpRight();
                }
                
                //균형 맞추기
                if (maxHeap.Count > minHeap.Count)
                {
                    minHeap.Add(middle);
                    HeapifyUpRight();
                    middle = maxHeap[0];
                    maxHeap[0] = maxHeap[maxHeap.Count - 1];
                    maxHeap.RemoveAt(maxHeap.Count - 1);
                    if (maxHeap.Count > 0)  HeapifyDownLeft();

                }
                else if ( maxHeap.Count < minHeap.Count)
                {
                    maxHeap.Add(middle);
                    HeapifyUpLeft();
                    middle = minHeap[0];
                    minHeap[0] = minHeap[minHeap.Count - 1];
                    minHeap.RemoveAt(minHeap.Count - 1);
                   if ( minHeap.Count > 0) HeapifyDownRight();
                }

                if ((j + 1) % 2 == 1)
                {
                    sb.Append(middle + " ");
                    outputCount++;
                    if ( outputCount % 10 == 0) sb.AppendLine();
                }
            }
            if (outputCount % 10 != 0) sb.AppendLine();
        }
        Console.WriteLine(sb.ToString());
    }

    public static void HeapifyUpRight()
    {
        int tempIndex = minHeap.Count - 1;
        while (tempIndex > 0)
        {
            int parent = (tempIndex - 1) / 2;
            if (minHeap[tempIndex] < minHeap[parent])
            {
                Swap(minHeap, tempIndex, parent);
                tempIndex = parent;
            }
            else break;
        }
    }

    public static void HeapifyUpLeft()
    {
        int tempIndex = maxHeap.Count - 1;
        while (tempIndex > 0)
        {
            int parent = (tempIndex - 1) / 2;
            if (maxHeap[tempIndex] > maxHeap[parent])
            {
                Swap(maxHeap, tempIndex, parent);
                tempIndex = parent;
            }
            else break;
        }
    }

    public static void HeapifyDownLeft()
    {
        int tempIndex = 0;

        int maxIndex = tempIndex;
        while (tempIndex < maxHeap.Count)
        {
            int left = tempIndex * 2 + 1;
            int right = tempIndex * 2 + 2;
            
            if ( left < maxHeap.Count && maxHeap[left] > maxHeap[maxIndex]) maxIndex = left;
            if (right < maxHeap.Count && maxHeap[right] > maxHeap[maxIndex]) maxIndex = right;
            if (tempIndex != maxIndex)
            {
                Swap(maxHeap, tempIndex, maxIndex);
                tempIndex = maxIndex;
            }
            else break;
        }
    }

    public static void HeapifyDownRight()
    {
        int tempIndex = 0;
        
        int minIndex = tempIndex;
        while (tempIndex < minHeap.Count)
        {
            int left = tempIndex * 2 + 1;
            int right = tempIndex * 2 + 2;
            
            if ( left < minHeap.Count && minHeap[left] < minHeap[minIndex]) minIndex = left;
            if ( right < minHeap.Count && minHeap[right] < minHeap[minIndex]) minIndex = right;
            if (tempIndex != minIndex)
            {
                Swap(minHeap, tempIndex, minIndex);
                tempIndex = minIndex;
            }
            else break;
        }
    }


    public static void Swap(List<int> list, int a, int b)
    {
        (list[a], list[b]) = (list[b], list[a]);
    }

}

 

지금까지 풀었던 문제 중 가장 길게... 써서 풀었던 문제다. 

우선순위 큐를 쓴다면 훨씬 편하게 풀었겠지만, 일단 내 손으로 해 보고 싶었다.

그래서 풀고 나서 다시 풀 문제에 적어두고, 그때는 우선순위 큐로 풀 예정이다.

 

 

아이디어는 다음과 같다. 

1. 수열의 안에 들어 있는 수가 홀수일 때, '중앙값'을 기준으로 좌우의 원소의 개수는 항상 같아야 한다.

2. 전체를 우선순위 큐로 만들 수 없으니, 왼쪽은 최대값 큐, 오른쪽은 최소값 큐로 만들고, 중앙값은 따로 설정해둔다.

3. 수가 들어오면, 중앙값과 비교한 후 , 작으면 왼쪽에, 크면 오른쪽에 넣고 정렬한다. ( 매 번 정렬이 필요하다 ) 

4. 정렬하고, 두 우선순위 큐의 차이가 1 이상이면 ( 한 쪽으로 두 번 들어갔으면 ) 큰 쪽에서 하나를 빼서 미들에 놓고, 미들을 반대쪽에 넣고 정렬한다. 

 

처음부터 아이디어 자체는 거의 맞았는데 , 계속해서 오류가 나서 , 예외처리를 하는데 시간이 오래걸렸다. 

 

 

개선점

위의 코드는 매 번 왼쪽과 오른쪽을 비교하지만 다음과 같이 한 쪽으로 두 번 이상 몰렸을 때 정렬하는 것이 효율적이다.

 

//균형 맞추기
if (maxHeap.Count > minHeap.Count + 1)
{
    minHeap.Add(middle);
    HeapifyUpRight();
    middle = maxHeap[0];
    maxHeap[0] = maxHeap[maxHeap.Count - 1];
    maxHeap.RemoveAt(maxHeap.Count - 1);
    if (maxHeap.Count > 0)  HeapifyDownLeft();

}
else if ( maxHeap.Count + 1 < minHeap.Count)
{
    maxHeap.Add(middle);
    HeapifyUpLeft();
    middle = minHeap[0];
    minHeap[0] = minHeap[minHeap.Count - 1];
    minHeap.RemoveAt(minHeap.Count - 1);
   if ( minHeap.Count > 0) HeapifyDownRight();
}

 

처음에 이게 오류뜨는줄 알고 고쳤다가 ,다시 고치는걸 깜빡했다 ㅎ, 테스트 해 보니 정상 동작한다. 

 

두 번째로 여기서 더 개선할 수 있는 점은 공통 부분을 묶어서 새로운 함수로 만들면 편하다는 것이다. 

 public static void ReplaceRoot(List<int> heap, Action heapifyDown)
    {
        if (heap.Count == 0) return;
        heap[0] = heap[heap.Count - 1];
        heap.RemoveAt(heap.Count - 1);
        heapifyDown();
    }

다음 함수처럼 , 루트로 새로운 수를 넣었을 때의 함수를 정의한다면, 조금 더 간결하게 코드를 작성할 수 있다 .

Action을 넣는 것은 생각하지 못해서 이걸 생각하지 못했었는데, 다음부터는 조금 더 고려해봐야겠다. 

 

다른 사람의 흥미로운 풀이

 

우선순위 큐를 하나만 사용해서 푸신 분이 계신다. 

어떻게.. ? 우선순위 큐가 정확하게 어떻게 동작하는지 모르기 때문에 일단 저장해두었다. 나중에 다시 풀 때 더 자세하게 봐야겠다.

대부분의 분들이 우선순위 큐를 사용하셔서 푸셨기에 , 다음에 풀 때는 우선순위 큐를 사용할 예정이다.