洛谷·bzoj·[HAOI2008]硬币购物

初见安~由于刚好近期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和完全。哦那就尝试完全背包统计方案数吧——用dp[i]表示凑出价值为i的方案数。可是就被次数限制卡住了。所以我们要从次数的限制入手思考怎么转化这个问题。如果只有一种硬币c[i]有次数限制d[i],其他的都可以完全背包,要凑出价值sum的方案数我们可以直接用dp[sum]-dp[sum-c[i]*(d[i]+1)].。很明显选这一种硬币的次数大于等于d[i]+1l了就都不合法了,所以减去从小于等于sum -c[i]*(d[i]+1)转化过来的方案数即可,也就是dp[sum-c[i]*(d[i]+1)]

那么对于多个硬币呢?很容易想到的是:直接挨个像上面这样减就行了吧?然而并不,你会发现答案小了很多甚至是负数。为什么?因为减去了重复的部分。所谓重复,就比如说是:第一个硬币减去的部分里面包含了第二种硬币的不合法情况。那么第二种硬币再直接减就减重了。既然两两重复的多减了那加回来呗,两个同时超出次数的方案数就是dp[sum-c[i]*(d[i]+1) -c[j]*(d[j]+1)]​​​​​​。可是如果还有三种硬币同时有交集的呢?又要减回去;四种呢?还要加回去。所以最后的方案数就是这样容斥出来的。

问题来了——怎么看有没有重复的?怎么枚举?一共就4种硬币打表是很容易的,也就1+4+6+4+1=16种情况。有一种很巧妙的方法是用二进制位来枚举——枚举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——

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值