← 문제 풀이 목록

백준 2156: 포도주 시식

/ 6분 분량 / 문제 풀이

Silver I 난이도의 동적 프로그래밍 문제를 C++로 풀이한 내용입니다. n개의 포도주 잔이 일렬로 놓여있을 때, 연속 3잔을 마시지 않는 제약 조건 하에서 최대한 많은 포도주를 마시는 문제입니다.

백준 2156: 포도주 시식

Silver I 난이도의 동적 프로그래밍 문제를 C++로 풀이한 내용입니다. n개의 포도주 잔이 일렬로 놓여있을 때, 연속 3잔을 마시지 않는 제약 조건 하에서 최대한 많은 포도주를 마시는 문제입니다.

문제 소개

이 문제는 다음과 같은 조건을 만족하며 최대 포도주 양을 구하는 것입니다:

  • n개의 포도주 잔이 순서대로 놓여있음
  • 각 잔은 마시거나 마시지 않을 수 있음
  • 제약: 연속된 3잔을 모두 마실 수 없음
  • 목표: 최대 포도주 양 구하기

예를 들어 n=3, 각 잔의 양이 6, 10, 5라면, 첫 번째와 두 번째만 마셔서 16을 얻습니다.

접근 방법

이 문제는 시간축 동적 프로그래밍으로 해결합니다. 핵심은 "연속 3잔 금지"라는 제약을 다르게 해석하는 것입니다.

논리적 변환: 각 위치 i에 대해, i번째 포도주를 포함한 최적 해를 구할 때, 마지막 3칸(i, i-1, i-2) 중 반드시 하나는 "마시지 않은 지점"이 존재해야 합니다.

이를 통해 모든 합법적인 경우를 정확히 3가지로 완전히 분해할 수 있습니다:

  1. Case 1: i번째를 마시지 않음 → dp[i-1]
  2. Case 2: i-1번째를 마시지 않음 → dp[i-2] + wine[i]
  3. Case 3: i-2번째를 마시지 않음 → dp[i-3] + wine[i-1] + wine[i]

이 3가지 외에는 존재 불가능합니다(세 잔을 모두 마시면 연속 3잔 위반).

풀이 과정

1단계: 입력 및 초기화

n개의 포도주 양을 입력받고, dp 배열을 초기화합니다.

2단계: 기저 사례 설정

  • dp[1] = wine[1] (첫 잔만 마심)
  • dp[2] = wine[1] + wine[2] (첫 두 잔을 모두 마심)

3단계: 점화식 적용

i = 3부터 n까지 위의 3가지 경우의 최댓값을 선택합니다.

4단계: 답 출력

dp[n]이 최종 답입니다.

코드 설명

#include <bits/stdc++.h>
using namespace std;

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;

    // wine[i] = i번째 포도주의 양 (1-indexed)
    vector<int> wine(n+1);
    for (int i = 1; i <= n; i++) {
        cin >> wine[i];
    }

    // dp[i] = 1 ~ i번째 포도주까지 고려했을 때 얻을 수 있는 최대 양
    vector<int> dp(n+1, 0);

    // 기저값
    if (n >= 1) dp[1] = wine[1];
    if (n >= 2) dp[2] = wine[1] + wine[2];

    /*
        핵심 아이디어 (시간축 DP):

        "연속 3잔 금지"라는 제약은 논리적으로:
        -> 마지막 3칸(i, i-1, i-2) 중
           반드시 하나는 '안 마신 지점'이 있어야 한다.

        즉, 합법적인 모든 경우는
        "마지막으로 안 마신 시점"이 어디냐로
        딱 3가지로 완전 분해된다.

        Case 1: i번째를 안 마신다
            패턴: ... - _
            dp[i-1]

        Case 2: i-1번째를 안 마신다
            패턴: ... - _ -
            dp[i-2] + wine[i]

        Case 3: i-2번째를 안 마신다
            패턴: ... - _ - -
            dp[i-3] + wine[i-1] + wine[i]

        이 3개 말고는 존재 불가능.
        (i, i-1, i-2 전부 마시면 3연속 위반)
    */

    for (int i = 3; i <= n; i++) {
        dp[i] = max({
            dp[i-1],                              // Case 1: 이번에 안 마심
            dp[i-2] + wine[i],                    // Case 2: i-1에서 끊김
            dp[i-3] + wine[i-1] + wine[i]         // Case 3: i-2에서 끊김
        });
    }

    cout << dp[n];
}

주요 부분 설명:

  • 벡터 초기화: 1-indexed 벡터를 사용하여 포도주 번호와 배열 인덱스를 일치시킵니다.
  • 기저값 설정: n=1, 2인 경우를 미리 처리하여 인덱스 오류를 방지합니다.
  • 점화식: max() 함수로 3가지 경우를 비교하여 최댓값을 선택합니다.
  • 빠른 입출력: ios::sync_with_stdio(false) 사용으로 성능을 최적화합니다.

복잡도 분석

시간 복잡도: O(n)

  • 단일 반복문이 n번 실행되고, 각 반복에서 상수 시간 연산만 수행합니다.

공간 복잡도: O(n)

  • dp 배열과 wine 배열 각각 O(n) 공간을 사용합니다.
  • 공간 최적화: 실제로는 직전 3개의 값만 필요하므로 O(1)로 줄일 수 있습니다.

배운 점

  1. 제약 조건의 논리적 변환: "연속 3잔 금지"를 "마지막 3칸 중 하나는 안 마신다"로 재해석하면 점화식 도출이 명확해집니다.

  2. 상태 정의의 중요성: dp[i]를 "i번째까지 고려했을 때의 최댓값"으로 정의하면, 각 단계에서 독립적으로 최적 선택을 할 수 있습니다.

  3. 완전 분해: 복잡한 제약 조건도 논리적으로 완전히 분해하면 작은 경우들의 합으로 표현 가능합니다. 이는 DP 문제 해결의 핵심 전략입니다.

  4. 다른 문제에의 응용: "최대 k개 연속 사용 금지" 패턴의 문제들에 이 접근법을 적용할 수 있습니다.