[题解] P1450 [HAOI2008] 硬币购物
P1450 [HAOI2008] 硬币购物
一个非常巧妙的背包 dp。
不难发现,题目相当于给了一个 4 个物品的多重背包,要我们求出装背包的方案数。
然而不幸的是,观察数据范围可知,直接跑多重背包会 TLE,即使用上了最强有力的单调队列优化,也无法通过。
那怎么做?我们发现,如果没有硬币的限制,我们直接预处理一个完全背包就行了。
那要是有了硬币的限制怎么办?
我们先从简单入手,如果只有第一种硬币限制最多放 \(c_1\) 个怎么做。
正难则反,正着不好做,我们考虑反其道而行,求出不合法的数量。
我们只需要先往背包里放 \(c_1+1\) 个第一种硬币,这样一定满足“不合法”的要求,剩下的跑完全背包即可。
设完全背包的状态数组(压维后)为 \(f[i]\) ,则不合法的数量即为 \(f[m-w_1\times (c_1+1)]\) ,合法的数量就是 \(f[m]-f[m-w_1 \times (c_1+1)]\)。
现在,我们考虑一般情况,即原题。
依旧正难则反,设 \(S_i\) 表示考虑第 \(i\) 种硬币时不合法的集合,那不合法的总方案数就是 \(|S_1 \or S_2 \or S_3 \or S_4|\)。
由容斥原理可得:
\[\begin{aligned} |S_1 \or S_2 \or S_3 \or S_4| = \sum_{i} |S_i|- \sum_{i,j} |S_i \and S_j | + \sum_{i,j,k} |S_i \and S_j \and S_k| - \sum_{i,j,k,l} |S_i \and S_j \and S_k \and S_l| \end{aligned}
\]
交集的求解也很简单。以 \(|S_1 \and S_2 \and S_3|\) 为例,其不合法的答案为 \(f[m-w_1 \times (c_1+1)-m-w_2 \times (c_2+1)-m-w_3 \times (c_3+1)]\)。
在实现上,我们使用二进制状压枚举,统计二进制状态中 1 的个数,如果个数为奇数就加上当前答案,如果为偶数就减去当前答案。
于是我们便可以快速求解不合法的方案数,不要忘了最后还要用 \(f[m]\) 去减哦~
#include<bits/stdc++.h>
#define int long long// 不开__见祖宗
using namespace std;
const int N=1e5+10, mod=998244353;
int f[N], w[N], c[N];
int n=4,m,ans;
inline void solve(){
for(int i=1; i<=n; i++) cin>>c[i];
cin>>m;
ans=0;
for(int i=1; i<(1<<n); i++){
int cnt=0, now=m;
for(int j=1; j<=n; j++){
if(i>>(j-1)&1){
now-=w[j]*(c[j]+1);
cnt++;
}
}
if(now<0) continue;// 排除放不下的情况,避免RE
if(cnt&1) ans+=f[now];// 奇加偶减
else ans-=f[now];
}
cout<<f[m]-ans<<"\n";// ans为不合法的方案数
}
signed main(){
cin.tie(0)->sync_with_stdio(0);
for(int i=1; i<=n; i++) cin>>w[i];
f[0]=1;
for(int i=1; i<=n; i++)
for(int j=w[i]; j<N; j++)
f[j]+=f[j-w[i]];
int t; cin>>t;
while(t--) solve();
return 0;
}

浙公网安备 33010602011771号