【题解】「HAOI2008」硬币购物(dp 优化、容斥)

「HAOI2008」硬币购物

题面

共有 4 种硬币。面值分别为 \(c_1,c_2,c_3,c_4\)

某人去商店买东西,去了 \(n\) 次,对于每次购买,他带了 \(d_i\) 枚第 \(i\) 种硬币,想购买价值和为 \(s\) 的东西。

请问每次有多少种付款方法。

\(1\le c_i,d_i,s\le10^5,1\le n\le 1000\)

题解一

我们一开始会想到完全背包,但是发现它会超时。

一开始的想法:设 \(dp[i, j]\) 表示前 \(i\) 种硬币,购买了价值为 \(j\) 的物品,方案数为多少。

对于每一次购买,我们的转移代码大致如下

memset(dp, 0, sizeof(dp));
dp[0][0] = 1;
for(int i = 1; i <= 4; ++i) { // 枚举物品 
    for(int j = 0; j <= s; ++j) { // 枚举购买价值
        for(int k = 0; k <= d[i]; ++k) { // 枚举当前物品买几个
            if(j - k * c[i] >= 0) {
                dp[i][j] += dp[i - 1][j - k * c[i]];
            }
        }
    }
}
cout << dp[4][s] << "\n";

我们仔细模拟其中 \(k\) 所在层所做的事,也就是下面这段代码

for(int k = 0; k <= d[i]; ++k) {
    if(j - k * c[i] >= 0) {
        dp[i][j] += dp[i - 1][j - k * c[i]];
    }
}

我们发现,它实际是将 \(dp[i - 1, j]\)\(dp[i - 1, j - c[i]]\)\(dp[i - 1, j - 2c[i]]\)\(\dots dp[i - 1, j - k\cdot c[i]]\) 加到了一个值 \(dp[i, j]\) 上。

注意到,被加的这些项类似于一个前缀和,只不过这个“前缀和”是间隔 \(c[i]\) 加一次。并且,在 \(i\) 确定之后,这个前缀和只跟 \(j\)\(c[i]\) 有关。

所以我们可以预处理这个前缀和。具体地,在 \(i\) 确定时,设 \(sum[x]\) 表示 \(dp[i - 1, x - k\cdot c[i]]~~(k=1,2,3,\dots)\) 的和(变量为 \(k\)),那么 \(sum[x]=sum[x - c[i]] + dp[i - 1, x]\)

然后,之前 \(k\) 所在层就可以改写为

dp[i][j] = sum[j];

但是这样还是不对,这是因为原来的代码里面,\(k\) 有一个限制

k <= d[i];

所以应该改写为

// dp[i][j] = sum[j] - sum[j - (d[i] + 1) * c[i]]
// 但是j - (d[i] + 1) * c[i]可能会越界,所以要特判一下
dp[i][j] = sum[j];
if(j - (d[i] + 1) * c[i] >= 0) {
    dp[i][j] -= sum[j - (d[i] + 1) * c[i]];
}

然后这道题就做完了,每一次购买的时间复杂度是 \(O(4s)=O(s)\),总时间复杂度是 \(O(ns)\)

总结

这里的 dp 优化最关键的一步是预处理 \(sum\) 数组,这启示我们将多维 dp 的内层所求提前预处理,达到平衡复杂度的作用(从预处理\(O(0)\),计算\(O(n^2)\) 到预处理\(O(n)\),计算\(O(n)\)

代码

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int n, s, c[5], d[5];
i64 dp[5][100005], sum[100005];
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    for(int i = 1; i <= 4; ++i) {
        cin >> c[i];
    }
    cin >> n;
    for(int t = 1; t <= n; ++t) {
        for(int i = 1; i <= 4; ++i) {
            cin >> d[i];
        }
        cin >> s;
        memset(dp, 0, sizeof(dp));
        dp[0][0] = 1;
        for(int i = 1; i <= 4; ++i) {
            // sum[x]表示从x开始向前跳c[i],dp[i - 1, x - k * c[i]]的和 
            sum[0] = dp[i - 1][0];
            for(int j = 1; j <= s; ++j) { // 预处理sum[]数组 
                if(j >= c[i]) {
                    sum[j] = sum[j - c[i]] + dp[i - 1][j];
                } else {
                    sum[j] = dp[i - 1][j];
                }
            }
            for(int j = 0; j <= s; ++j) {
                dp[i][j] = sum[j];
                if(j - (d[i] + 1) * c[i] >= 0) {
                    dp[i][j] -= sum[j - (d[i] + 1) * c[i]];
                }
            }
        } 
        cout << dp[4][s] << "\n";
    }
    return 0;
}

题解二

先不考虑每次询问中 \(d_i\) 的限制,即在认为所有硬币都有无限个的前提下,预处理 \(dp[i]\) 表示买价值为 \(i\) 的物品的总方案数(完全背包)。

然后对于每次的 \(d_i\) 限制,考虑容斥。答案 $=dp[i]\ - $ 1种硬币超限的方案数 \(+\) 2种硬币超限的方案数 \(-\) 3种硬币超限的方案数 \(+\) 4种硬币超限的方案数。

考虑第 \(i\) 种硬币超限的方案数,我们直接钦定 \(d_i+1\) 个这种硬币被选,其他的随便选即可。因此方案数为 \(dp[n - c_i\times (d_i+1)]\)

如果计算多个硬币同时超限的方案数,类似地可以得出方案数为 \(dp[n-c_i\times(d_i+1)-c_j\times(d_j+1)-\dots]\)

枚举哪些硬币超限的二进制集合 \(sta\),然后根据判断是 \(+\) 还是 \(-\) 上该状态的方案数即可。

时间复杂度 \(O(4s+4\cdot 2^4\cdot n)\)

代码

#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
const int S = 1E5 + 5;
int n, s, c[5], d[5];
i64 dp[S];
int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    for(int i = 0; i < 4; ++i) {
        cin >> c[i];
    }
    cin >> n;
    dp[0] = 1;
    for(int i = 0; i < 4; ++i) {
        for(int j = c[i]; j < S; ++j) {
            dp[j] += dp[j - c[i]]; // 完全背包 
        }
    }
    while(n--) {
        for(int i = 0; i < 4; ++i) {
            cin >> d[i];
        }
        cin >> s;
        i64 ans = dp[s];
        for(int sta = 1; sta < (1 << 4); ++sta) { // sta != 0
            int cnt = 0, now = s;
            for(int i = 0; i < 4; ++i) {
                if((sta >> i) & 1) {
                    ++cnt;
                    now -= (d[i] + 1) * c[i];
                }
            }
            if(now >= 0) {
                if(cnt % 2 == 0) {
                    ans += dp[now];
                } else {
                    ans -= dp[now];
                }
            }
        }
        cout << ans << "\n";
    }
    return 0;
}
posted @ 2022-05-03 21:30  hzy1  阅读(126)  评论(0编辑  收藏  举报