题解 P1450 【[HAOI2008]硬币购物】
I_AM_HelloWord · · 题解
好巧妙的一道dp题!
如果我们就赤裸裸的多重背包那么就是O(10^5*10^5*1000)
一周的时间都运行不完(手动滑稽)!
那么怎么办呢?如果没有硬币数量的限制那就多好啊?直接一个完全背包预处理,然后O(1)输出就好了
可是有了硬币的限制怎么办?我们先考虑一个简单一点的情况:只有第一个硬币有限制。
如果我们用类似前缀和的思想(术语叫差分),我们先完全背包预处理好无限制的情况,拿dp[tot]减去dp[tot-c[i]*(d[i]+1)]就是我们所需的方案数。
这是为什么呢?为什么要弄个c[i]*(d[i]+1)?其实我们可以这样想,无限制的情况就是没有那个di,而有限制时,不应该计入答案的方案数就是把c[i]这个硬币取了超过d[i]次,对吧?那么我们手动先取出d[i]+1个c[i]的硬币,然后剩下的价值弄个完全背包,这时就是我们所不需要的答案, 把它减掉就行了。
那么对于4个(或更多)的硬币有限制,我们就逐一把4个硬币单独限制的方案数减掉,这时可能会减重了(即同时两个硬币有限制的情况减了两次),所以我们再把4个硬币两两同时限制的方案数加上,可能又加重了,再把4个硬币33同时限制减掉,最后加上4个同时限制的方案数就是我们所需的答案。这就是大名鼎鼎的容斥原理啊!写成位运算就很优美了!
方案数会爆int哟。
参考代码(和楼下不太一样,0表示满足限制,1表示不满足限制):
#include<cstdio>
#include<algorithm>
#include<cstring>
#define REP(i,a,b) for (int i=(a);i<=(b);i++)
#define in(a) scanf("%d",&(a))
using namespace std;
const int N=100001;
long long dp[N+10];
int c[5],d[5];
int main(){
REP(i,1,4)in(c[i]);
dp[0]=1;
REP(i,1,4)REP(j,c[i],N)dp[j]+=dp[j-c[i]];
int T;in(T);
while (T--){
int sum;long long res=0;
REP(i,1,4)in(d[i]);
in(sum);
REP(i,0,15){
long long t=sum;
int cnt=0;
REP(j,1,4)if ((i>>(j-1))&1)t-=c[j]*(d[j]+1),cnt^=1;
if (t<0)continue;
if (!cnt)res+=dp[t];else res-=dp[t];
}
printf("%lld\n",res);
}
return 0;
}