[HAOI2008] 동전 구매 - 완전 배낭과 포함-배제 원리

문제 설명

4가지 종류의 동전이 있으며 각각의 액면가는 \(c_1, c_2, c_3, c_4\)입니다.

어떤 사람이 가게에서 물건을 \(n\)번 구매합니다. 각 구매마다 \(i\)번째 동전을 \(d_i\)개씩 가지고 있으며, 총 \(s\) 가치의 물건을 사려고 합니다. 각 구매에 대해 지불 방법의 수를 구하세요.

입력 형식

첫 번째 줄에는 다섯 개의 정수 \(c_1, c_2, c_3, c_4, n\)이 주어집니다.

다음 \(n\)개의 줄에는 각각 다섯 개의 정수 \(d_1, d_2, d_3, d_4, s\)가 주어집니다.

출력 형식

각 구매에 대해 답을 한 줄에 하나씩 출력합니다.

예제 #1

예제 입력 #1

1 2 5 10 2
3 2 3 1 10
1000 2 2 2 900

예제 출력 #1

4
27

제약 조건

  • \(1 \leq c_i, d_i, s \leq 10^5\)
  • \(1 \leq n \leq 1000\)

분석

이 문제는 여러 동전으로 특정 금액을 만드는 방법의 수를 구하는 전형적인 다중 배낭 문제로 보이지만, 각 동전의 사용 개수에 제한이 있고 쿼리 횟수가 많습니다. 단순 다중 배낭으로 풀면 시간 복잡도가 \(O(4 \cdot n \cdot s)\)로 매우 커져 비효율적입니다. 따라서 완전 배낭과 포함-배제 원리를 사용하는 다른 접근 방식이 필요합니다.

완전 배낭 + 포함-배제 원리

먼저 각 동전의 개수 제한을 무시하고, 모든 동전을 무한히 사용할 수 있다고 가정합니다. 이때 금액 \(j\)를 만드는 방법의 수를 \(f[j]\)라고 정의합니다. 완전 배낭 점화식은 다음과 같습니다.

f[0] = 1
for i in 1..4:
    for j in c[i] to MAX_SUM:
        f[j] += f[j - c[i]]

이제 개수 제한을 고려합니다. 예를 들어 첫 번째 동전을 \(d_1\)개까지만 사용해야 한다면, \(d_1+1\)개 이상 사용하는 경우는 불가능합니다. \(f[s]\)는 무제한 경우이므로, 첫 번째 동전을 \(d_1+1\)개 이상 사용한 경우를 빼야 합니다. 이는 \(s - (d_1+1) \cdot c_1\) 금액을 남은 동전(무제한)으로 만드는 방법의 수와 같습니다.

하지만 여러 동전의 제한을 동시에 고려할 때는 각 제한을 위반한 경우가 겹칠 수 있습니다. 따라서 포함-배제 원리를 적용해야 합니다. 각 동전 \(i\)에 대해 개수 제한을 위반한 사건을 \(A_i\)라고 하면, 전체 경우의 수는 \(f[s]\)이고 우리가 원하는 경우의 수는 \(f[s]\)에서 모든 \(A_i\)의 합집합을 뺀 값입니다.

포함-배제 원리에 따라, 4개의 동전에 대한 최종 답은 다음과 같습니다.

ans = f[s]
    - (A1 + A2 + A3 + A4)
    + (A1∩A2 + A1∩A3 + A1∩A4 + A2∩A3 + A2∩A4 + A3∩A4)
    - (A1∩A2∩A3 + A1∩A2∩A4 + A1∩A3∩A4 + A2∩A3∩A4)
    + (A1∩A2∩A3∩A4)

여기서 각 교집합의 크기는 해당 동전들을 모두 \(d_i+1\)개 이상 사용한 경우의 수를 의미합니다. 예를 들어 \(A_1 \cap A_2\)의 크기는 \(f[s - (d_1+1)\cdot c_1 - (d_2+1)\cdot c_2]\)입니다.

비트마스킹을 사용하여 0부터 15까지의 모든 부분집합을 순회하며, 각 부분집합에 포함된 동전의 수가 짝수이면 더하고 홀수이면 빼는 방식으로 포함-배제를 구현할 수 있습니다.

코드 (C++)

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

const int MAX_VAL = 100005;
ll f[MAX_VAL];
int coin[5], limit[5];

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

    for (int i = 1; i <= 4; i++) cin >> coin[i];
    
    // 완전 배낭: 무제한 동전으로 금액 만드는 경우의 수
    f[0] = 1;
    for (int i = 1; i <= 4; i++) {
        for (int j = coin[i]; j < MAX_VAL; j++) {
            f[j] += f[j - coin[i]];
        }
    }

    int q;
    cin >> q;
    while (q--) {
        for (int i = 1; i <= 4; i++) cin >> limit[i];
        int target;
        cin >> target;
        ll ans = 0;

        // 0부터 15까지 모든 부분집합 순회
        for (int mask = 0; mask < (1 << 4); mask++) {
            int cnt = 0;
            ll deduction = 0;
            for (int i = 1; i <= 4; i++) {
                if (mask & (1 << (i - 1))) {
                    cnt++;
                    deduction += (ll)(limit[i] + 1) * coin[i];
                }
            }
            if (target >= deduction) {
                if (cnt & 1)
                    ans -= f[target - deduction];
                else
                    ans += f[target - deduction];
            }
        }
        cout << ans << '\n';
    }
    return 0;
}

태그: 완전배낭 포함-배제원리 비트마스킹 다이나믹프로그래밍 조합론

6월 3일 00:38에 게시됨