DSA/코딩테스트

99클럽 코테 스터디 6일차 TIL - Java 백준 27160번 할리갈리

readyoun 2025. 1. 22. 06:57

🃏 [백준 27160] 할리갈리: HashMap과 배열, ENUM

🌟 Today's Focus

  • HashMap 연산과 활용
  • 자료구조 성능 최적화
  • Java Stream API
  • 입출력 처리 최적화

📝 Today I Learned

1. 문제 분석 (재정의)

오늘 도전한 문제는 할리갈리라는 카드 게임을 구현하는 것이다. 테이블에 여러 장의 카드가 있고, 각 카드에는 과일의 종류와 개수가 적혀있다. 같은 종류의 과일이 정확히 5개가 되는 순간을 찾아내야 한다. 처음 문제를 보았을 때는 배열로 간단하게 해결할 수 있겠다고 생각했다.

하지만 오늘 Hash 특강을 듣고 나서, HashMap을 활용하면 더 우아한 해결책이 될 수 있겠다는 생각이 들었다.

2. 문제 접근 방식

처음에는 단순히 이렇게 접근했다:

int[] fruitCounts = new int[4]; // 과일 종류가 4개로 고정
// STRAWBERRY = 0, BANANA = 1, LIME = 2, PLUM = 3

그러나 Hash 특강에서 배운 내용을 적용하면서 생각이 바뀌었다:

Map<String, Integer> fruitCount = new HashMap<>();
// 과일 이름을 직접 키로 사용할 수 있다!

3. 문제 해결 과정

첫 번째 시도: 기본 배열 접근

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());

        int[] fruitCount = new int[4];
        for (int i = 0; i < N; i++) {
            String[] input = br.readLine().split(" ");
            int index = getFruitIndex(input[0]);  // 별도의 매핑 함수 필요
            fruitCount[index] += Integer.parseInt(input[1]);
        }

        boolean found = false;
        for (int count : fruitCount) {
            if (count == 5) {
                found = true;
                break;
            }
        }

        System.out.println(found ? "YES" : "NO");
    }

    private static int getFruitIndex(String fruit) {
        switch (fruit) {
            case "STRAWBERRY": return 0;
            case "BANANA": return 1;
            case "LIME": return 2;
            default: return 3; // PLUM
        }
    }
}

 

이 방식의 문제점:

  1. 과일 이름과 인덱스 매핑이 필요하다
  2. 코드가 직관적이지 않다
  3. 새로운 과일이 추가되면 코드 수정이 필요하다

두 번째 시도: HashMap 활용

Hash 특강을 듣고 나서 시도한 개선된 버전이다.

import java.io.*;
import java.util.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());

        Map<String, Integer> fruitCount = new HashMap<>();

        while (N-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            String fruit = st.nextToken();
            int count = Integer.parseInt(st.nextToken());

            // merge 메서드 활용
            fruitCount.merge(fruit, count, Integer::sum);
        }

        System.out.println(fruitCount.values().stream()
                .anyMatch(count -> count == 5) ? "YES" : "NO");
    }
}

세 번째 시도: ENUM을 활용한 타입 안전성 확보

ENUM을 사용하면 컴파일 타임에 타입 안전성을 확보할 수 있다는 점을 떠올렸다. 실수로 잘못된 문자열을 입력하는 것을 방지할 수 있기 때문이다.

import java.io.*;

public class Main {
    enum Fruit {
        STRAWBERRY, BANANA, LIME, PLUM;

        // 문자열을 ENUM으로 안전하게 변환
        public static Fruit from(String name) {
            try {
                return Fruit.valueOf(name);
            } catch (IllegalArgumentException e) {
                throw new IllegalArgumentException("유효하지 않은 과일 이름입니다: " + name);
            }
        }
    }

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int N = Integer.parseInt(br.readLine());

        // 각 ENUM 값에 대한 카운트를 저장할 배열
        int[] counts = new int[Fruit.values().length];

        while (N-- > 0) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            Fruit fruit = Fruit.from(st.nextToken());
            int count = Integer.parseInt(st.nextToken());

            counts[fruit.ordinal()] += count;
        }

        boolean hasExactlyFive = false;
        for (int count : counts) {
            if (count == 5) {
                hasExactlyFive = true;
                break;
            }
        }

        System.out.println(hasExactlyFive ? "YES" : "NO");
    }
}

 

ENUM을 사용한 접근의 장점:

  1. 컴파일 타임에 타입 안전성 확보
  2. IDE의 자동 완성 지원
  3. 과일 종류가 제한적이므로 메모리 사용 최적화
  4. 잘못된 입력에 대한 명확한 예외 처리 가능

단점:

  1. ENUM 정의로 인한 추가 코드 작성 필요
  2. valueOf() 메서드 호출 시 약간의 오버헤드 발생
  3. 새로운 과일 추가 시 ENUM 수정 필요

4. 최종 개선 사항

  1. StringTokenizer 사용으로 문자열 파싱 성능 개선
  2. merge 메서드로 코드 간결화
  3. Stream API로 가독성 향상

💭 Reflection

어려웠던 점과 해결 방법

처음에는 가장 기본적인 배열로 접근했는데, 이는 과일 이름과 인덱스 간의 매핑이 필요해서 코드가 복잡해졌다. Hash 특강을 통해 HashMap의 장점을 알게 되었고, 이를 적용하여 더 깔끔한 코드를 작성할 수 있었다.

세 가지 방식의 성능 비교

각 방식별로 백준에서 테스트한 결과는 다음과 같다.

구현 방식 실행 시간 메모리 사용량 코드 라인 수
배열 128ms 14,196KB 35줄
HashMap 136ms 14,320KB 28줄
ENUM 124ms 14,244KB 45줄

 

ENUM 방식이 약간의 코드량 증가에도 불구하고 실행 시간과 메모리 사용량 측면에서 좋은 성능을 보여주었다. 이는 ENUM의 ordinal()을 통한 배열 접근이 HashMap의 해시 연산보다 효율적이기 때문이다.

새롭게 알게된 점

  1. HashMap의 merge 메서드가 이런 상황에서 매우 유용하다
  2. 입력 처리할 때 StringTokenizersplit보다 효율적일 수 있다
  3. 때로는 가장 단순해 보이는 방법이 최선이 아닐 수 있다
  4. ENUM의 ordinal()과 valueOf() 메서드의 활용
  5. 타입 안전성과 성능 사이의 트레이드오프

앞으로 학습할 것

  1. HashMap의 다른 유용한 메서드들 살펴보기
  2. 입출력 처리 최적화 방법 더 알아보기
  3. Stream API의 다양한 활용법 학습하기
  4. ENUM의 다양한 활용 방법 학습하기
  5. 자바의 타입 시스템 깊이 있게 이해하기

이렇게 세 가지 서로 다른 접근 방식을 통해 같은 문제를 해결해보니, 각각의 장단점을 더 명확하게 이해할 수 있었다. 특히 ENUM을 활용한 방식은 타입 안전성이라는 새로운 관점을 제시해 주었다.