初见安~由于刚好近期bzoj炸了所以只有洛谷的传送门了:洛谷P1450 硬币购物
题目描述
硬币购物一共有4种硬币。面值分别为c1,c2,c3,c4。某人去商店买东西,去了tot次。每次带di枚ci硬币,买si的价值的东西。请问每次有多少种付款方法。
输入格式
第一行 c1,c2,c3,c4,tot 下面tot行 d1,d2,d3,d4,s
输出格式
每次的方法数
输入
1 2 5 10 2
3 2 3 1 10
1000 2 2 2 900
输出
4
27
说明/提示
di,s<=100000
tot<=1000
题解
看到硬币凑钱就是背包问题【大雾】。
本狸太菜了一开始还想用多重背包凑方案数……然后就发现不能用二进制拆分【如果可以用两次,会被拆成两个一次,然而是等价的】QuQ
除了多重背包还有什么?01和完全。哦那就尝试完全背包统计方案数吧——用表示凑出价值为i的方案数。可是就被次数限制卡住了。所以我们要从次数的限制入手思考怎么转化这个问题。如果只有一种硬币有次数限制,其他的都可以完全背包,要凑出价值sum的方案数我们可以直接用.。很明显选这一种硬币的次数大于等于l了就都不合法了,所以减去从小于等于转化过来的方案数即可,也就是。
那么对于多个硬币呢?很容易想到的是:直接挨个像上面这样减就行了吧?然而并不,你会发现答案小了很多甚至是负数。为什么?因为减去了重复的部分。所谓重复,就比如说是:第一个硬币减去的部分里面包含了第二种硬币的不合法情况。那么第二种硬币再直接减就减重了。既然两两重复的多减了那加回来呗,两个同时超出次数的方案数就是。可是如果还有三种硬币同时有交集的呢?又要减回去;四种呢?还要加回去。所以最后的方案数就是这样容斥出来的。
问题来了——怎么看有没有重复的?怎么枚举?一共就4种硬币打表是很容易的,也就种情况。有一种很巧妙的方法是用二进制位来枚举——枚举0~15,也就是2^4-1,如果二进制下某一位为1,那么我们就当它是要参与这一次枚举的 容斥的。看它是加回去还是减下去,统计一下有多少个数就可以了。当然,也要判断一下合法性——如果sum减下来是个负数,那就很明显不会有重复的部分了。
过于巧妙了呢……到底是哪一部分重合困扰了我很久。
上代码——
#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 100005
using namespace std;
typedef long long ll;
int read() {
int x = 0, f = 1, ch = getchar();
while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
return x * f;
}
int c[5], T, d[5], op;
ll dp[maxn];
ll sum, tmp, tot, ans;
signed main() {
for(int i = 1; i < 5; i++) c[i] = read();
dp[0] = 1;//直接完全背包
for(int i = 1; i < 5; i++) for(int j = c[i]; j <= maxn; j++) dp[j] += dp[j - c[i]];
T = read();
while(T--) {
ans = 0;
for(int i = 1; i < 5; i++) d[i] = read();
sum = read();
for(int i = 0; i < (1 << 4); i++) {//二进制枚举子集
tmp = sum; op = 0;
for(int j = 1; j < 5; j++) if(1 & (i >> j - 1))
tmp -= c[j] * (d[j] + 1), op ^= 1;
if(tmp < 0) continue;//如果不可能同时重合
if(op) ans -= dp[tmp]; else ans += dp[tmp];//容斥
}
printf("%lld\n", ans);
}
}
迎评:)
——End——