관리 메뉴

개발공부기록

Java - 푸드 파이트 대회, 콜라 문제 / SQL(MySQL) - 식품분류별 가장 비싼 식품의 정보 조회하기, 5월 식품들의 총매출 조회하기 본문

기타 개발 공부/온라인 코딩 테스트 회고

Java - 푸드 파이트 대회, 콜라 문제 / SQL(MySQL) - 식품분류별 가장 비싼 식품의 정보 조회하기, 5월 식품들의 총매출 조회하기

소소한나구리 2025. 3. 20. 10:20
728x90

java

푸드 파이트 대회

문제

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/134240
  • 대회에서 제공할 음식의 개수인 food가 int[]로 낮은 칼로리 순서대로 주어졌을 때 대회를 위한 음식의 배치를 나타내는 문자열을 return하는 함수를 원성
  • 대회에서 선수는 1대 1로 대결하는데 준비된 음식을 일렬로 배치한 뒤 한 선수는 제일 왼쪽에 음식부터 오른쪽으로 다른 선수는 제일 오른쪽에 음식부터 왼쪽으로 순서대로 먹는 방식으로 진행되며 중앙에는 물을 배치함
  • 두 선수가 먹는 음식의 종류와 양이 같아야 하며 음식을 먹는 순서도 같아야 하고 칼로리가 낮은 음식부터 먼저 먹을 수 있게 배치해야 함
  • 대회에 사용되지 않는 음식은 버려짐
  • 예를 들어 3가지의 음식이 주어지고 칼로리가 적은 순서대로 1번 음식3개, 2번 음식 4개, 3번 음식을 6개 준비하고 물이 편의상 0이라고 한다면 food는 [1, 3, 4, 6]이 되며 2 선수는 1번음식 1개, 2번음식 2개, 3번 음식 3개씩을 먹게 되므로 음식의 배치는 1223330333221 이 되야하고 1번 음식 1개는 사용하지 못함

제한조건

  • food의 길이는 2 이상 9 이하이며 food의 각 원소의 길이는 1 이상 1,000 이하임
  • food에는 칼로리가 적은 순서대로 음식의 양이 담겨있으며 food[i]는 i번의 음식의 수임
  • food[0]은 준비한 물의 양이며 항상 1임
  • 정담의 길이가 3 이상인 경우만 입력으로 주어짐

입출력 예시

나의 풀이

import java.util.*;

class Solution {
    public String solution(int[] food) {

        List<Integer> buffer = new ArrayList<>();
        List<Integer> buffer2 = new ArrayList<>();
        int[] foodSeq = new int[food.length];

        for (int i = 0; i < food.length; i++) {
            foodSeq[i] = food[i] / 2;
        }
        for (int i = 0; i < foodSeq.length; i++) {
            for (int j = 0; j < foodSeq[i]; j++) {
                buffer.add(i);
                buffer2.add(i);
            }
        }

        buffer2.sort(Comparator.reverseOrder());
        buffer.add(foodSeq[0]);
        buffer.addAll(buffer2);

        StringBuilder answer = new StringBuilder();
        for (int i : buffer) {
            answer.append(i);
        }
        return answer.toString();
    }
}
  • 문제를 풀면서 상당히 코드 구조가 비효율 적이라고 생각했지만 매우 단순하고 원초적으로 동일한 자료구조를 2개를 만들어서 하나를 역순 정렬하려 합치는 방법으로 접근했다.
  • 먼저 주어진 음식의 개수인 food를 모두 동일하게 배치해야하기 때문에 짝수로 만들기 위해 각 요소를 나누기 2연산을하여 각 음식을 배치할 개수를 짝수 값으로 구해서 int[]에 저장했다
  • 이때 물은 무조건 1개가 들어오기 때문에 0으로 저장된다
  • 그 다음 2중 반복문을 통해서 foodSeq의 길이만큼 반복하는데 이게 준비한 음식의 번호(칼로리가 낮은 음식의 순서)이며 그 다음 반복으로 foodSeq[i]만큼 반복하여 음식의 개수만큼 음식의 번호가 준비한 2개의 버퍼에 담기도록 작성했다.
  • 모든 음식의 순서가 음식의 개수만큼 버퍼에 담기면 1개의 버퍼에는 buffer.add(foodSeq[0])로 음식 중간에 물을 배치한다.
  • 그다음 또다른 버퍼를 역순 정렬한 buffer.addAll(buffer2)로 물 다음에 음식 순서를 붙이면 List로 대회에 필요한 음식과 물의 배치가 끝난다.
  • 그다음 List에 담긴 순서를 문자열로 반환하기 위해 StringBuilder를 생성하여 List의 요소를 하나씩 꺼내서 StringBuilder에 추가하고 이후 String으로 변환하여 문제를 해결했다.

