BOJ 1106 - 호텔

문제 요약

고객을 최소 C명 이상 늘려야 한다. N개의 도시가 있고, 각 도시에서 홍보하면 일정 비용으로 일정 수의 고객이 늘어난다. 같은 도시에서 여러 번 홍보할 수 있다. 최소 비용을 구하라.

(GTA의 카지노 홍보 미션이 떠오른다. 버자드 타고 전단지 뿌렸던것 같은데..)


처음 접근 — 2D DP (2차원)

처음엔 자연스럽게 2D DP로 접근했다.

dp[i][c] = i번 도시까지 고려했을 때, 고객 c명을 늘리는 최소 비용

익숙한 배낭 문제 구조처럼 보였기 때문이다. 보통 배낭 문제는 dp[i][w] — i번째 물건까지 고려했을 때 무게 w 이하에서의 최대 가치 — 형태로 풀었던 전적이 있기 때문이다.

내 최초 코드

#include<iostream>
#include<bits/stdc++.h>
using namespace std;
int main()
{
    int C, N;
    cin >> C >> N;
    
    vector<int> money(N+1);
    vector<int> customer(N+1);
    for(int i = 0; i < N; i++){
        int m, c;
        cin >> money[i] >> customer[i];
    }
    vector<vector<int>> dp(N, vector<int>(C+1, 0));
    for(int i = 0; i < N; i++){
        for(int c = 1; c <= C; c++){
            if(i == 0){
                dp[i][c] = ((c + customer[i]-1) / customer[i]) * money[i];
            }
            else{
                if(c - customer[i] >= 1){
                    dp[i][c] = min({dp[i-1][c], (((c + customer[i]-1) / customer[i]) * money[i]),dp[i][c-customer[i]] + money[i]});
                }
                else{
                    dp[i][c] = min(dp[i-1][c], (((c + customer[i]-1) / customer[i]) * money[i]));
                }
            }
        }
    }
    
    cout << dp[N-1][C];
    
    return 0;
}

AC는 났다. (뽀록)
하지만 코드를 다시 보면 조건 분기가 지저분하고, 올림 나눗셈 (c + customer[i]-1) / customer[i]으로 엣지 케이스를 우회한 흔적이 역력하다. (올림하는 식임)
뭔가 억지로 맞춘 느낌.


왜 2D로 접근했을까?

"도시를 여러 번 쓸 수 있다"는 조건을 제대로 소화하지 못했기 때문이다.

일반적인 0/1 배낭 문제는 각 물건을 한 번씩만 쓸 수 있어서, "어디까지 고려했는가"를 추적하는 i 차원이 필요하다. 그 패턴에 익숙하다 보니 이 문제도 반사적으로 2D로 짰다.

하지만 이 문제는 unbounded knapsack — 같은 도시를 몇 번이든 쓸 수 있다. 이 경우 "어떤 도시까지 고려했냐"는 차원 자체가 필요 없다. 고객 수만 추적하면 충분하다.

핵심 질문: 이 아이템을 여러 번 쓸 수 있는가?

  • Yes → 1D DP (unbounded knapsack)
  • No → 2D DP (0/1 knapsack)

배낭 문제 (Knapsack) 소개

배낭 문제는 DP의 대표적인 유형이다. 기본 구조는 이렇다.

용량 W인 배낭에 N개의 물건을 담을 때, 가치를 최대화하라.

0/1 Knapsack

각 물건을 최대 한 번만 쓸 수 있다.

dp[i][w] = i번째 물건까지 고려했을 때, 무게 w 이하에서의 최대 가치

물건을 중복 사용하지 못하므로 i 차원으로 "어디까지 봤는가"를 추적한다.

Unbounded Knapsack

각 물건을 횟수 제한 없이 쓸 수 있다.

dp[w] = 무게 w를 채우는 최적값

i 차원이 필요 없다. 모든 아이템을 매번 다시 고려하면 된다.

이번 호텔 문제는 unbounded knapsack의 최소 비용 버전이다.


개선된 접근 — 1D DP

dp[i] = 고객을 i명 늘리는 최소 비용
dp[0] = 0  (아무것도 안 해도 0명 늘리는 비용은 0)

점화식:

dp[i] = min(dp[i], dp[max(0, i - customer[j])] + money[j])  for all j

max(0, i - customer[j])는 고객 증가량이 i를 초과할 때도 자연스럽게 처리해준다. dp[0] = 0이기 때문에 별도 분기 없이 처리된다.
(싫으면 그냥 0 기준으로 분기처리해도 됨.)

최종 코드

#include<bits/stdc++.h>
using namespace std;
int INF = 1e9;
int main()
{
    int C, N;
    cin >> C >> N;
    vector<int> money(N);
    vector<int> customer(N);
    for(int i = 0; i < N; i++){
        cin >> money[i] >> customer[i];
    }
    vector<int> dp(C+1, INF);
    dp[0] = 0;
    for(int i = 1; i <= C; i++){
        for(int j = 0; j < N; j++){
            dp[i] = min(dp[i], dp[max(0, i-customer[j])] + money[j]);
        }
    }
    cout << dp[C];
    
    return 0;
}

비교 정리

| 항목 | 최초 코드 | 최종 코드 | |------|-----------|-----------| | DP 차원 | 2D (N × C) | 1D (C) | | 공간복잡도 | O(NC) | O(C) | | 시간복잡도 | O(NC) | O(NC) | | 코드 복잡도 | 조건 분기 多 | 단순 | | 설계 명확성 | 낮음 | 높음 |

시간복잡도는 같지만, 공간과 코드 가독성에서 1D가 압도적으로 낫다.


회고

AC가 났다고 끝이 아니었다. 코드를 뜯어보니 내가 문제의 핵심 조건 — 중복 사용 가능 — 을 제대로 소화하지 않고, 익숙한 2D 패턴에 억지로 끼워 맞췄다는 게 보였다.

배낭 문제에서 아이템을 여러 번 쓸 수 있는지 없는지를 먼저 확인하는 습관을 들이자. 그게 DP 차원 설계의 출발점이다.