문제점 개선 풀이

import java.util.*;

class Solution {
    public String solution(int[] food) {
        StringBuilder sb = new StringBuilder();

        for (int i = 1; i < food.length; i++) {
            int count = food[i] / 2;
            sb.append(String.valueOf(i).repeat(count));
        }

        String result = sb.toString();
        sb.append("0");

        sb.append(new StringBuilder(result).reverse());

        return sb.toString();
    }
}
  • 여기서 아무리 제약조건이 food[0]이 항상 1이라고 해도 foodSeq[0]을 계산하는 과정과 foodSeq배열 자체가 불필요하며 buffer를 두개를 사용하는 것은 메모리 낭비라는 것을 알았다.
  • 우선 food[0]을 따로 처리하지 않아도 어차피 물은 0이기 때문에 구해진 음식 뒤에 0을 추가하기만 하면 된다.
  • 여러 반복을 할 필요 없이 StringBuilder를 생성한 다음 1번에 반복으로 food[i] / 2로 배치할 음식을 짝수개로 구한다.
  • 그 다음 sb.append()로 음식의 번호를 저장하는데, repeat(count)로 count의 개수만큼 음식의 번호를 문자열로 저장한다
    • repeat(): Java 11에서 제공되는 String 클래스의 메서드로 문자열을 주어진 횟수만큼 반복해서 새로운 문자열을 생성한다
  • StringBuilder에 담은 문자열을 새로운 String으로 변환해둔 다음 StringBuilder에 0을 추가하여 물을 추가해준다
  • 그 다음 String으로 변환해둔 문자열을 물까지 추가한 StringBuilder의 뒤에 new StringBuilder(result)로 다시 추가해주는데 이때 .reverse()를 사용해서 뒤집어서 추가해주고 String으로 변환하여 완성된 문자열을 반환한다.

다른 풀이

class Solution {
    public String solution(int[] food) {
        String answer = "0";

        for (int i = food.length - 1; i > 0; i--) {
            for (int j = 0; j < food[i] / 2; j++) {
                answer = i + answer + i; 
            }
        }
        return answer;
    }
}
  • 매우 간단한 문법으로 깔끔하게 문제를 해결할 수 있는 방법을 생각했다는 것이 놀라워서 가져왔다.
  • 먼저 문자열 answer 변수에 물을 뜻하는 "0"을 저장하고 2중 반복문을 돌린다.
  • 이때 처음 반복문의 i는 food.length -1 로 food의 마지막 인덱스부터 1씩 감소한다
  • 두 번째 반복문의 j는 0부터 시작하고 food[i] / 2 만큼 반복하는데 배치할 음식의 개수만큼 반복할 수 있도록 구성한다.
  • 그다음 수행할 로직으로 answer의 양쪽으로 i(음식의 번호)를 j(음식의 개수)만큼 추가시켜주면 매우 깔끔하게 문제를 해결할 수 있다.

콜라 문제

문제

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/132267
  • 빈 병 a개를 가져다 주면 콜라 b병을 주는 마트가 있을 때 빈 병 n개를 가져다 주면 몇 병을 받을 수 있는지 계산하는 문제
  • 보유 중인 빈 병이 a개 미만이면 추가적인 빈 병을 받을 수 없음

제한조건

  • 1 <=  b < a <= n <= 1,000,000
  • 정답은 항상 int 범위를 넘지 않게 주어짐

입출력 예시

나의 풀이

class Solution {
    public int solution(int a, int b, int n) {
        int answer = 0;

        while (n >= a) {
            
            int newCola = (n / a) * b;
            answer += newCola;
            n = newCola + (n % a);
            
        }
        
        return answer;
    }
}
  • 처음에는 n % 2 == 0의 조건으로 분기를 나누어 코드를 작성했지만 오히려 이 분기가 필요없이 단순한 연산으로 문제를 해결할 수 있다는 것을 알았다.
  • 빈 병 n을 a로 나누게 되면 새로 콜라를 받을 수 있는 횟수가 나오게 된다
  • 빈 병 a개를 가져다 주면 b개를 주기 때문에 b / a * b 는 새로 콜라를 받는 개수가 되기 때문에 이 개수를 반복문 안에서 누적연산하면 된다.
  • 빈 병 n이 a를 나누었을 때 나머지는 새로 받은 콜라를 다 마시고 합쳐서 다시 반복한다.
  • 이 때 빈 병의 개수가 a보다 작게되면 콜라를 받을 수 없기 때문에 반복을 중지한다

다른 풀이

class Solution {
    public int solution(int a, int b, int n) {
        return (n > b ? n - b : 0) / (a - b) * b;
    }
}
  • 반복문이 없이 단순히 수학적으로 문제를 푼 방식이 너무 신기하고 수학을 잘 모르는 나로서는 생각하기 어려운 방식이지만 한번쯤 풀이해보면 좋을 것 같다고 생각하여 가져왔다.
  • 다만 직관성이나 가독면 성에서는 반복문 풀이가 더 이해하기 쉬울 수 있어 실제 프로그래밍에서의 사용은 고민이 필요할 것 같다
  • (n > b ? n - b : 0)
    • n이 b보다 큰 경우에는 n - b를, 그렇지 않으면 0을 사용했는데 교환이 한 번이라도 이뤄지려면 n이 최소 b + 1이상이여야 한다는 것을 나타낸다.
    • 문제의 기본 제한으로 b < a <= n이기 때문에 n < b인 상황은 교화 자체가 불가능한 것을 처리한 삼항 연산자이다.
  • / (a - b)
    • 한 번 교환할 때마다 실제로 소모되는 빈 병의 개수이다
    • 이 개수를 삼항 연산자의 조건이 true일 때 연산되는 n - b 값과 나누어서 실제 교환이 가능한 총 횟수를 구한다
    • 처음에 n개의 빈 병중에서 마지막 단계 직전까지는 최소한 b개는 남아 있어야 한 번의 교환을 진행할 수 있다고 보고 n - b만큼을 뺀뒤 한 번 교환 시 줄어드는 양(a - b)으로 나눈 것이다
  • ... * b
    • 실제 교환이 가능한 총 횟수에 교환 한번에 받을 수 있는 새 콜라 수를 곱해서 최종적으로 얻을 수 있는 병의 총 수를 계산한다

SQL(MySQL)

식품분류별 가장 비싼 식품의 정보 조회하기

문제 및 테이블 예시

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/131116
  • FOOD_PRODUCT 테이블에서 식품분류별로 가격이 제일 비싼 식품의 분류, 가격, 이름을 조회하는 SQL 문을 작성
  • 식품분류는 '과자', '국', '김치', '식용유'인 경우만 출력
  • 결과는 식품 가격을 기준으로 내림차순 정렬

입출력 예시

나의 풀이

WITH, INNER JOIN, GROUP BY

WITH MAX_PRICE AS (
    SELECT 
        CATEGORY, MAX(PRICE) AS MAX_PRICE
    FROM FOOD_PRODUCT
    GROUP BY CATEGORY
)
SELECT 
    FD.CATEGORY, 
    FD.PRICE AS MAX_PRICE,
    FD.PRODUCT_NAME
FROM FOOD_PRODUCT AS FD
JOIN MAX_PRICE AS MP
ON FD.CATEGORY = MP.CATEGORY AND FD.PRICE = MP.MAX_PRICE
WHERE FD.CATEGORY IN ('과자','국','식용유','김치')
ORDER BY MAX_PRICE DESC
  • 저번 SQL 풀이에서 기억을 되찾아서 알게된 WITH를 사용하여 서브 쿼리를 설정하고 쿼리를 작성했다.
  • 먼저 WITH를 통해 GROUP BY를 걸어서 CATEGORY별 최대 가격을 조회하는 쿼리를 만들었다.
  • 그 다음 메인 쿼리에서 이 서브 쿼리의 결과와 INNER 조인을 하는데 이때 카테고리와, 가격을 서로 매칭시켜서 카테고리별 최대 금액이 매칭되도록 한다음 조회할 컬럼들만 SELECT 절에 입력한다.
  • 그 다음 조건 요구사항을 위해 WHERE 절에 IN을 사용하여 과자, 국, 식용유, 김치만 조회하도록 하고 최대 가격을 내림차순으로 정렬하여 최종 결과를 반환했다
  • 메인 쿼리의 WHERE 절을 서브쿼리에서 GROUP BY를 할 때 HAVING을 사용하여 조건을 지정해도 된다

다른 풀이

ROW_NUMBER()

SELECT CATEGORY,
       PRODUCT_NAME,
       PRICE
FROM (
    SELECT CATEGORY,
           PRODUCT_NAME,
           PRICE,
           ROW_NUMBER() OVER(PARTITION BY CATEGORY ORDER BY PRICE DESC) AS RN
    FROM FOOD_PRODUCT
    WHERE CATEGORY IN ('과자', '국', '식용유', '김치')
) T
WHERE RN = 1
ORDER BY PRICE DESC;
  • 비슷한 풀이를 할 때 윈도우 함수인 ROW_NUMBER()를 활용할 수 있다는 것도 항상 함께 생각해보면 상황에 따라 쿼리를 최적화 할 수 있지 않을까 생각된다.

WITH ORDERS AS (
    SELECT PRODUCT_ID,
        SUM(AMOUNT) AS AMOUNT
    FROM FOOD_ORDER
    WHERE PRODUCE_DATE LIKE '2022-05%'
    GROUP BY PRODUCT_ID
)

SELECT
    FP.PRODUCT_ID,
    FP.PRODUCT_NAME,
    FP.PRICE * O.AMOUNT AS TOTAL_SALES
FROM FOOD_PRODUCT AS FP
JOIN ORDERS AS O
ON FP.PRODUCT_ID = O.PRODUCT_ID
ORDER BY TOTAL_SALES DESC, FP.PRODUCT_ID

5월 식품들의 총매출 조회하기

문제 및 테이블 예시

  • 프로그래머스 - https://school.programmers.co.kr/learn/courses/30/lessons/131117
  • FOOD_PRODUCT와 FOOD_ORDER 테이블에서 생산일자가 2022년 5월인 식품들의 식품 ID, 식품 이름, 총매출을 조회하는 SQL문 작성
  • 결과는 총매출을 기준으로 내림차순 정렬하고 총매출이 같다면 식품 ID를 기준으로 오름차순 정렬

 

입출력 예시

나의 풀이

WITH, INNER JOIN, GROUP BY

WITH ORDERS AS (
    SELECT PRODUCT_ID,
        SUM(AMOUNT) AS AMOUNT
    FROM FOOD_ORDER
    WHERE PRODUCE_DATE LIKE '2022-05%'
    GROUP BY PRODUCT_ID
)

SELECT
    FP.PRODUCT_ID,
    FP.PRODUCT_NAME,
    FP.PRICE * O.AMOUNT AS TOTAL_SALES
FROM FOOD_PRODUCT AS FP
JOIN ORDERS AS O
ON FP.PRODUCT_ID = O.PRODUCT_ID
ORDER BY TOTAL_SALES DESC, FP.PRODUCT_ID
  • 처음에는 FOOD_PRODUCT테이블과 FOOD_ORDER테이블을 조인해서 문제를 해결해보려고 시도했는데 쿼리문을 작성하다가 문제를 잘못 파악하여 문제를 다시 읽고 위와 동일한 방법으로 문제를 해결했다
  • FOOD_ORDER의 PRODUCT_ID 별로 그룹화를 진행할 때 생산일자가 2022년 5월인 상품들로 그룹화를 한 다음 주문 수량을 모두 묶는 쿼리를 WITH 문을 활용해 ORDERS 쿼리를 정의했다.
  • 그 다음 메인 쿼리에서 FOOD_PRODUCT 테이블과 두 테이블의 PRODUCT_ID가 같은 값끼리 INNER JOIN을 진행하여 테이블을 묶었는데 여기서 FOOD_PRODUCT 테이블의 PRICE와 ORDERS 테이블의 수량을 곱해서 매출액을 구했다
  • 이후 문제 요구사항을 맞추기 위해 ORDEY BY를 활용하여 매출을 기준으로 내림차순하고 매출액이 같으면 PRODUCT_ID를 오름차순으로 결과를 정렬했다.

다른 풀이

SELECT
    o.PRODUCT_ID,
    p.PRODUCT_NAME,
    SUM(o.AMOUNT * p.PRICE) AS TOTAL_SALES
FROM
    FOOD_PRODUCT p INNER JOIN FOOD_ORDER o
    ON p.PRODUCT_ID = o.PRODUCT_ID
WHERE
    o.PRODUCE_DATE REGEXP '2022-05'
GROUP BY
    o.PRODUCT_ID
ORDER BY
    TOTAL_SALES DESC,
    PRODUCT_ID ASC;
  • 이 방법이 내가 원래 접근해보려고 했던 방식이었는데 WITH문 없이도 GROUP BY를 FOOD_ORDER의 PRODUCT_ID를 묶어서 바로 메인쿼리에서 FOOD_ORDER와 FOOD_PRODUCT의 가격을 곱해서 구한 매출액을 SUM()으로 합계를 구하면 PRODUCT_ID 별 매출액을 구할 수 있었다
  • 여기서 WHERE절에서 조회할 날짜를 지정하고 정렬 로직도 맞춰 주면 2022년 5월의 상품별 매출액을 요구사항의 정렬을 기준으로 깔끔하고 명확하게 구현할 수 있다.
  • 이 방법을 통해서 GROUP BY를 조금 더 잘 쓸 수 있을 것 같다.
  • 다만 여기서 REGEXP 대신 LIKE나 BETWEEN을 사용하는 것이 더 성능과 가독성 면에서 더 적절하고 MySQL에서는 문제가 없을 수 있지만 SQL 표준에서는 SELECT 절에있는 모든 비집계 컬럼이 GROUP BY에 포함되어야 하기 때문에 PRODUCT_NAME도 포함시켜주어야 한다.
728x90